const fs = require('fs'); const path = require('path'); const zlib = require('zlib'); function crc32(buf) { let c = ~0 >>> 0; for (let i = 0; i < buf.length; i++) { c ^= buf[i]; for (let k = 0; k < 8; k++) c = (c >>> 1) ^ (0xEDB88320 & -(c & 1)); } return ~c >>> 0; } function chunk(type, data) { const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0); const t = Buffer.from(type, 'ascii'); const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(Buffer.concat([t, data])), 0); return Buffer.concat([len, t, data, crc]); } function encodePNG(rgba, w, h) { const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0; const stride = w * 4; const raw = Buffer.alloc((stride + 1) * h); for (let y = 0; y < h; y++) { raw[y * (stride + 1)] = 0; 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 makeICO(images) { const n = images.length; const header = Buffer.alloc(6); header.writeUInt16LE(0, 0); header.writeUInt16LE(1, 2); header.writeUInt16LE(n, 4); let offset = 6 + 16 * n; const entries = []; const datas = []; for (const im of images) { const e = Buffer.alloc(16); e[0] = im.w >= 256 ? 0 : im.w; e[1] = im.h >= 256 ? 0 : im.h; e[2] = 0; e[3] = 0; e.writeUInt16LE(1, 4); e.writeUInt16LE(32, 6); e.writeUInt32LE(im.png.length, 8); e.writeUInt32LE(offset, 12); entries.push(e); datas.push(im.png); offset += im.png.length; } return Buffer.concat([header, ...entries, ...datas]); } function inRR(x, y, w, h, r) { if (x < r && y < r) return Math.hypot(r - x, r - y) <= r; if (x > w - 1 - r && y < r) return Math.hypot(w - 1 - r - x, r - y) <= r; if (x < r && y > h - 1 - r) return Math.hypot(r - x, h - 1 - r - y) <= r; if (x > w - 1 - r && y > h - 1 - r) return Math.hypot(w - 1 - r - x, h - 1 - r - y) <= r; return x >= 0 && y >= 0 && x < w && y < h; } function inCapsule(x, y, x1, x2, cy, r) { const clampX = Math.max(x1, Math.min(x2, x)); const dx = x - clampX; const dy = y - cy; return dx * dx + dy * dy <= r * r; } function render(W) { const buf = Buffer.alloc(W * W * 4); const rad = W * 0.235; const A = [34, 211, 238]; const B = [99, 102, 241]; const bars = [ { x1: W * 0.30, x2: W * 0.74, cy: W * 0.40, r: W * 0.030 }, { x1: W * 0.22, x2: W * 0.80, cy: W * 0.515, r: W * 0.030 }, { x1: W * 0.36, x2: W * 0.70, cy: W * 0.63, r: W * 0.030 }, ]; for (let y = 0; y < W; y++) { for (let x = 0; x < W; x++) { const i = (y * W + x) * 4; if (!inRR(x, y, W, W, rad)) { buf[i + 3] = 0; continue; } const t = (x + y) / (2 * (W - 1)); let r = A[0] + (B[0] - A[0]) * t; let g = A[1] + (B[1] - A[1]) * t; let b = A[2] + (B[2] - A[2]) * t; let onBar = false; for (const bar of bars) { if (inCapsule(x, y, bar.x1, bar.x2, bar.cy, bar.r)) { onBar = true; break; } } if (onBar) { r = 255; g = 255; b = 255; } buf[i] = r | 0; buf[i + 1] = g | 0; buf[i + 2] = b | 0; buf[i + 3] = 255; } } return buf; } function downsampleAvg(buf, W, ss) { const size = W / ss; const out = Buffer.alloc(size * size * 4); for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { let r = 0, g = 0, b = 0, a = 0; for (let dy = 0; dy < ss; dy++) { for (let dx = 0; dx < ss; dx++) { const i = ((y * ss + dy) * W + (x * ss + dx)) * 4; r += buf[i]; g += buf[i + 1]; b += buf[i + 2]; a += buf[i + 3]; } } const n = ss * ss; const o = (y * size + x) * 4; out[o] = Math.round(r / n); out[o + 1] = Math.round(g / n); out[o + 2] = Math.round(b / n); out[o + 3] = Math.round(a / n); } } return out; } function nearest(src, sw, sh, dw, dh) { const out = Buffer.alloc(dw * dh * 4); for (let y = 0; y < dh; y++) { for (let x = 0; x < dw; x++) { const sx = Math.min(sw - 1, Math.round((x * sw) / dw)); const sy = Math.min(sh - 1, Math.round((y * sh) / dh)); const si = (sy * sw + sx) * 4; const di = (y * dw + x) * 4; out[di] = src[si]; out[di + 1] = src[si + 1]; out[di + 2] = src[si + 2]; out[di + 3] = src[si + 3]; } } return out; } const SS = 4; const BASE = 256; const hi = render(BASE * SS); const big = downsampleAvg(hi, BASE * SS, SS); const png256 = encodePNG(big, BASE, BASE); const png48 = encodePNG(nearest(big, BASE, BASE, 48, 48), 48, 48); const png32 = encodePNG(nearest(big, BASE, BASE, 32, 32), 32, 32); const png16 = encodePNG(nearest(big, BASE, BASE, 16, 16), 16, 16); const ico = makeICO([ { png: png256, w: 256, h: 256 }, { png: png48, w: 48, h: 48 }, { png: png32, w: 32, h: 32 }, { png: png16, w: 16, h: 16 }, ]); const outDir = path.join(__dirname, '..', 'build'); fs.mkdirSync(outDir, { recursive: true }); fs.writeFileSync(path.join(outDir, 'icon.ico'), ico); console.log('icon.ico written:', path.join(outDir, 'icon.ico'), ico.length, 'bytes');