// Draws the app icons (a waveform on terracotta) as PNGs with no dependencies. import { deflateSync } from 'node:zlib'; import { writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; const crcTable = Array.from({ length: 256 }, (_, n) => { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; return c >>> 0; }); const crc = (buf) => { let c = ~0; for (const b of buf) c = crcTable[(c ^ b) & 255] ^ (c >>> 8); return ~c >>> 0; }; const chunk = (type, data) => { const len = Buffer.alloc(4); len.writeUInt32BE(data.length); const td = Buffer.concat([Buffer.from(type), data]); const c = Buffer.alloc(4); c.writeUInt32BE(crc(td)); return Buffer.concat([len, td, c]); }; function draw(size, maskable) { const ss = 3, S = size * ss; // supersample for smooth edges const BG = [180, 83, 42], FG = [244, 239, 230]; const heights = [0.18, 0.36, 0.56, 0.36, 0.24, 0.46, 0.2]; const scale = maskable ? 0.62 : 0.8; const n = heights.length, w = (S * scale) / (n * 1.7); const total = n * w + (n - 1) * w * 0.7; const x0 = (S - total) / 2, R = S * 0.22; const inRound = (x, y, x1, y1, x2, y2, r) => { if (x < x1 || x > x2 || y < y1 || y > y2) return false; const cx = Math.min(Math.max(x, x1 + r), x2 - r), cy = Math.min(Math.max(y, y1 + r), y2 - r); return (x - cx) ** 2 + (y - cy) ** 2 <= r * r; }; const px = (x, y) => { if (!maskable && !inRound(x, y, 0, 0, S, S, R)) return null; for (let i = 0; i < n; i++) { const bx = x0 + i * w * 1.7, hh = S * heights[i] * scale; if (inRound(x, y, bx, S / 2 - hh / 2, bx + w, S / 2 + hh / 2, w / 2)) return FG; } return BG; }; const raw = Buffer.alloc(size * (size * 4 + 1)); for (let y = 0; y < size; y++) { raw[y * (size * 4 + 1)] = 0; for (let x = 0; x < size; x++) { let r = 0, g = 0, b = 0, a = 0; for (let sy = 0; sy < ss; sy++) for (let sx = 0; sx < ss; sx++) { const c = px(x * ss + sx + 0.5, y * ss + sy + 0.5); if (c) { r += c[0]; g += c[1]; b += c[2]; a++; } } const o = y * (size * 4 + 1) + 1 + x * 4; if (a) { raw[o] = r / a; raw[o + 1] = g / a; raw[o + 2] = b / a; } raw[o + 3] = Math.round((a / (ss * ss)) * 255); } } const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(size, 0); ihdr.writeUInt32BE(size, 4); ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0; return Buffer.concat([ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw, { level: 9 })), chunk('IEND', Buffer.alloc(0)), ]); } export function writeIcons(dir) { mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, 'icon-192.png'), draw(192, false)); writeFileSync(join(dir, 'icon-512.png'), draw(512, false)); writeFileSync(join(dir, 'maskable-512.png'), draw(512, true)); }