Files
Mitchell-JohnsonandClaude Fable 5.1 3a882e2d84 New app icon and login logo: the HamLab penguin
Every icon is rendered from web/scripts/logo-source.png by scripts/icons.mjs: trimmed to
the artwork's own edge, transparent corners for the browser icons, a dark ground from the
artwork's palette for iOS (which paints transparency black) and for Android's maskable
icon, where the whole artwork sits inside the 80% safe circle. The login screen no longer
crops the logo to a circle or repeats the wordmark. Service worker cache bumped so
installed apps fetch the new files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 19:41:47 -07:00

69 lines
3.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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, loadImage } = require('/home/core/projects/calorie-counter/node_modules/canvas')
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 }
}
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`)