/** * Generates assets/icon.png and assets/icon.ico * using only built-in Node.js (no external deps). * * ICO contains an embedded 256x256 PNG (Vista+ format). */ 'use strict' const zlib = require('zlib') const fs = require('fs') const path = require('path') // ─── CRC32 for PNG chunks ───────────────────────────────────────────────────── function crc32(buf) { let crc = 0xFFFFFFFF for (let i = 0; i < buf.length; i++) { crc ^= buf[i] for (let j = 0; j < 8; j++) crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1) } return (crc ^ 0xFFFFFFFF) >>> 0 } function pngChunk(type, data) { const t = Buffer.from(type, 'ascii') const len = Buffer.alloc(4); len.writeUInt32BE(data.length) const crcVal = Buffer.alloc(4); crcVal.writeUInt32BE(crc32(Buffer.concat([t, data]))) return Buffer.concat([len, t, data, crcVal]) } // ─── Minimal PNG encoder (RGBA, no interlace) ───────────────────────────────── function makePNG(size, drawFn) { const stride = 1 + size * 4 // filter byte + RGBA per pixel const raw = Buffer.alloc(size * stride) for (let y = 0; y < size; y++) { raw[y * stride] = 0 // filter: None for (let x = 0; x < size; x++) { const [r, g, b, a = 255] = drawFn(x, y, size) const off = y * stride + 1 + x * 4 raw[off] = r; raw[off+1] = g; raw[off+2] = b; raw[off+3] = a } } const ihdrData = Buffer.alloc(13) ihdrData.writeUInt32BE(size, 0) ihdrData.writeUInt32BE(size, 4) ihdrData[8] = 8 // bit depth ihdrData[9] = 6 // RGBA ihdrData[10] = 0; ihdrData[11] = 0; ihdrData[12] = 0 return Buffer.concat([ Buffer.from([0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a]), // PNG sig pngChunk('IHDR', ihdrData), pngChunk('IDAT', zlib.deflateSync(raw, { level: 9 })), pngChunk('IEND', Buffer.alloc(0)) ]) } // ─── Draw function ──────────────────────────────────────────────────────────── function drawIcon(x, y, S) { const cx = S / 2, cy = S / 2 const r = S * 0.48 // outer radius // Distance from center const dx = x - cx, dy = y - cy const dist = Math.sqrt(dx*dx + dy*dy) // Outside circle → transparent if (dist > r) return [0, 0, 0, 0] // Background gradient: dark blue const t = dist / r const bg = [ Math.round(26 + t * 8), Math.round(26 + t * 6), Math.round(46 + t * 10), 255 ] // ── Play triangle ────────────────────────────────────────────────────────── const tLeft = cx - S * 0.22 const tRight = cx + S * 0.28 const tTop = cy - S * 0.30 const tBot = cy + S * 0.30 // Point is inside triangle if: (x >= tLeft) AND left of right edge const edgeTop = tLeft + (tRight - tLeft) * (y - tTop) / (tBot - tTop) const edgeBot = tLeft + (tRight - tLeft) * (tBot - y) / (tBot - tTop) const inTri = x >= tLeft && x <= edgeTop && x <= edgeBot && y >= tTop && y <= tBot if (inTri) { // Accent blue gradient const fx = (x - tLeft) / (tRight - tLeft) return [ Math.round(79 + fx * 50), Math.round(142 + fx * 30), Math.round(247 - fx * 50), 255 ] } // ── Red "LIVE" dot (top-right) ───────────────────────────────────────────── const dotX = cx + S * 0.25, dotY = cy - S * 0.26, dotR = S * 0.12 const ddx = x - dotX, ddy = y - dotY const dotDist = Math.sqrt(ddx*ddx + ddy*ddy) if (dotDist <= dotR) { const inner = dotR * 0.45 if (dotDist <= inner) return [255, 255, 255, 255] // white center return [239, 68, 68, 255] // red ring } return bg } // ─── Wrap PNG in ICO ────────────────────────────────────────────────────────── function pngToIco(pngBuf, size) { const header = Buffer.alloc(6) header.writeUInt16LE(0, 0) // reserved header.writeUInt16LE(1, 2) // type: icon header.writeUInt16LE(1, 4) // image count const entry = Buffer.alloc(16) entry[0] = size >= 256 ? 0 : size // width (0 = 256) entry[1] = size >= 256 ? 0 : size // height (0 = 256) entry[2] = 0 // color count entry[3] = 0 // reserved entry.writeUInt16LE(1, 4) // planes entry.writeUInt16LE(32, 6) // bit count entry.writeUInt32LE(pngBuf.length, 8) // bytes in image entry.writeUInt32LE(6 + 16, 12) // offset = header + 1 entry return Buffer.concat([header, entry, pngBuf]) } // ─── Main ───────────────────────────────────────────────────────────────────── const assetsDir = path.join(__dirname, '..', 'assets') if (!fs.existsSync(assetsDir)) fs.mkdirSync(assetsDir, { recursive: true }) console.log('Generating icon 256×256 PNG…') const png256 = makePNG(256, drawIcon) fs.writeFileSync(path.join(assetsDir, 'icon.png'), png256) console.log(' ✓ assets/icon.png') console.log('Wrapping in ICO…') const ico = pngToIco(png256, 256) fs.writeFileSync(path.join(assetsDir, 'icon.ico'), ico) console.log(' ✓ assets/icon.ico') // 16px tray icon console.log('Generating tray icon 16×16…') const png16 = makePNG(16, drawIcon) fs.writeFileSync(path.join(assetsDir, 'tray-icon.png'), png16) console.log(' ✓ assets/tray-icon.png') console.log('Done.')