// Generates the extension icons (green "added" band over a red "removed" band) // as PNGs, with zero dependencies — just Node's zlib. Run: node scripts/make-icons.mjs import zlib from 'node:zlib'; import { writeFileSync, mkdirSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const outDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../icons'); 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 crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0); return Buffer.concat([len, typeBuf, data, crc]); } function png(size, rgba) { const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(size, 0); ihdr.writeUInt32BE(size, 4); ihdr[8] = 8; // bit depth ihdr[9] = 6; // color type RGBA const stride = size * 4; const raw = Buffer.alloc((stride + 1) * size); for (let y = 0; y < size; y++) { raw[y * (stride + 1)] = 0; // filter: none rgba.copy(raw, y * (stride + 1) + 1, y * stride, y * stride + stride); } const idat = zlib.deflateSync(raw, { level: 9 }); return Buffer.concat([sig, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', Buffer.alloc(0))]); } function insideRounded(x, y, x0, y0, x1, y1, r) { const cxL = x0 + r, cxR = x1 - r, cyT = y0 + r, cyB = y1 - r; let cx = null, cy = null; if (x < cxL && y < cyT) { cx = cxL; cy = cyT; } else if (x > cxR && y < cyT) { cx = cxR; cy = cyT; } else if (x < cxL && y > cyB) { cx = cxL; cy = cyB; } else if (x > cxR && y > cyB) { cx = cxR; cy = cyB; } if (cx === null) return true; const dx = x - cx, dy = y - cy; return dx * dx + dy * dy <= r * r; } function makeIcon(size) { const px = Buffer.alloc(size * size * 4); // transparent const green = [46, 160, 67, 255]; const red = [215, 58, 73, 255]; const inset = Math.max(1, Math.round(size * 0.06)); const r = Math.round(size * 0.2); const x0 = inset, y0 = inset, x1 = size - 1 - inset, y1 = size - 1 - inset; const mid = Math.floor(size / 2); const gap = Math.max(1, Math.round(size * 0.04)); const set = (x, y, c) => { const i = (y * size + x) * 4; px[i] = c[0]; px[i + 1] = c[1]; px[i + 2] = c[2]; px[i + 3] = c[3]; }; for (let y = y0; y <= y1; y++) { for (let x = x0; x <= x1; x++) { if (!insideRounded(x, y, x0, y0, x1, y1, r)) continue; if (Math.abs(y - mid) < gap) continue; // divider between bands set(x, y, y < mid ? green : red); } } return png(size, px); } mkdirSync(outDir, { recursive: true }); for (const size of [16, 32, 48, 128]) { writeFileSync(path.join(outDir, `icon${size}.png`), makeIcon(size)); } console.log('icons written ->', path.relative(process.cwd(), outDir));