/**
* Generates the hub's PWA icons — the catalog's six-diamond color rule on the
* night paper — as raw PNGs (tiny hand-rolled encoder, no dependencies).
* Run: node scripts/make-icons.mjs (writes public/icons/)
*/
import { deflateSync } from "node:zlib";
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const OUT = join(dirname(fileURLToPath(import.meta.url)), "..", "public", "icons");
mkdirSync(OUT, { recursive: true });
const PAPER = [0x17, 0x15, 0x20];
const DIAMONDS = [
[0xe8, 0xa0, 0x9a], // rose
[0xf0, 0xb8, 0x77], // peach
[0x93, 0xc1, 0xa5], // sage
[0xb3, 0xa4, 0xd4], // lilac
[0x8f, 0xb8, 0xd8], // sky
[0xe6, 0xc2, 0x6e], // gold
];
function crc32(buf) {
let c = ~0;
for (const b of buf) {
c ^= b;
for (let i = 0; i < 8; i++) c = (c >>> 1) ^ (0xedb88320 & -(c & 1));
}
return ~c >>> 0;
}
function chunk(type, data) {
const t = Buffer.from(type, "ascii");
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length);
const crc = Buffer.alloc(4);
crc.writeUInt32BE(crc32(Buffer.concat([t, data])));
return Buffer.concat([len, t, data, crc]);
}
function png(size, pixels /* RGB Buffer */) {
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(size, 0);
ihdr.writeUInt32BE(size, 4);
ihdr[8] = 8; // bit depth
ihdr[9] = 2; // color type RGB
const raw = Buffer.alloc(size * (size * 3 + 1));
for (let y = 0; y < size; y++) {
pixels.copy(raw, y * (size * 3 + 1) + 1, y * size * 3, (y + 1) * size * 3);
}
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk("IHDR", ihdr),
chunk("IDAT", deflateSync(raw, { level: 9 })),
chunk("IEND", Buffer.alloc(0)),
]);
}
/** The motif: two rows of three diamonds (|x|+|y| ≤ r), centered. */
function draw(size, shrink = 0) {
const px = Buffer.alloc(size * size * 3);
for (let i = 0; i < size * size; i++) px.set(PAPER, i * 3);
const u = size / 100;
const r = (13 - shrink) * u;
const centers = [];
for (const cy of [40, 62]) for (const cx of [30, 50, 70]) centers.push([cx * u, cy * u]);
centers.forEach(([cx, cy], i) => {
const col = DIAMONDS[i];
for (let y = Math.floor(cy - r); y <= cy + r; y++) {
for (let x = Math.floor(cx - r); x <= cx + r; x++) {
if (x < 0 || y < 0 || x >= size || y >= size) continue;
if (Math.abs(x - cx) + Math.abs(y - cy) <= r) px.set(col, (y * size + x) * 3);
}
}
});
return png(size, px);
}
writeFileSync(join(OUT, "icon-192.png"), draw(192));
writeFileSync(join(OUT, "icon-512.png"), draw(512));
writeFileSync(join(OUT, "icon-512-maskable.png"), draw(512, 4)); // safe-zone shrink
writeFileSync(join(OUT, "apple-touch-icon.png"), draw(180));
console.log("icons written to", OUT);