Spaces:
Runtime error
Runtime error
| // Generates the desktop icons from the brand mark (client/public/favicon.svg | |
| // design: dot inside a broken routing ring) without any image dependencies β | |
| // pure-Node PNG encoding (zlib IDAT + hand-rolled CRC32), 4x supersampled. | |
| // | |
| // Outputs: | |
| // assets/trayTemplate.png 16x16 black-on-transparent (macOS template) | |
| // assets/trayTemplate@2x.png 32x32 | |
| // assets/appicon_1024.png full-color app icon (icns built via iconutil) | |
| import { deflateSync } from 'node:zlib'; | |
| import fs from 'node:fs'; | |
| import path from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const assets = path.resolve(__dirname, '../assets'); | |
| fs.mkdirSync(assets, { recursive: true }); | |
| // ββ PNG encoder ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 (const b of buf) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8); | |
| return (c ^ 0xffffffff) >>> 0; | |
| } | |
| function chunk(type, data) { | |
| const len = Buffer.alloc(4); | |
| len.writeUInt32BE(data.length); | |
| const body = Buffer.concat([Buffer.from(type, 'ascii'), data]); | |
| const crc = Buffer.alloc(4); | |
| crc.writeUInt32BE(crc32(body)); | |
| return Buffer.concat([len, body, crc]); | |
| } | |
| function encodePng(rgba, w, h) { | |
| const ihdr = Buffer.alloc(13); | |
| ihdr.writeUInt32BE(w, 0); | |
| ihdr.writeUInt32BE(h, 4); | |
| ihdr[8] = 8; // bit depth | |
| ihdr[9] = 6; // color type RGBA | |
| const raw = Buffer.alloc((w * 4 + 1) * h); | |
| for (let y = 0; y < h; y++) { | |
| raw[y * (w * 4 + 1)] = 0; // filter: none | |
| rgba.copy(raw, y * (w * 4 + 1) + 1, y * w * 4, (y + 1) * w * 4); | |
| } | |
| return Buffer.concat([ | |
| Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), | |
| chunk('IHDR', ihdr), | |
| chunk('IDAT', deflateSync(raw, { level: 9 })), | |
| chunk('IEND', Buffer.alloc(0)), | |
| ]); | |
| } | |
| // ββ rasterizer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Renders with 4x supersampling. `shapes` is an ordered list painted | |
| // back-to-front; each returns coverage [0..1] at a sample point plus a color. | |
| function render(size, shapes) { | |
| const SS = 4; | |
| const S = size * SS; | |
| const img = Buffer.alloc(size * size * 4); | |
| for (let py = 0; py < size; py++) { | |
| for (let px = 0; px < size; px++) { | |
| let r = 0, g = 0, b = 0, a = 0; | |
| for (let sy = 0; sy < SS; sy++) { | |
| for (let sx = 0; sx < SS; sx++) { | |
| const x = (px * SS + sx + 0.5) / S; // normalized [0,1] | |
| const y = (py * SS + sy + 0.5) / S; | |
| // composite shapes front-to-back at this sample | |
| let sr = 0, sg = 0, sb = 0, sa = 0; | |
| for (const sh of shapes) { | |
| const cov = sh.cover(x, y); | |
| if (cov <= 0) continue; | |
| const [cr, cg, cb, ca] = sh.color; | |
| const srcA = ca * cov; | |
| sr = cr * srcA + sr * (1 - srcA); | |
| sg = cg * srcA + sg * (1 - srcA); | |
| sb = cb * srcA + sb * (1 - srcA); | |
| sa = srcA + sa * (1 - srcA); | |
| } | |
| r += sr; g += sg; b += sb; a += sa; | |
| } | |
| } | |
| const n = SS * SS; | |
| const idx = (py * size + px) * 4; | |
| const alpha = a / n; | |
| img[idx] = alpha > 0 ? Math.round((r / n / alpha) * 255) : 0; | |
| img[idx + 1] = alpha > 0 ? Math.round((g / n / alpha) * 255) : 0; | |
| img[idx + 2] = alpha > 0 ? Math.round((b / n / alpha) * 255) : 0; | |
| img[idx + 3] = Math.round(alpha * 255); | |
| } | |
| } | |
| return encodePng(img, size, size); | |
| } | |
| const dist = (x, y, cx, cy) => Math.hypot(x - cx, y - cy); | |
| // Ring with a gap. gapCenter/gapWidth in degrees, standard math orientation. | |
| function ringCover(x, y, cx, cy, radius, stroke, gapCenterDeg, gapWidthDeg) { | |
| const d = Math.abs(dist(x, y, cx, cy) - radius); | |
| if (d > stroke / 2) return 0; | |
| let ang = (Math.atan2(cy - y, x - cx) * 180) / Math.PI; // y-flip: screenβmath | |
| let delta = Math.abs(((ang - gapCenterDeg + 540) % 360) - 180); | |
| return delta < gapWidthDeg / 2 ? 0 : 1; | |
| } | |
| const circle = (cx, cy, r, color) => ({ | |
| color, | |
| cover: (x, y) => (dist(x, y, cx, cy) <= r ? 1 : 0), | |
| }); | |
| const ring = (cx, cy, r, stroke, gapC, gapW, color) => ({ | |
| color, | |
| cover: (x, y) => ringCover(x, y, cx, cy, r, stroke, gapC, gapW), | |
| }); | |
| const roundedRect = (x0, y0, x1, y1, rad, color) => ({ | |
| color, | |
| cover: (x, y) => { | |
| const qx = Math.max(x0 + rad - x, 0, x - (x1 - rad)); | |
| const qy = Math.max(y0 + rad - y, 0, y - (y1 - rad)); | |
| if (x < x0 || x > x1 || y < y0 || y > y1) return 0; | |
| return Math.hypot(qx, qy) <= rad ? 1 : 0; | |
| }, | |
| }); | |
| // ββ tray template (black on transparent; macOS tints it) ββββββββββββββββββ | |
| // Slightly chunkier than the 64px favicon so it reads at 16px. | |
| const BLACK = [0, 0, 0, 1]; | |
| const trayShapes = [ | |
| ring(0.5, 0.5, 0.36, 0.115, 45, 80, BLACK), // gap top-right, like the mark | |
| circle(0.5, 0.5, 0.185, BLACK), | |
| ]; | |
| fs.writeFileSync(path.join(assets, 'trayTemplate.png'), render(16, trayShapes)); | |
| fs.writeFileSync(path.join(assets, 'trayTemplate@2x.png'), render(32, trayShapes)); | |
| // ββ app icon (1024, HIG grid: art occupies ~824/1024 rounded rect) ββββββββ | |
| const BG = [0x09 / 255, 0x09 / 255, 0x0b / 255, 1]; // #09090b | |
| const FG = [0xfa / 255, 0xfa / 255, 0xfa / 255, 1]; // #fafafa | |
| const FG40 = [0xfa / 255, 0xfa / 255, 0xfa / 255, 0.4]; | |
| const inset = 100 / 1024, rectR = 180 / 1024; | |
| const appShapes = [ | |
| roundedRect(inset, inset, 1 - inset, 1 - inset, rectR, BG), | |
| ring(0.5, 0.5, 232 / 1024, 45 / 1024, 45, 80, FG40), | |
| circle(0.5, 0.5, 122 / 1024, FG), | |
| ]; | |
| fs.writeFileSync(path.join(assets, 'appicon_1024.png'), render(1024, appShapes)); | |
| console.log('icons written to', assets); | |