// 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`)