// Dependency-free icon generator: draws a "floodlit pitch" crest and writes // favicon.svg + pwa-192x192.png + pwa-512x512.png into public/. // Pure JS (zlib only) so it runs in the minimal Docker build image too — no // ImageMagick / librsvg required. import { deflateSync } from 'node:zlib'; import { writeFileSync, mkdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const OUT = join(dirname(fileURLToPath(import.meta.url)), '..', 'public'); mkdirSync(OUT, { recursive: true }); // ---- palette (matches the dark theme) ---- const BG = [10, 20, 16]; // surface const FIELD = [20, 83, 56]; // pitch green const FIELD_DK = [15, 64, 44]; const LINE = [220, 240, 230]; const BALL = [224, 184, 92]; // gold // ---- pure-JS PNG encoder (RGBA, filter 0) ---- const CRC = (() => { 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[(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 body = Buffer.concat([typeBuf, data]); const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(body), 0); return Buffer.concat([len, body, crc]); } function encodePNG(width, height, rgba) { const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); 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 raw = Buffer.alloc((width * 4 + 1) * height); for (let y = 0; y < height; y++) { raw[y * (width * 4 + 1)] = 0; // filter: none rgba.copy(raw, y * (width * 4 + 1) + 1, y * width * 4, (y + 1) * width * 4); } return Buffer.concat([ sig, chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw, { level: 9 })), chunk('IEND', Buffer.alloc(0)), ]); } // ---- drawing helpers ---- function makeIcon(size) { const buf = Buffer.alloc(size * size * 4); const set = (x, y, [r, g, b], a = 255) => { if (x < 0 || y < 0 || x >= size || y >= size) return; const i = (y * size + x) * 4; const ia = a / 255; buf[i] = Math.round(buf[i] * (1 - ia) + r * ia); buf[i + 1] = Math.round(buf[i + 1] * (1 - ia) + g * ia); buf[i + 2] = Math.round(buf[i + 2] * (1 - ia) + b * ia); buf[i + 3] = Math.max(buf[i + 3], a); }; // full-bleed background (good for maskable) for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) set(x, y, BG); // inset rounded field const pad = Math.round(size * 0.14); const r = Math.round(size * 0.18); const x0 = pad, y0 = pad, x1 = size - pad, y1 = size - pad; const inField = (x, y) => { if (x < x0 || x > x1 || y < y0 || y > y1) return false; const cx = x < x0 + r ? x0 + r : x > x1 - r ? x1 - r : x; const cy = y < y0 + r ? y0 + r : y > y1 - r ? y1 - r : y; return (x - cx) ** 2 + (y - cy) ** 2 <= r * r; }; const cx = size / 2; for (let y = y0; y <= y1; y++) { for (let x = x0; x <= x1; x++) { if (!inField(x, y)) continue; // subtle vertical mowing stripes const stripe = Math.floor((x - x0) / (size * 0.09)) % 2 === 0; set(x, y, stripe ? FIELD : FIELD_DK); } } // markings (white): halfway line, center circle, center spot as gold ball const lw = Math.max(2, Math.round(size * 0.012)); const ring = Math.round(size * 0.17); const ringW = Math.max(2, Math.round(size * 0.016)); for (let y = y0; y <= y1; y++) { for (let x = x0; x <= x1; x++) { if (!inField(x, y)) continue; // halfway line if (Math.abs(x - cx) <= lw / 2) set(x, y, LINE, 220); // center circle ring const d = Math.hypot(x - cx, y - size / 2); if (Math.abs(d - ring) <= ringW / 2) set(x, y, LINE, 220); } } // center gold ball const ballR = Math.round(size * 0.055); for (let y = -ballR; y <= ballR; y++) for (let x = -ballR; x <= ballR; x++) if (x * x + y * y <= ballR * ballR) set(Math.round(cx + x), Math.round(size / 2 + y), BALL); return encodePNG(size, size, buf); } const svg = ` `; writeFileSync(join(OUT, 'favicon.svg'), svg); writeFileSync(join(OUT, 'pwa-192x192.png'), makeIcon(192)); writeFileSync(join(OUT, 'pwa-512x512.png'), makeIcon(512)); console.log('icons → public/favicon.svg, pwa-192x192.png, pwa-512x512.png');