import { deflateSync } from "node:zlib"; import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; const [packageRoot, uiImageRoot] = process.argv.slice(2); if (!packageRoot || !uiImageRoot) { throw new Error("usage: node generate-fnos-icons.mjs package-root ui-image-root"); } const crcTable = new Uint32Array(256); for (let index = 0; index < 256; index++) { let value = index; for (let bit = 0; bit < 8; bit++) { value = (value & 1) !== 0 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; } crcTable[index] = value >>> 0; } function crc32(buffer) { let value = 0xffffffff; for (const byte of buffer) { value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8); } return (value ^ 0xffffffff) >>> 0; } function chunk(type, data) { const typeBuffer = Buffer.from(type, "ascii"); const length = Buffer.alloc(4); length.writeUInt32BE(data.length); const checksum = Buffer.alloc(4); checksum.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data]))); return Buffer.concat([length, typeBuffer, data, checksum]); } function roundedRectangleDistance(x, y, left, top, right, bottom, radius) { const centerX = (left + right) / 2; const centerY = (top + bottom) / 2; const halfWidth = (right - left) / 2 - radius; const halfHeight = (bottom - top) / 2 - radius; const dx = Math.max(Math.abs(x - centerX) - halfWidth, 0); const dy = Math.max(Math.abs(y - centerY) - halfHeight, 0); return Math.hypot(dx, dy) - radius; } function render(size) { const pixels = Buffer.alloc(size * size * 4); const samples = 3; for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const rgba = [0, 0, 0, 0]; for (let sy = 0; sy < samples; sy++) { for (let sx = 0; sx < samples; sx++) { const px = x + (sx + 0.5) / samples; const py = y + (sy + 0.5) / samples; const scale = size / 256; let color = [0, 0, 0, 0]; const background = roundedRectangleDistance(px, py, 12 * scale, 12 * scale, 244 * scale, 244 * scale, 50 * scale); if (background <= 0) { const mix = Math.min(1, Math.max(0, (px + py) / (512 * scale))); color = [Math.round(27 + 33 * mix), Math.round(94 + 66 * mix), Math.round(180 + 49 * mix), 255]; } const body = roundedRectangleDistance(px, py, 51 * scale, 75 * scale, 190 * scale, 185 * scale, 22 * scale); if (body <= 0) { color = [245, 249, 255, 255]; } const lensDistance = Math.hypot(px - 120 * scale, py - 130 * scale); if (lensDistance <= 36 * scale) { color = lensDistance <= 23 * scale ? [42, 109, 196, 255] : [128, 185, 239, 255]; } const viewfinder = roundedRectangleDistance(px, py, 73 * scale, 56 * scale, 122 * scale, 86 * scale, 9 * scale); if (viewfinder <= 0) { color = [232, 241, 253, 255]; } if (px >= 190 * scale && px <= 222 * scale && py >= 101 * scale && py <= 159 * scale) { const edge = Math.abs(py - 130 * scale) / (29 * scale); const leftEdge = 190 * scale + edge * 16 * scale; if (px >= leftEdge) { color = [237, 244, 253, 255]; } } const statusDistance = Math.hypot(px - 165 * scale, py - 98 * scale); if (statusDistance <= 9 * scale) { color = [244, 85, 93, 255]; } for (let channel = 0; channel < 4; channel++) { rgba[channel] += color[channel]; } } } const offset = (y * size + x) * 4; for (let channel = 0; channel < 4; channel++) { pixels[offset + channel] = Math.round(rgba[channel] / (samples * samples)); } } } const scanlines = Buffer.alloc((size * 4 + 1) * size); for (let row = 0; row < size; row++) { const target = row * (size * 4 + 1); scanlines[target] = 0; pixels.copy(scanlines, target + 1, row * size * 4, (row + 1) * size * 4); } const header = Buffer.alloc(13); header.writeUInt32BE(size, 0); header.writeUInt32BE(size, 4); header[8] = 8; header[9] = 6; return Buffer.concat([ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk("IHDR", header), chunk("IDAT", deflateSync(scanlines, { level: 9 })), chunk("IEND", Buffer.alloc(0)) ]); } mkdirSync(packageRoot, { recursive: true }); mkdirSync(uiImageRoot, { recursive: true }); const icon64 = render(64); const icon256 = render(256); writeFileSync(join(packageRoot, "ICON.PNG"), icon64); writeFileSync(join(packageRoot, "ICON_256.PNG"), icon256); writeFileSync(join(uiImageRoot, "icon_64.png"), icon64); writeFileSync(join(uiImageRoot, "icon_256.png"), icon256);