import { deflateSync } from 'node:zlib'; import { promises as fs } from 'node:fs'; import nodePath from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = nodePath.dirname(fileURLToPath(import.meta.url)); const OUT = nodePath.join(__dirname, '..', 'build'); const SIZES = [16, 24, 32, 48, 64, 128, 256, 512]; // ---------- 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 (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } function pngChunk(type, data) { const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0); const typeBuf = Buffer.from(type, 'ascii'); const crcBuf = Buffer.alloc(4); crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0); return Buffer.concat([len, typeBuf, data, crcBuf]); } function encodePNG(w, h, rgba) { const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 6; // bit depth 8, color type 6 (RGBA) ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0; const stride = w * 4; const raw = Buffer.alloc(h * (stride + 1)); 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); } const idat = pngChunk('IDAT', deflateSync(raw, { level: 9 })); return Buffer.concat([sig, pngChunk('IHDR', ihdr), idat, pngChunk('IEND', Buffer.alloc(0))]); } // ---------- ICO encoder ---------- function encodeICO(sizes, pngs) { const header = Buffer.alloc(6); header.writeUInt16LE(0, 0); header.writeUInt16LE(1, 2); header.writeUInt16LE(sizes.length, 4); const entries = []; let offset = 6 + 16 * sizes.length; for (let i = 0; i < sizes.length; i++) { const s = sizes[i]; const e = Buffer.alloc(16); e[0] = s >= 256 ? 0 : s; e[1] = s >= 256 ? 0 : s; e[2] = 0; e[3] = 0; e.writeUInt16LE(1, 4); e.writeUInt16LE(32, 6); e.writeUInt32LE(pngs[i].length, 8); e.writeUInt32LE(offset, 12); entries.push(e); offset += pngs[i].length; } return Buffer.concat([header, ...entries, ...pngs]); } // ---------- Rasterizer ---------- function blend(buf, W, x, y, r, g, b, a) { if (a <= 0.001) return; x = Math.round(x); y = Math.round(y); if (x < 0 || y < 0 || x >= W || y >= W) return; const i = (y * W + x) * 4; const da = buf[i + 3] / 255; const oa = a + da * (1 - a); if (oa <= 0) return; buf[i] = Math.round((r * a + buf[i] * da * (1 - a)) / oa); buf[i + 1] = Math.round((g * a + buf[i + 1] * da * (1 - a)) / oa); buf[i + 2] = Math.round((b * a + buf[i + 2] * da * (1 - a)) / oa); buf[i + 3] = Math.round(oa * 255); } function inRR(px, py, x, y, w, h, r) { if (px < x || px > x + w || py < y || py > y + h) return false; const cx = Math.max(x + r, Math.min(px, x + w - r)); const cy = Math.max(y + r, Math.min(py, y + h - r)); const dx = px - cx, dy = py - cy; return dx * dx + dy * dy <= r * r; } function inCircle(px, py, cx, cy, r) { const dx = px - cx, dy = py - cy; return dx * dx + dy * dy <= r * r; } function lerp(a, b, t) { return a + (b - a) * t; } function lerpColor(c1, c2, t) { return [lerp(c1[0], c2[0], t), lerp(c1[1], c2[1], t), lerp(c1[2], c2[2], t)]; } // linear gradient stops: [[pos, [r,g,b], alpha], ...] function vGradientColor(stops, y, y0, y1) { const t = Math.min(1, Math.max(0, (y - y0) / (y1 - y0))); for (let i = 0; i < stops.length - 1; i++) { const a = stops[i], b = stops[i + 1]; if (t >= a[0] && t <= b[0]) { const lt = (t - a[0]) / (b[0] - a[0] || 1); return [lerp(a[1][0], b[1][0], lt), lerp(a[1][1], b[1][1], lt), lerp(a[1][2], b[1][2], lt), lerp(a[2], b[2], lt)]; } } const last = stops[stops.length - 1]; return [...last[1], last[2]]; } // Box blur on alpha channel (for soft shadows) function boxBlur(buf, W, radius) { const H = W; const tmp = new Float32Array(W * H); for (let i = 0; i < W * H; i++) tmp[i] = buf[i * 4 + 3] / 255; const src = tmp.slice(); const win = radius * 2 + 1; // horizontal for (let y = 0; y < H; y++) { let acc = 0; for (let x = -radius; x <= radius; x++) acc += src[Math.max(0, Math.min(W - 1, x)) + y * W]; for (let x = 0; x < W; x++) { tmp[x + y * W] = acc / win; const add = Math.min(W - 1, x + radius + 1), rem = Math.max(0, x - radius); acc += src[add + y * W] - src[rem + y * W]; } } const src2 = tmp.slice(); for (let x = 0; x < W; x++) { let acc = 0; for (let y = -radius; y <= radius; y++) acc += src2[x + Math.max(0, Math.min(H - 1, y)) * W]; for (let y = 0; y < H; y++) { tmp[x + y * W] = acc / win; const add = Math.min(H - 1, y + radius + 1), rem = Math.max(0, y - radius); acc += src2[x + add * W] - src2[x + rem * W]; } } for (let i = 0; i < W * H; i++) buf[i * 4 + 3] = Math.round(tmp[i] * 255); } // Affine: rotate around pivot function rotPoint(px, py, px_, py_, cosA, sinA) { const dx = px - px_, dy = py - py_; return [px_ + dx * cosA - dy * sinA, py_ + dx * sinA + dy * cosA]; } function render(size) { const hi = Math.max(256, size * 2); const k = hi / 512; const W = hi; const buf = new Uint8ClampedArray(W * W * 4); // 循环坐标即缓冲区坐标(缓冲区 W=hi,循环 0..hi);k 只用于设计坐标→缓冲区换算 const blendAt = (x, y, r, g, b, a) => blend(buf, W, x, y, r, g, b, a); const sA = (ang) => Math.sin(ang), cA = (ang) => Math.cos(ang); // ---- Tile base (vertical gradient, rounded 116) ---- const tileStops = [[0, [0x1a, 0x1e, 0x2b], 1], [0.55, [0x16, 0x1a, 0x26], 1], [1, [0x10, 0x13, 0x1c], 1]]; for (let y = 0; y < hi; y++) { const c = vGradientColor(tileStops, y / k, 0, 512); for (let x = 0; x < hi; x++) { if (inRR(x / k, y / k, 0, 0, 512, 512, 116)) blendAt(x, y, c[0], c[1], c[2], 1); } } // ---- Top sheen (rounded top, white 0.09 -> 0) ---- for (let y = 0; y < hi; y++) { const t = Math.min(1, Math.max(0, (y / k) / 260)); const a = 0.09 * (1 - t); for (let x = 0; x < hi; x++) { if (a > 0.002 && inRR(x / k, y / k, 0, 0, 512, 512, 116)) blendAt(x, y, 255, 255, 255, a); } } // ---- Stars ---- const stars = [[96, 120, 2.6, 0.5], [150, 196, 1.7, 0.5], [420, 360, 2, 0.5], [356, 92, 1.6, 0.5], [88, 340, 1.8, 0.5], [452, 200, 2.2, 0.5]]; for (const [sx, sy, sr, sa] of stars) { const r0 = Math.max(0, Math.floor((sy - sr - 1) * k)), r1 = Math.min(hi, Math.ceil((sy + sr + 1) * k)); const c0 = Math.max(0, Math.floor((sx - sr - 1) * k)), c1 = Math.min(hi, Math.ceil((sx + sr + 1) * k)); for (let y = r0; y < r1; y++) for (let x = c0; x < c1; x++) { if (inCircle(x / k, y / k, sx, sy, sr)) blendAt(x, y, 231, 236, 255, sa); } } // ---- Moon: glow + crescent ---- // glow for (let y = 0; y < hi; y++) for (let x = 0; x < hi; x++) { const dx = x / k - 404, dy = y / k - 116; const d = Math.sqrt(dx * dx + dy * dy); if (d <= 62) blendAt(x, y, 238, 242, 255, 0.22 * (1 - d / 62)); } // crescent (rotate -18 deg around (404,116)) const cosA = cA(-18 * Math.PI / 180), sinA = sA(-18 * Math.PI / 180); for (let y = 0; y < hi; y++) for (let x = 0; x < hi; x++) { const [lx, ly] = rotPoint(x / k, y / k, 404, 116, cosA, sinA); if (inCircle(lx, ly, 404, 116, 40)) { const t = Math.min(1, Math.max(0, (ly - 76) / 80)); const mc = lerpColor([255, 255, 255], [201, 212, 242], t); blendAt(x, y, mc[0], mc[1], mc[2], 1); } if (inCircle(lx, ly, 418, 106, 36)) blendAt(x, y, 0x1a, 0x1e, 0x2b, 1); } // ---- Card drop shadow (behind card, dark blurred, offset down-right) ---- const cardCos = cA(-6 * Math.PI / 180), cardSin = sA(-6 * Math.PI / 180); const cardLocal = (x, y) => rotPoint(x / k, y / k, 256, 268, cardCos, cardSin); const shadowBuf = new Uint8ClampedArray(W * W * 4); for (let y = 0; y < hi; y++) for (let x = 0; x < hi; x++) { const [lx, ly] = cardLocal(x, y); if (inRR(lx + 4, ly + 12, 96, 150, 320, 236, 44)) { const i = (y * W + x) * 4; shadowBuf[i] = 0; shadowBuf[i + 1] = 0; shadowBuf[i + 2] = 0; shadowBuf[i + 3] = 255; } } boxBlur(shadowBuf, W, Math.max(2, Math.round(18 * k / 4))); for (let i = 0; i < W * W; i++) { const a = shadowBuf[i * 4 + 3] / 255 * 0.5; if (a > 0.002) { const x = i % W, y = (i / W) | 0; blend(buf, W, x, y, 0, 0, 0, a); } } // ---- Card group (rotate -6 deg around 256,268) ---- // draw card (and inner bar + lines) via local coords for (let y = 0; y < hi; y++) for (let x = 0; x < hi; x++) { const [lx, ly] = cardLocal(x, y); // card body: gradient white 0.16->0.07 if (inRR(lx, ly, 96, 150, 320, 236, 44)) { const t = Math.min(1, Math.max(0, (ly - 150) / 236)); const a = 0.16 - 0.09 * t; blendAt(x, y, 255, 255, 255, a); } // inner top light bar if (inRR(lx, ly, 112, 164, 288, 30, 15)) blendAt(x, y, 255, 255, 255, 0.10); // card border (ring): draw by testing just outside inner, inside outer // note lines if (inRR(lx, ly, 140, 338, 150, 11, 5.5)) blendAt(x, y, 255, 255, 255, 0.55); if (inRR(lx, ly, 140, 360, 110, 11, 5.5)) blendAt(x, y, 255, 255, 255, 0.38); // H monogram const hStops = [[0, [0xf2, 0xf5, 0xff], 1], [1, [0xa7, 0xb8, 0xf0], 1]]; const hc = vGradientColor(hStops, ly, 210, 330); if (inRR(lx, ly, 204, 210, 46, 120, 23) || inRR(lx, ly, 286, 210, 46, 120, 23) || inRR(lx, ly, 227, 254, 82, 42, 21)) { blendAt(x, y, hc[0], hc[1], hc[2], 1); } } // card border: stroke ring (approx) - draw after body for (let y = 0; y < hi; y++) for (let x = 0; x < hi; x++) { const [lx, ly] = cardLocal(x, y); if (inRR(lx, ly, 96, 150, 320, 236, 44) && !inRR(lx, ly, 100, 154, 312, 228, 40)) { blendAt(x, y, 255, 255, 255, 0.4); } } // ---- Downsample hi -> size (box average, straight alpha) ---- if (size === hi) return Buffer.from(buf); const out = Buffer.alloc(size * size * 4); const ratio = hi / size; for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { let pr = 0, pg = 0, pb = 0, pa = 0; const x0 = Math.floor(x * ratio), y0 = Math.floor(y * ratio); const x1 = Math.min(hi, Math.ceil((x + 1) * ratio)), y1 = Math.min(hi, Math.ceil((y + 1) * ratio)); for (let yy = y0; yy < y1; yy++) for (let xx = x0; xx < x1; xx++) { const i = (yy * W + xx) * 4; const aa = buf[i + 3] / 255; if (aa > 0) { pr += buf[i] * aa; pg += buf[i + 1] * aa; pb += buf[i + 2] * aa; pa += aa; } } const o = (y * size + x) * 4; if (pa > 0) { out[o] = Math.round(pr / pa); out[o + 1] = Math.round(pg / pa); out[o + 2] = Math.round(pb / pa); out[o + 3] = Math.round(pa * 255); } } } return out; } // ---------- Main ---------- async function main() { const pngs = []; for (const s of SIZES) { const rgba = render(s); const png = encodePNG(s, s, rgba); await fs.writeFile(nodePath.join(OUT, 'icon', s + '.png'), png); pngs.push(png); console.log('rendered', s + 'px', png.length, 'bytes'); } const ico = encodeICO(SIZES, pngs); await fs.writeFile(nodePath.join(OUT, 'icon.ico'), ico); console.log('icon.ico', ico.length, 'bytes'); console.log('DONE'); } await main();