| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- "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)`);
|