diff --git a/web/public/icons/apple-touch-icon.png b/web/public/icons/apple-touch-icon.png index 29f27f2..2396703 100644 Binary files a/web/public/icons/apple-touch-icon.png and b/web/public/icons/apple-touch-icon.png differ diff --git a/web/public/icons/icon-192.png b/web/public/icons/icon-192.png index aa4dd6e..c999ea4 100644 Binary files a/web/public/icons/icon-192.png and b/web/public/icons/icon-192.png differ diff --git a/web/public/icons/icon-512-maskable.png b/web/public/icons/icon-512-maskable.png index dae4909..2b2486c 100644 Binary files a/web/public/icons/icon-512-maskable.png and b/web/public/icons/icon-512-maskable.png differ diff --git a/web/public/icons/icon-512.png b/web/public/icons/icon-512.png index 6f1b20f..785a579 100644 Binary files a/web/public/icons/icon-512.png and b/web/public/icons/icon-512.png differ diff --git a/web/public/icons/logo.png b/web/public/icons/logo.png index b8851be..79e060b 100644 Binary files a/web/public/icons/logo.png and b/web/public/icons/logo.png differ diff --git a/web/public/sw.js b/web/public/sw.js index 5950ece..975fdb3 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -1,5 +1,5 @@ // Ham Lab service worker: cache hashed assets, never the API, network-first for the shell. -const CACHE = 'hamlab-v2'; +const CACHE = 'hamlab-v3'; // bumped when files under /icons/ change: they are served cache-first self.addEventListener('install', () => self.skipWaiting()); self.addEventListener('activate', (e) => { e.waitUntil(caches.keys().then((ks) => Promise.all(ks.filter((k) => k !== CACHE).map((k) => caches.delete(k)))).then(() => self.clients.claim())); diff --git a/web/scripts/icons.mjs b/web/scripts/icons.mjs index 9f102f7..4f00eff 100644 --- a/web/scripts/icons.mjs +++ b/web/scripts/icons.mjs @@ -1,26 +1,68 @@ -// Renders the PWA icons with node-canvas (borrowed from ../../calorie-counter/node_modules). +// Renders the PWA icon set and the login logo from scripts/logo-source.png, with node-canvas +// (borrowed from ../../calorie-counter/node_modules). node scripts/icons.mjs +// +// The artwork is a rounded square on a transparent background, a little off-centre in its +// file. It is trimmed to its own edge, then drawn three ways: +// icon-192 / icon-512 / logo the artwork alone, transparent corners, filling the square +// apple-touch-icon iOS paints transparency black and cuts its own, tighter +// corners: a dark ground from the artwork's own palette, the +// artwork inset a little so its neon ring clears that cut +// icon-512-maskable Android may crop to a circle 80% across: the whole artwork +// sits inside that circle, on the same dark ground to the edge import { createRequire } from 'node:module' import { writeFileSync } from 'node:fs' const require = createRequire(import.meta.url) -const { createCanvas } = require('/home/core/projects/calorie-counter/node_modules/canvas') +const { createCanvas, loadImage } = require('/home/core/projects/calorie-counter/node_modules/canvas') -function draw(size, { pad = 0, radius = 0 }) { - const c = createCanvas(size, size), g = c.getContext('2d') - g.fillStyle = '#111418' - if (radius) { g.beginPath(); g.roundRect(0, 0, size, size, radius); g.fill() } else g.fillRect(0, 0, size, size) - const inner = size - pad * 2 - g.fillStyle = '#e3a33b' - g.font = `bold ${inner * 0.62}px "DejaVu Serif", serif` - g.textAlign = 'center'; g.textBaseline = 'middle' - g.fillText('H', size / 2, size / 2 + inner * 0.03) - // prompt caret - g.fillStyle = '#58a6ff' - g.font = `bold ${inner * 0.22}px "DejaVu Sans Mono", monospace` - g.fillText('>_', size / 2 + inner * 0.27, size / 2 + inner * 0.27) - return c.toBuffer('image/png') +const img = await loadImage(new URL('./logo-source.png', import.meta.url).pathname) +const src = createCanvas(img.width, img.height), sg = src.getContext('2d') +sg.drawImage(img, 0, 0) +const px = sg.getImageData(0, 0, img.width, img.height), d = px.data +// The export left the artwork at alpha 252 and a stray alpha-1 speck outside it: make what +// is there fully there, what isn't fully gone, and keep the anti-aliased edge between. +let x0 = img.width, y0 = img.height, x1 = 0, y1 = 0 +for (let y = 0; y < img.height; y++) for (let x = 0; x < img.width; x++) { + const i = (y * img.width + x) * 4 + 3 + if (d[i] >= 240) d[i] = 255; else if (d[i] < 8) d[i] = 0 + if (d[i] > 128) { if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y } } -writeFileSync('public/icons/icon-192.png', draw(192, {})) -writeFileSync('public/icons/icon-512.png', draw(512, {})) -writeFileSync('public/icons/icon-512-maskable.png', draw(512, { pad: 80 })) -writeFileSync('public/icons/apple-touch-icon.png', draw(180, {})) -console.log('icons written') +sg.putImageData(px, 0, 0) +// a square around the artwork's own centre, as wide as its longer side: never stretched +const side = Math.max(x1 - x0, y1 - y0) + 1, cx = (x0 + x1) / 2, cy = (y0 + y1) / 2 +const art = createCanvas(side, side) +art.getContext('2d').drawImage(src, Math.round(cx - side / 2), Math.round(cy - side / 2), side, side, 0, 0, side, side) + +// Shrinking six-fold in one step aliases the fine lines; halving until close does not. +function shrink(from, size) { + let cur = from + while (cur.width / 2 >= size) { + const next = createCanvas(Math.ceil(cur.width / 2), Math.ceil(cur.height / 2)), g = next.getContext('2d') + g.imageSmoothingEnabled = true; g.imageSmoothingQuality = 'high'; g.quality = 'best'; g.patternQuality = 'best' + g.drawImage(cur, 0, 0, next.width, next.height) + cur = next + } + return cur +} + +function draw(size, { scale = 1, ground = false } = {}) { + const c = createCanvas(size, size), g = c.getContext('2d') + if (ground) { // the artwork's own night-green, lit a little behind the middle + const bg = g.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size * 0.72) + bg.addColorStop(0, '#0b2e2a'); bg.addColorStop(1, '#03110f') + g.fillStyle = bg; g.fillRect(0, 0, size, size) + } + const w = Math.round(size * scale), from = shrink(art, w) + g.imageSmoothingEnabled = true; g.imageSmoothingQuality = 'high'; g.quality = 'best'; g.patternQuality = 'best' + g.drawImage(from, (size - w) / 2, (size - w) / 2, w, w) + return c.toBuffer('image/png', { compressionLevel: 9 }) +} + +const out = { + 'icon-192.png': draw(192), + 'icon-512.png': draw(512), + 'logo.png': draw(480), + 'apple-touch-icon.png': draw(180, { scale: 0.95, ground: true }), + 'icon-512-maskable.png': draw(512, { scale: 0.68, ground: true }), +} +for (const [name, buf] of Object.entries(out)) { writeFileSync(new URL('../public/icons/' + name, import.meta.url).pathname, buf); console.log(name, Math.round(buf.length / 1024) + ' KB') } +console.log(`artwork ${x1 - x0 + 1}×${y1 - y0 + 1} at ${x0},${y0} → ${side} px square`) diff --git a/web/scripts/logo-source.png b/web/scripts/logo-source.png new file mode 100644 index 0000000..146393a Binary files /dev/null and b/web/scripts/logo-source.png differ diff --git a/web/src/Login.tsx b/web/src/Login.tsx index 6b4026a..5ea67a4 100644 --- a/web/src/Login.tsx +++ b/web/src/Login.tsx @@ -17,7 +17,7 @@ export default function Login({ onLogin }: { onLogin: () => void }) { return (
- +

Ham Lab

setPw(e.target.value)} disabled={busy} /> diff --git a/web/src/styles.css b/web/src/styles.css index f15832f..278fc7c 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -16,7 +16,9 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi /* login */ .login { display: flex; flex-direction: column; gap: 10px; width: min(320px, 90vw); } -.login-logo { width: 160px; height: 160px; align-self: center; border-radius: 50%; margin-bottom: 4px; } +/* the logo carries the wordmark now; the heading stays for screen readers and is not drawn twice */ +.login h1 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } +.login-logo { width: 176px; height: 176px; align-self: center; margin-bottom: 4px; } /* the artwork brings its own shape: no circular crop */ .login h1 { text-align: center; margin: 0 0 8px; font-weight: 600; color: var(--accent); letter-spacing: .5px; } .login input { padding: 10px 12px; background: var(--bg2); border: 1px solid var(--border); border-radius: 6px; color: var(--fg); font-size: 16px; } .login button, .primary { padding: 10px 12px; background: var(--accent); color: var(--btn-fg); border-radius: 6px; font-weight: 600; }