| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- /**
- * 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}`)
|