mirror of
https://github.com/Gloridust/WechatOnCloud.git
synced 2026-06-16 19:53:53 +08:00
121 lines
4.3 KiB
JavaScript
121 lines
4.3 KiB
JavaScript
// 生成 PWA / Apple 图标:绿底 macOS 终端风格(白色 `>_` 提示符 + 左上小圆点)。
|
|
// 纯 Node 实现,无需外部工具(rsvg/imagemagick),保证本地 / Docker / CI 构建都产出一致图标。
|
|
// 与 public/favicon.svg 同一视觉;改图标时两边一起改(坐标基于 100×100 视图)。
|
|
import { deflateSync } from 'node:zlib';
|
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
const TOP = [0x13, 0xd8, 0x73]; // 顶部亮绿 #13D873
|
|
const BOT = [0x05, 0xa8, 0x52]; // 底部深绿 #05A852
|
|
const WHITE = [255, 255, 255];
|
|
const OUT = join(dirname(fileURLToPath(import.meta.url)), '..', 'public');
|
|
|
|
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 chunk(type, data) {
|
|
const len = Buffer.alloc(4);
|
|
len.writeUInt32BE(data.length, 0);
|
|
const typeBuf = Buffer.from(type, 'ascii');
|
|
const crc = Buffer.alloc(4);
|
|
crc.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
|
|
return Buffer.concat([len, typeBuf, data, crc]);
|
|
}
|
|
|
|
// 点到线段距离(用于画带圆角端点的笔画)
|
|
function distToSeg(px, py, ax, ay, bx, by) {
|
|
const dx = bx - ax;
|
|
const dy = by - ay;
|
|
const len2 = dx * dx + dy * dy || 1;
|
|
let t = ((px - ax) * dx + (py - ay) * dy) / len2;
|
|
t = Math.max(0, Math.min(1, t));
|
|
const cx = ax + t * dx;
|
|
const cy = ay + t * dy;
|
|
return Math.hypot(px - cx, py - cy);
|
|
}
|
|
|
|
const clamp01 = (v) => Math.max(0, Math.min(1, v));
|
|
|
|
function makePng(size) {
|
|
const s = size / 100; // 视图 100 → 像素
|
|
const radius = 23 * s; // 圆角,与 favicon 一致
|
|
const stroke = (7.5 / 2) * s; // 笔画半宽
|
|
// 提示符坐标(基于 100 视图)
|
|
const chevron = [
|
|
[28, 24],
|
|
[42, 38],
|
|
[28, 52],
|
|
].map(([x, y]) => [x * s, y * s]);
|
|
const underline = [
|
|
[50, 54],
|
|
[66, 54],
|
|
].map(([x, y]) => [x * s, y * s]);
|
|
|
|
const inRounded = (x, y) => {
|
|
const r = radius;
|
|
const cx = x < r ? r : x > size - r ? size - r : x;
|
|
const cy = y < r ? r : y > size - r ? size - r : y;
|
|
if (cx === x && cy === y) return 1;
|
|
const d = Math.hypot(x - cx, y - cy);
|
|
return clamp01(r - d + 0.5); // 边缘 1px 抗锯齿
|
|
};
|
|
|
|
const rowLen = size * 4 + 1;
|
|
const raw = Buffer.alloc(rowLen * size);
|
|
for (let y = 0; y < size; y++) {
|
|
raw[y * rowLen] = 0; // filter type 0
|
|
const gy = y + 0.5;
|
|
const mix = gy / size; // 竖直渐变系数
|
|
const bg = [
|
|
Math.round(TOP[0] + (BOT[0] - TOP[0]) * mix),
|
|
Math.round(TOP[1] + (BOT[1] - TOP[1]) * mix),
|
|
Math.round(TOP[2] + (BOT[2] - TOP[2]) * mix),
|
|
];
|
|
for (let x = 0; x < size; x++) {
|
|
const gx = x + 0.5;
|
|
// 白色提示符覆盖度(取各形状最大值)
|
|
let cov = 0;
|
|
cov = Math.max(cov, clamp01(stroke - distToSeg(gx, gy, chevron[0][0], chevron[0][1], chevron[1][0], chevron[1][1]) + 0.5));
|
|
cov = Math.max(cov, clamp01(stroke - distToSeg(gx, gy, chevron[1][0], chevron[1][1], chevron[2][0], chevron[2][1]) + 0.5));
|
|
cov = Math.max(cov, clamp01(stroke - distToSeg(gx, gy, underline[0][0], underline[0][1], underline[1][0], underline[1][1]) + 0.5));
|
|
|
|
const o = y * rowLen + 1 + x * 4;
|
|
raw[o] = Math.round(bg[0] + (WHITE[0] - bg[0]) * cov);
|
|
raw[o + 1] = Math.round(bg[1] + (WHITE[1] - bg[1]) * cov);
|
|
raw[o + 2] = Math.round(bg[2] + (WHITE[2] - bg[2]) * cov);
|
|
raw[o + 3] = Math.round(255 * inRounded(gx, gy));
|
|
}
|
|
}
|
|
const ihdr = Buffer.alloc(13);
|
|
ihdr.writeUInt32BE(size, 0);
|
|
ihdr.writeUInt32BE(size, 4);
|
|
ihdr[8] = 8; // bit depth
|
|
ihdr[9] = 6; // color type RGBA
|
|
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
return Buffer.concat([sig, chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw)), chunk('IEND', Buffer.alloc(0))]);
|
|
}
|
|
|
|
mkdirSync(OUT, { recursive: true });
|
|
for (const [name, size] of [
|
|
['icon-192.png', 192],
|
|
['icon-512.png', 512],
|
|
['icon-180.png', 180],
|
|
]) {
|
|
writeFileSync(join(OUT, name), makePng(size));
|
|
console.log('generated', name);
|
|
}
|