WechatOnCloud/panel/web/scripts/gen-icons.mjs
Kris e35f1d64cf feat: 初始化项目,添加完整的微信云部署方案与面板实现
包含以下核心内容:
1.  完整的Docker容器化部署方案,支持多架构amd64/arm64
2.  自研Web管理面板,包含登录认证、实例管理、权限控制
3.  微信实例容器镜像,内置中文字体与输入法修复
4.  飞牛OS应用打包适配
5.  完整的文档与运维指南
2026-07-05 03:50:30 +08:00

121 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 生成 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);
}