Files
2026-07-21 09:01:46 +08:00

172 lines
5.2 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
function crc32(buf) {
let c = ~0 >>> 0;
for (let i = 0; i < buf.length; i++) {
c ^= buf[i];
for (let k = 0; k < 8; k++) c = (c >>> 1) ^ (0xEDB88320 & -(c & 1));
}
return ~c >>> 0;
}
function chunk(type, data) {
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length, 0);
const t = Buffer.from(type, 'ascii');
const crc = Buffer.alloc(4);
crc.writeUInt32BE(crc32(Buffer.concat([t, data])), 0);
return Buffer.concat([len, t, data, crc]);
}
function encodePNG(rgba, w, h) {
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; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
const stride = w * 4;
const raw = Buffer.alloc((stride + 1) * h);
for (let y = 0; y < h; y++) {
raw[y * (stride + 1)] = 0;
rgba.copy(raw, y * (stride + 1) + 1, y * stride, y * stride + stride);
}
const idat = zlib.deflateSync(raw, { level: 9 });
return Buffer.concat([sig, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', Buffer.alloc(0))]);
}
function makeICO(images) {
const n = images.length;
const header = Buffer.alloc(6);
header.writeUInt16LE(0, 0);
header.writeUInt16LE(1, 2);
header.writeUInt16LE(n, 4);
let offset = 6 + 16 * n;
const entries = [];
const datas = [];
for (const im of images) {
const e = Buffer.alloc(16);
e[0] = im.w >= 256 ? 0 : im.w;
e[1] = im.h >= 256 ? 0 : im.h;
e[2] = 0; e[3] = 0;
e.writeUInt16LE(1, 4);
e.writeUInt16LE(32, 6);
e.writeUInt32LE(im.png.length, 8);
e.writeUInt32LE(offset, 12);
entries.push(e);
datas.push(im.png);
offset += im.png.length;
}
return Buffer.concat([header, ...entries, ...datas]);
}
function inRR(x, y, w, h, r) {
if (x < r && y < r) return Math.hypot(r - x, r - y) <= r;
if (x > w - 1 - r && y < r) return Math.hypot(w - 1 - r - x, r - y) <= r;
if (x < r && y > h - 1 - r) return Math.hypot(r - x, h - 1 - r - y) <= r;
if (x > w - 1 - r && y > h - 1 - r) return Math.hypot(w - 1 - r - x, h - 1 - r - y) <= r;
return x >= 0 && y >= 0 && x < w && y < h;
}
function inCapsule(x, y, x1, x2, cy, r) {
const clampX = Math.max(x1, Math.min(x2, x));
const dx = x - clampX;
const dy = y - cy;
return dx * dx + dy * dy <= r * r;
}
function render(W) {
const buf = Buffer.alloc(W * W * 4);
const rad = W * 0.235;
const A = [34, 211, 238];
const B = [99, 102, 241];
const bars = [
{ x1: W * 0.30, x2: W * 0.74, cy: W * 0.40, r: W * 0.030 },
{ x1: W * 0.22, x2: W * 0.80, cy: W * 0.515, r: W * 0.030 },
{ x1: W * 0.36, x2: W * 0.70, cy: W * 0.63, r: W * 0.030 },
];
for (let y = 0; y < W; y++) {
for (let x = 0; x < W; x++) {
const i = (y * W + x) * 4;
if (!inRR(x, y, W, W, rad)) {
buf[i + 3] = 0;
continue;
}
const t = (x + y) / (2 * (W - 1));
let r = A[0] + (B[0] - A[0]) * t;
let g = A[1] + (B[1] - A[1]) * t;
let b = A[2] + (B[2] - A[2]) * t;
let onBar = false;
for (const bar of bars) {
if (inCapsule(x, y, bar.x1, bar.x2, bar.cy, bar.r)) { onBar = true; break; }
}
if (onBar) { r = 255; g = 255; b = 255; }
buf[i] = r | 0;
buf[i + 1] = g | 0;
buf[i + 2] = b | 0;
buf[i + 3] = 255;
}
}
return buf;
}
function downsampleAvg(buf, W, ss) {
const size = W / ss;
const out = Buffer.alloc(size * size * 4);
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
let r = 0, g = 0, b = 0, a = 0;
for (let dy = 0; dy < ss; dy++) {
for (let dx = 0; dx < ss; dx++) {
const i = ((y * ss + dy) * W + (x * ss + dx)) * 4;
r += buf[i]; g += buf[i + 1]; b += buf[i + 2]; a += buf[i + 3];
}
}
const n = ss * ss;
const o = (y * size + x) * 4;
out[o] = Math.round(r / n);
out[o + 1] = Math.round(g / n);
out[o + 2] = Math.round(b / n);
out[o + 3] = Math.round(a / n);
}
}
return out;
}
function nearest(src, sw, sh, dw, dh) {
const out = Buffer.alloc(dw * dh * 4);
for (let y = 0; y < dh; y++) {
for (let x = 0; x < dw; x++) {
const sx = Math.min(sw - 1, Math.round((x * sw) / dw));
const sy = Math.min(sh - 1, Math.round((y * sh) / dh));
const si = (sy * sw + sx) * 4;
const di = (y * dw + x) * 4;
out[di] = src[si];
out[di + 1] = src[si + 1];
out[di + 2] = src[si + 2];
out[di + 3] = src[si + 3];
}
}
return out;
}
const SS = 4;
const BASE = 256;
const hi = render(BASE * SS);
const big = downsampleAvg(hi, BASE * SS, SS);
const png256 = encodePNG(big, BASE, BASE);
const png48 = encodePNG(nearest(big, BASE, BASE, 48, 48), 48, 48);
const png32 = encodePNG(nearest(big, BASE, BASE, 32, 32), 32, 32);
const png16 = encodePNG(nearest(big, BASE, BASE, 16, 16), 16, 16);
const ico = makeICO([
{ png: png256, w: 256, h: 256 },
{ png: png48, w: 48, h: 48 },
{ png: png32, w: 32, h: 32 },
{ png: png16, w: 16, h: 16 },
]);
const outDir = path.join(__dirname, '..', 'build');
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, 'icon.ico'), ico);
console.log('icon.ico written:', path.join(outDir, 'icon.ico'), ico.length, 'bytes');