// One-off generator for the PWA app icons: a rounded remote-control glyph // on a dark background. Pure Node (zlib + zlib.crc32), no image deps. import { deflateSync, crc32 } from "node:zlib"; import { writeFileSync, mkdirSync } from "node:fs"; const BG = [11, 13, 18]; // #0b0d12 const FG = [88, 214, 141]; // accent green const FG_DIM = [46, 58, 74]; function chunk(type, data) { const typeBuf = Buffer.from(type, "ascii"); const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0); const crcInput = Buffer.concat([typeBuf, data]); const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(crcInput) >>> 0, 0); return Buffer.concat([len, typeBuf, data, crc]); } function inRoundedRect(x, y, w, h, left, top, right, bottom, radius) { if (x < left || x > right || y < top || y > bottom) return false; const cx = x < left + radius ? left + radius : x > right - radius ? right - radius : x; const cy = y < top + radius ? top + radius : y > bottom - radius ? bottom - radius : y; const dx = x - cx; const dy = y - cy; return dx * dx + dy * dy <= radius * radius || (x >= left + radius && x <= right - radius) || (y >= top + radius && y <= bottom - radius); } function makeIcon(size) { const margin = Math.round(size * 0.2); const left = margin; const right = size - margin; const top = Math.round(size * 0.08); const bottom = size - top; const radius = Math.round(size * 0.14); const btnRadius = Math.max(2, Math.round(size * 0.035)); const btnCenters = [ [size * 0.5, size * 0.28], [size * 0.5, size * 0.46], [size * 0.32, size * 0.62], [size * 0.68, size * 0.62], [size * 0.5, size * 0.78], ]; const raw = Buffer.alloc(size * (1 + size * 4)); for (let y = 0; y < size; y++) { let rowStart = y * (1 + size * 4); raw[rowStart] = 0; // filter type: none for (let x = 0; x < size; x++) { let [r, g, b] = BG; if (inRoundedRect(x, y, size, size, left, top, right, bottom, radius)) { [r, g, b] = FG_DIM; for (const [bx, by] of btnCenters) { const dx = x - bx; const dy = y - by; if (dx * dx + dy * dy <= btnRadius * btnRadius) { [r, g, b] = FG; break; } } } const off = rowStart + 1 + x * 4; raw[off] = r; raw[off + 1] = g; raw[off + 2] = b; raw[off + 3] = 255; } } const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(size, 0); ihdr.writeUInt32BE(size, 4); ihdr[8] = 8; // bit depth ihdr[9] = 6; // color type RGBA ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0; const idat = deflateSync(raw); const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); return Buffer.concat([ signature, chunk("IHDR", ihdr), chunk("IDAT", idat), chunk("IEND", Buffer.alloc(0)), ]); } mkdirSync(new URL("../public/icons", import.meta.url), { recursive: true }); for (const size of [192, 512]) { const png = makeIcon(size); writeFileSync(new URL(`../public/icons/icon-${size}.png`, import.meta.url), png); console.log(`wrote icon-${size}.png (${png.length} bytes)`); }