// Generates the PWA icon set with no external dependencies. // A tiny, correct PNG encoder (RGBA, filter 0) built on Node's zlib. // // node scripts/gen-icons.mjs // import { deflateSync } from "node:zlib"; import { mkdirSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const OUT = resolve(__dirname, "../public/icons"); // ---- PNG encoding ----------------------------------------------------------- const CRC_TABLE = (() => { const t = new Uint32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c >>> 0; } return t; })(); function crc32(buf) { let c = 0xffffffff; for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } function chunk(type, data) { const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0); const typeBuf = Buffer.from(type, "ascii"); const crcBuf = Buffer.alloc(4); crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0); return Buffer.concat([len, typeBuf, data, crcBuf]); } function encodePng(width, height, rgba) { const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4); ihdr[8] = 8; // bit depth ihdr[9] = 6; // color type: RGBA const stride = width * 4; const raw = Buffer.alloc((stride + 1) * height); for (let y = 0; y < height; y++) { raw[y * (stride + 1)] = 0; // filter: none rgba.copy(raw, y * (stride + 1) + 1, y * stride, y * stride + stride); } const idat = deflateSync(raw, { level: 9 }); return Buffer.concat([sig, chunk("IHDR", ihdr), chunk("IDAT", idat), chunk("IEND", Buffer.alloc(0))]); } // ---- Drawing ---------------------------------------------------------------- function makeCanvas(size) { return { size, data: Buffer.alloc(size * size * 4, 0) }; } function blend(canvas, x, y, [r, g, b], a) { if (x < 0 || y < 0 || x >= canvas.size || y >= canvas.size || a <= 0) return; const i = (y * canvas.size + x) * 4; const d = canvas.data; const sa = Math.min(1, a); const da = d[i + 3] / 255; const outA = sa + da * (1 - sa); if (outA === 0) return; d[i] = Math.round((r * sa + d[i] * da * (1 - sa)) / outA); d[i + 1] = Math.round((g * sa + d[i + 1] * da * (1 - sa)) / outA); d[i + 2] = Math.round((b * sa + d[i + 2] * da * (1 - sa)) / outA); d[i + 3] = Math.round(outA * 255); } // Signed distance to a rounded rectangle, used for antialiased edges. function roundedRectSdf(px, py, cx, cy, hw, hh, r) { const qx = Math.abs(px - cx) - (hw - r); const qy = Math.abs(py - cy) - (hh - r); const ax = Math.max(qx, 0); const ay = Math.max(qy, 0); return Math.sqrt(ax * ax + ay * ay) + Math.min(Math.max(qx, qy), 0) - r; } function fillRoundedRect(canvas, cx, cy, hw, hh, r, color, alpha = 1) { const { size } = canvas; for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const d = roundedRectSdf(x + 0.5, y + 0.5, cx, cy, hw, hh, r); const cov = Math.min(1, Math.max(0, 0.5 - d)); if (cov > 0) blend(canvas, x, y, color, cov * alpha); } } } function gradientAt(y, size, top, bottom) { const t = Math.max(0, Math.min(1, y / (size - 1))); return [ Math.round(top[0] + (bottom[0] - top[0]) * t), Math.round(top[1] + (bottom[1] - top[1]) * t), Math.round(top[2] + (bottom[2] - top[2]) * t), ]; } function fillRoundedRectGradient(canvas, cx, cy, hw, hh, r, top, bottom) { const { size } = canvas; for (let y = 0; y < size; y++) { const color = gradientAt(y + 0.5, size, top, bottom); for (let x = 0; x < size; x++) { const d = roundedRectSdf(x + 0.5, y + 0.5, cx, cy, hw, hh, r); const cov = Math.min(1, Math.max(0, 0.5 - d)); if (cov > 0) blend(canvas, x, y, color, cov); } } } function fillCircle(canvas, cx, cy, r, color, alpha = 1) { const { size } = canvas; for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const d = Math.hypot(x + 0.5 - cx, y + 0.5 - cy) - r; const cov = Math.min(1, Math.max(0, 0.5 - d)); if (cov > 0) blend(canvas, x, y, color, cov * alpha); } } } // derez.ai palette const TOP = [0x00, 0x35, 0x45]; const BOTTOM = [0x00, 0x20, 0x2e]; const CYAN = [0x00, 0xf5, 0xff]; // Draws the derez.ai mark: a teal tile with the cyan terminal glyph. function drawIcon(size, { maskable }) { const canvas = makeCanvas(size); const s = size; const pad = maskable ? 0 : s * 0.05; const radius = maskable ? 0 : s * 0.24; fillRoundedRectGradient(canvas, s / 2, s / 2, s / 2 - pad, s / 2 - pad, radius, TOP, BOTTOM); // The glyph is designed in a 34x34 box; scale it into the tile. const k = (s * (maskable ? 0.7 : 0.84)) / 34; const ox = (s - 34 * k) / 2; const oy = (s - 34 * k) / 2; const X = (v) => ox + v * k; const Y = (v) => oy + v * k; const stroke = 1.8 * k; // Monitor outline: cyan rounded rect, then erase the interior with the tile gradient. const mcx = X(17); const mcy = Y(18.5); const mhw = 11 * k; const mhh = 7.5 * k; const mr = 2.5 * k; fillRoundedRect(canvas, mcx, mcy, mhw, mhh, mr, CYAN, 1); fillRoundedRectGradient( canvas, mcx, mcy, mhw - stroke, mhh - stroke, Math.max(0, mr - stroke), TOP, BOTTOM, ); // Antenna stalk + dot. fillRoundedRect(canvas, X(17), Y(9), stroke / 2, 2 * k, stroke / 2, CYAN, 1); fillCircle(canvas, X(17), Y(5.5), 1.5 * k, CYAN); // Indicator dots. fillCircle(canvas, X(10), Y(15.5), 1 * k, CYAN); fillCircle(canvas, X(14), Y(15.5), 1 * k, CYAN); // Bottom bar. fillRoundedRect(canvas, X(17), Y(20), 7 * k, 0.75 * k, 0.75 * k, CYAN, 1); return canvas; } function write(name, canvas) { writeFileSync(resolve(OUT, name), encodePng(canvas.size, canvas.size, canvas.data)); console.log("wrote", name, `${canvas.size}x${canvas.size}`); } mkdirSync(OUT, { recursive: true }); write("icon-192.png", drawIcon(192, { maskable: false })); write("icon-512.png", drawIcon(512, { maskable: false })); write("maskable-512.png", drawIcon(512, { maskable: true })); write("apple-touch-icon.png", drawIcon(180, { maskable: false })); // Matching SVG favicon (the derez.ai mark, crisp at any size). const favicon = ` `; writeFileSync(resolve(__dirname, "../public/favicon.svg"), favicon); console.log("wrote favicon.svg");