"use strict"; /** * Render DSH Desktop icons from the official DeepSeek whale path * (assets-src/whale-path.txt, extracted from dsh web's /favicon.svg). * * Pure-Node rasterizer — cubic-bezier flattening + nonzero-winding scanline * fill + 4× supersampling AA. (qlmanage is NOT usable: it composites SVG * thumbnails onto an opaque white background, destroying transparency.) * * Outputs: * assets/trayTemplate.png (20×16) + trayTemplate@2x.png (40×32) — black * whale, alpha-only → macOS menu-bar template images * assets/icon.iconset/icon_*.png — DeepSeek-blue rounded square with a * white whale; `iconutil` then builds assets/icon.icns */ const fs = require("node:fs"); const path = require("node:path"); const zlib = require("node:zlib"); const clamp01 = (v) => Math.max(0, Math.min(1, v)); const lerp = (a, b, t) => a + (b - a) * t; // --- minimal PNG writer (8-bit RGBA) ---------------------------------------- const CRC_TABLE = (() => { const table = new Int32Array(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; table[n] = c; } return table; })(); function crc32(buf) { let c = -1; for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 255] ^ (c >>> 8); return (c ^ -1) >>> 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(w, h, rgba) { 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; // bit depth ihdr[9] = 6; // RGBA const stride = w * 4; const raw = Buffer.alloc((stride + 1) * h); for (let y = 0; y < h; y++) { raw[y * (stride + 1)] = 0; // filter: none rgba.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride); } return Buffer.concat([ sig, chunk("IHDR", ihdr), chunk("IDAT", zlib.deflateSync(raw, { level: 9 })), chunk("IEND", Buffer.alloc(0)), ]); } // --- whale path → flattened polygons ----------------------------------------- /** The path uses only absolute M/C/Z (verified: 4 M, 71 C, 4 Z). */ function parsePath(d) { const tokens = d.match(/[MCZ]|-?\d+(?:\.\d+)?/g); const subpaths = []; let current = null; let i = 0; while (i < tokens.length) { const t = tokens[i]; if (t === "M") { current = []; subpaths.push(current); current.push({ x: Number(tokens[i + 1]), y: Number(tokens[i + 2]) }); i += 3; } else if (t === "C") { const p0 = current[current.length - 1]; const p1 = { x: Number(tokens[i + 1]), y: Number(tokens[i + 2]) }; const p2 = { x: Number(tokens[i + 3]), y: Number(tokens[i + 4]) }; const p3 = { x: Number(tokens[i + 5]), y: Number(tokens[i + 6]) }; for (let s = 1; s <= 24; s++) { const u = s / 24; const v = 1 - u; current.push({ x: v * v * v * p0.x + 3 * v * v * u * p1.x + 3 * v * u * u * p2.x + u * u * u * p3.x, y: v * v * v * p0.y + 3 * v * v * u * p1.y + 3 * v * u * u * p2.y + u * u * u * p3.y, }); } i += 7; } else if (t === "Z") { i += 1; } else { throw new Error(`unsupported path command: ${t}`); } } return subpaths; } function bboxOf(subpaths) { let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (const poly of subpaths) { for (const p of poly) { if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x; if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y; } } return { minX, minY, maxX, maxY, w: maxX - minX, h: maxY - minY }; } /** Scale the whale so its height is targetH, centered on the canvas. */ function place(subpaths, canvasW, canvasH, targetH) { const b = bboxOf(subpaths); const s = targetH / b.h; const tx = (canvasW - b.w * s) / 2 - b.minX * s; const ty = (canvasH - targetH) / 2 - b.minY * s; return subpaths.map((poly) => poly.map((p) => ({ x: p.x * s + tx, y: p.y * s + ty }))); } const SS = 4; // supersampling factor /** Nonzero-winding scanline fill at SS×, box-downsampled to an 8-bit mask. * Input polygons are in final pixel coordinates; they are scaled by SS so the * scanline pass samples at SS× resolution. */ function rasterizeMask(subpaths, w, h) { const SW = w * SS; const SH = h * SS; const scaled = subpaths.map((poly) => poly.map((p) => ({ x: p.x * SS, y: p.y * SS }))); const sample = new Uint8Array(SW * SH); for (let y = 0; y < SH; y++) { const yc = y + 0.5; const xs = []; for (const poly of scaled) { for (let i = 0; i < poly.length; i++) { const a = poly[i]; const b = poly[(i + 1) % poly.length]; if ((a.y <= yc && b.y > yc) || (b.y <= yc && a.y > yc)) { const t = (yc - a.y) / (b.y - a.y); xs.push({ x: a.x + t * (b.x - a.x), dir: b.y > a.y ? 1 : -1 }); } } } xs.sort((p, q) => p.x - q.x); let winding = 0; let spanStart = 0; for (const e of xs) { if (winding === 0) spanStart = e.x; winding += e.dir; if (winding === 0) { const x0 = Math.max(0, Math.ceil(spanStart - 0.5)); const x1 = Math.min(SW - 1, Math.ceil(e.x - 0.5) - 1); const row = y * SW; for (let x = x0; x <= x1; x++) sample[row + x] = 1; } } } const mask = new Uint8Array(w * h); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { let sum = 0; for (let sy = 0; sy < SS; sy++) { const row = (y * SS + sy) * SW + x * SS; for (let sx = 0; sx < SS; sx++) sum += sample[row + sx]; } mask[y * w + x] = Math.round((sum / (SS * SS)) * 255); } } return mask; } /** Analytic rounded-rect coverage with ~1px edge AA, rect = full canvas. */ function roundRectCov(x, y, size, r) { const px = x + 0.5; const py = y + 0.5; const cx = Math.min(Math.max(px, r), size - r); const cy = Math.min(Math.max(py, r), size - r); return clamp01(0.5 - (Math.hypot(px - cx, py - cy) - r)); } const WHALE = parsePath(fs.readFileSync(path.join(__dirname, "..", "assets-src", "whale-path.txt"), "utf8").trim()); /** App icon: vertical-gradient DeepSeek-blue rounded square + white whale. */ function drawIcon(size) { const whale = rasterizeMask(place(WHALE, size, size, size * 0.62), size, size); const r = size * 0.2246; const buf = Buffer.alloc(size * size * 4); for (let y = 0; y < size; y++) { const t = y / (size - 1); const bgR = lerp(0x5e, 0x3d, t); const bgG = lerp(0x7b, 0x5b, t); const bgB = lerp(0xff, 0xf5, t); for (let x = 0; x < size; x++) { const bgA = roundRectCov(x, y, size, r); const wA = whale[y * size + x] / 255; // whale OVER background const outA = wA + bgA * (1 - wA); if (outA <= 0) continue; const i = (y * size + x) * 4; buf[i] = Math.round((255 * wA + bgR * bgA * (1 - wA)) / outA); buf[i + 1] = Math.round((255 * wA + bgG * bgA * (1 - wA)) / outA); buf[i + 2] = Math.round((255 * wA + bgB * bgA * (1 - wA)) / outA); buf[i + 3] = Math.round(outA * 255); } } return encodePng(size, size, buf); } /** Tray template: black whale, alpha-only (macOS renders template by alpha). */ function drawTray(w, h, whaleH) { const whale = rasterizeMask(place(WHALE, w, h, whaleH), w, h); const buf = Buffer.alloc(w * h * 4); for (let p = 0; p < w * h; p++) { buf[p * 4] = 0; buf[p * 4 + 1] = 0; buf[p * 4 + 2] = 0; buf[p * 4 + 3] = whale[p]; } return encodePng(w, h, buf); } // --- outputs ----------------------------------------------------------------- const assets = path.join(__dirname, "..", "assets"); fs.mkdirSync(assets, { recursive: true }); fs.writeFileSync(path.join(assets, "trayTemplate.png"), drawTray(20, 16, 14)); fs.writeFileSync(path.join(assets, "trayTemplate@2x.png"), drawTray(40, 32, 28)); const iconset = path.join(assets, "icon.iconset"); fs.mkdirSync(iconset, { recursive: true }); const RENDERED = {}; const render = (size) => (RENDERED[size] ??= drawIcon(size)); const SIZES = { "icon_16x16.png": 16, "icon_16x16@2x.png": 32, "icon_32x32.png": 32, "icon_32x32@2x.png": 64, "icon_128x128.png": 128, "icon_128x128@2x.png": 256, "icon_256x256.png": 256, "icon_256x256@2x.png": 512, "icon_512x512.png": 512, "icon_512x512@2x.png": 1024, }; for (const [name, size] of Object.entries(SIZES)) { fs.writeFileSync(path.join(iconset, name), render(size)); } console.log(`whale bbox: ${JSON.stringify(bboxOf(WHALE))}`); console.log(`icons rendered into ${assets} (+ icon.iconset; run iconutil for .icns)`);