/** * 生成 Tab Bar 图标 PNG 文件 (81x81) * 使用 Node.js 内置模块,无需安装额外依赖 */ const fs = require('fs') const path = require('path') const zlib = require('zlib') const SIZE = 81 const OUT_DIR = path.join(__dirname, '../client/src/static/icons') // 颜色 const INACTIVE = [191, 179, 179] // #BFB3B3 const ACTIVE = [255, 140, 105] // #FF8C69 const TRANSPARENT = [0, 0, 0, 0] function createPNG(pixels) { // PNG signature const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]) // IHDR chunk const ihdr = Buffer.alloc(13) ihdr.writeUInt32BE(SIZE, 0) ihdr.writeUInt32BE(SIZE, 4) ihdr[8] = 8 // bit depth ihdr[9] = 6 // color type: RGBA ihdr[10] = 0 // compression ihdr[11] = 0 // filter ihdr[12] = 0 // interlace // Raw image data: filter byte (0) + RGBA pixels per row const rawData = Buffer.alloc(SIZE * (1 + SIZE * 4)) for (let y = 0; y < SIZE; y++) { const rowOffset = y * (1 + SIZE * 4) rawData[rowOffset] = 0 // no filter for (let x = 0; x < SIZE; x++) { const srcIdx = (y * SIZE + x) * 4 const dstIdx = rowOffset + 1 + x * 4 rawData[dstIdx] = pixels[srcIdx] rawData[dstIdx + 1] = pixels[srcIdx + 1] rawData[dstIdx + 2] = pixels[srcIdx + 2] rawData[dstIdx + 3] = pixels[srcIdx + 3] } } const compressed = zlib.deflateSync(rawData) function makeChunk(type, data) { const typeBuffer = Buffer.from(type, 'ascii') const length = Buffer.alloc(4) length.writeUInt32BE(data.length, 0) const combined = Buffer.concat([typeBuffer, data]) const crc = crc32(combined) return Buffer.concat([length, combined, crc]) } const ihdrChunk = makeChunk('IHDR', ihdr) const idatChunk = makeChunk('IDAT', compressed) const iendChunk = makeChunk('IEND', Buffer.alloc(0)) return Buffer.concat([signature, ihdrChunk, idatChunk, iendChunk]) } // CRC32 table const crcTable = 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) } crcTable[n] = c } function crc32(buf) { let crc = 0xFFFFFFFF for (let i = 0; i < buf.length; i++) { crc = crcTable[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8) } const result = Buffer.alloc(4) result.writeUInt32BE((crc ^ 0xFFFFFFFF) >>> 0, 0) return result } function makePixels(drawFn, color) { const pixels = new Uint8Array(SIZE * SIZE * 4) // Fill transparent for (let i = 0; i < pixels.length; i += 4) { pixels[i + 3] = 0 } drawFn(pixels, color) return pixels } function setPixel(pixels, x, y, r, g, b, a = 255) { if (x < 0 || x >= SIZE || y < 0 || y >= SIZE) return const idx = (y * SIZE + x) * 4 // Alpha blending const srcA = a / 255 const dstA = pixels[idx + 3] / 255 const outA = srcA + dstA * (1 - srcA) if (outA > 0) { pixels[idx] = Math.round((r * srcA + pixels[idx] * dstA * (1 - srcA)) / outA) pixels[idx + 1] = Math.round((g * srcA + pixels[idx + 1] * dstA * (1 - srcA)) / outA) pixels[idx + 2] = Math.round((b * srcA + pixels[idx + 2] * dstA * (1 - srcA)) / outA) pixels[idx + 3] = Math.round(outA * 255) } } function drawLine(pixels, x0, y0, x1, y1, r, g, b, thickness = 2) { const dx = Math.abs(x1 - x0) const dy = Math.abs(y1 - y0) const sx = x0 < x1 ? 1 : -1 const sy = y0 < y1 ? 1 : -1 let err = dx - dy while (true) { for (let tx = -thickness; tx <= thickness; tx++) { for (let ty = -thickness; ty <= thickness; ty++) { if (tx * tx + ty * ty <= thickness * thickness) { setPixel(pixels, x0 + tx, y0 + ty, r, g, b) } } } if (x0 === x1 && y0 === y1) break const e2 = 2 * err if (e2 > -dy) { err -= dy; x0 += sx } if (e2 < dx) { err += dx; y0 += sy } } } function drawCircle(pixels, cx, cy, radius, r, g, b, filled = true) { for (let y = -radius; y <= radius; y++) { for (let x = -radius; x <= radius; x++) { const dist = Math.sqrt(x * x + y * y) if (filled ? dist <= radius : (dist >= radius - 1.5 && dist <= radius + 0.5)) { setPixel(pixels, cx + x, cy + y, r, g, b) } } } } function drawRect(pixels, x, y, w, h, r, g, b) { for (let dy = 0; dy < h; dy++) { for (let dx = 0; dx < w; dx++) { setPixel(pixels, x + dx, y + dy, r, g, b) } } } // === Icon drawing functions === function drawHome(pixels, color) { const [r, g, b] = color const cx = 40, cy = 40 // House body (rectangle) drawRect(pixels, 24, 38, 34, 26, r, g, b) // Roof (triangle) for (let i = 0; i < 20; i++) { drawLine(pixels, cx, 18, cx - 20 + i * 2, 38, r, g, b, 1) } // Door drawRect(pixels, 35, 50, 12, 14, 255, 248, 240) } function drawChart(pixels, color) { const [r, g, b] = color // Three bars drawRect(pixels, 18, 45, 12, 20, r, g, b) drawRect(pixels, 35, 30, 12, 35, r, g, b) drawRect(pixels, 52, 20, 12, 45, r, g, b) // Base line drawRect(pixels, 14, 65, 54, 2, r, g, b) } function drawBill(pixels, color) { const [r, g, b] = color // Document shape drawRect(pixels, 22, 14, 38, 54, r, g, b) // Inner white area drawRect(pixels, 28, 22, 26, 38, 255, 248, 240) // Lines on document drawRect(pixels, 31, 30, 20, 2, r, g, b) drawRect(pixels, 31, 38, 16, 2, r, g, b) drawRect(pixels, 31, 46, 20, 2, r, g, b) drawRect(pixels, 31, 54, 12, 2, r, g, b) // Corner fold drawLine(pixels, 52, 14, 60, 22, r, g, b, 1) } function drawUser(pixels, color) { const [r, g, b] = color // Head (circle) drawCircle(pixels, 40, 28, 12, r, g, b) // Body (half circle / arc) for (let x = -24; x <= 24; x++) { const y = Math.round(Math.sqrt(Math.max(0, 24 * 24 - x * x))) for (let dy = 0; dy < 3; dy++) { setPixel(pixels, 40 + x, 58 - y + dy, r, g, b) } } // Fill body for (let y = 48; y <= 66; y++) { for (let x = 16; x <= 64; x++) { const dx = x - 40 const dy = y - 58 if (dy * dy + dx * dx <= 26 * 26) { setPixel(pixels, x, y, r, g, b) } } } } // Generate all icons const icons = [ { name: 'home', draw: drawHome }, { name: 'chart', draw: drawChart }, { name: 'bill', draw: drawBill }, { name: 'user', draw: drawUser }, ] if (!fs.existsSync(OUT_DIR)) { fs.mkdirSync(OUT_DIR, { recursive: true }) } for (const icon of icons) { // Inactive const inactivePixels = makePixels(icon.draw, INACTIVE) const inactivePNG = createPNG(inactivePixels) fs.writeFileSync(path.join(OUT_DIR, `${icon.name}.png`), inactivePNG) // Active const activePixels = makePixels(icon.draw, ACTIVE) const activePNG = createPNG(activePixels) fs.writeFileSync(path.join(OUT_DIR, `${icon.name}-active.png`), activePNG) console.log(`Generated: ${icon.name}.png, ${icon.name}-active.png`) } console.log('All icons generated successfully!')