/** * tabBar 图标生成脚本(零依赖:内置 zlib + 手写最小 PNG 编码器) * * 生成 81x81 RGBA 透明底线性图标到 src/static/tabbar/: * - workbench(四宫格)/ bills(单据)/ profile(人像) * - 普通态 #909399,选中态 #2979FF(与 pages.json tabBar 配色一致) * * 用法:npm run icons:gen */ import { deflateSync } from 'node:zlib' import { mkdirSync, writeFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const OUT_DIR = resolve(ROOT, 'src/static/tabbar') const SIZE = 81 // 微信推荐 tabBar 图标尺寸 const SAMPLES = 4 // 每像素 4x4 超采样抗锯齿 const COLOR_NORMAL = [0x90, 0x93, 0x99] const COLOR_ACTIVE = [0x29, 0x79, 0xff] // ---------- 最小 PNG 编码器 ---------- let CRC_TABLE = null function crc32(buf) { if (!CRC_TABLE) { CRC_TABLE = 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 } CRC_TABLE[n] = c >>> 0 } } 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) const typeBuf = Buffer.from(type, 'ascii') const crc = Buffer.alloc(4) crc.writeUInt32BE(crc32(Buffer.concat([typeBuf, data]))) return Buffer.concat([len, typeBuf, data, crc]) } function encodePng(width, height, rgba) { const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) const ihdr = Buffer.alloc(13) ihdr.writeUInt32BE(width, 0) ihdr.writeUInt32BE(height, 4) ihdr[8] = 8 // 位深 ihdr[9] = 6 // RGBA const stride = width * 4 const raw = Buffer.alloc(height * (stride + 1)) for (let y = 0; y < height; y++) { raw[y * (stride + 1)] = 0 // 每行 filter 0 rgba.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride) } const idat = deflateSync(raw, { level: 9 }) return Buffer.concat([sig, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', Buffer.alloc(0))]) } // ---------- 形状 SDF(返回像素点是否落在描边内) ---------- /** 圆角矩形描边 |sdf| <= w/2 */ function roundRect(px, py, cx, cy, hw, hh, r, w) { const qx = Math.abs(px - cx) - (hw - r) const qy = Math.abs(py - cy) - (hh - r) const d = Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) + Math.min(Math.max(qx, qy), 0) - r return Math.abs(d) <= w / 2 } /** 圆形描边 */ function circle(px, py, cx, cy, r, w) { return Math.abs(Math.hypot(px - cx, py - cy) - r) <= w / 2 } /** 水平胶囊线段(横线) */ function hCapsule(px, py, x1, x2, y, r) { const cx = Math.min(Math.max(px, x1), x2) return Math.hypot(px - cx, py - y) <= r } // ---------- 三个图标(inside(x, y) → boolean) ---------- /** 工作台:四宫格(4 个圆角方框) */ function iconWorkbench(x, y) { const cells = [ [24, 24], [57, 24], [24, 57], [57, 57] ] return cells.some(([cx, cy]) => roundRect(x, y, cx, cy, 14, 14, 5, 5)) } /** 单据:圆角矩形外框 + 三条横线 */ function iconBills(x, y) { if (roundRect(x, y, 40.5, 40.5, 22, 30, 7, 5)) return true return [29, 41, 53].some((ly) => hCapsule(x, y, 30, 51, ly, 2.5)) } /** 我的:圆头 + 肩弧 */ function iconProfile(x, y) { if (circle(x, y, 40.5, 26, 12, 5)) return true // 肩弧:大圆描边裁剪出顶部弧段 return y <= 62 && circle(x, y, 40.5, 78, 26, 5) } // ---------- 渲染与输出 ---------- function render(inside, [r, g, b]) { const rgba = Buffer.alloc(SIZE * SIZE * 4) const step = 1 / SAMPLES for (let y = 0; y < SIZE; y++) { for (let x = 0; x < SIZE; x++) { let hit = 0 for (let sy = 0; sy < SAMPLES; sy++) { for (let sx = 0; sx < SAMPLES; sx++) { if (inside(x + (sx + 0.5) * step, y + (sy + 0.5) * step)) hit++ } } const alpha = Math.round((hit / (SAMPLES * SAMPLES)) * 255) const idx = (y * SIZE + x) * 4 rgba[idx] = r rgba[idx + 1] = g rgba[idx + 2] = b rgba[idx + 3] = alpha } } return encodePng(SIZE, SIZE, rgba) } const icons = [ { name: 'workbench', draw: iconWorkbench }, { name: 'bills', draw: iconBills }, { name: 'profile', draw: iconProfile } ] mkdirSync(OUT_DIR, { recursive: true }) for (const { name, draw } of icons) { const normal = resolve(OUT_DIR, `${name}.png`) const active = resolve(OUT_DIR, `${name}-active.png`) writeFileSync(normal, render(draw, COLOR_NORMAL)) writeFileSync(active, render(draw, COLOR_ACTIVE)) console.log(`[icons] ${name}.png / ${name}-active.png 已生成`) } console.log(`[icons] 输出目录: ${OUT_DIR}`)