/** * Generates 81×81 RGBA PNG tab bar icons (no npm deps — zlib + raw PNG). * Run: node tools/generate-tabbar-icons.js */ import { createWriteStream, mkdirSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { deflateSync } from "zlib"; const __dirname = dirname(fileURLToPath(import.meta.url)); const W = 81; const H = 81; const OUT_DIR = join(__dirname, "../miniprogram/assets/tabBar"); const PNG_SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); /** PNG CRC-32 (polynomial 0xedb88320) */ function crc32(buf) { let c = 0xffffffff; for (let i = 0; i < buf.length; i++) { c ^= buf[i]; for (let k = 0; k < 8; k++) { c = (c >>> 1) ^ (0xedb88320 & -(c & 1)); } } return (c ^ 0xffffffff) >>> 0; } function chunk(type, data) { const t = Buffer.from(type, "ascii"); const len = Buffer.allocUnsafe(4); len.writeUInt32BE(data.length, 0); const crc = Buffer.allocUnsafe(4); crc.writeUInt32BE(crc32(Buffer.concat([t, data])), 0); return Buffer.concat([len, t, data, crc]); } function encodePng(rgba) { const rowLen = 1 + W * 4; const raw = Buffer.allocUnsafe(H * rowLen); let o = 0; for (let y = 0; y < H; y++) { raw[o++] = 0; // filter: None const rowStart = y * W * 4; // top row first (y = 0) rgba.copy(raw, o, rowStart, rowStart + W * 4); o += W * 4; } const ihdr = Buffer.allocUnsafe(13); ihdr.writeUInt32BE(W, 0); ihdr.writeUInt32BE(H, 4); ihdr[8] = 8; // bit depth ihdr[9] = 6; // RGBA ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0; const idat = deflateSync(raw, { level: 9 }); return Buffer.concat([PNG_SIG, chunk("IHDR", ihdr), chunk("IDAT", idat), chunk("IEND", Buffer.alloc(0))]); } function parseHexRgb(hex) { const h = hex.replace("#", ""); return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16), }; } function setPixel(rgba, x, y, r, g, b, a) { if (x < 0 || x >= W || y < 0 || y >= H) return; const i = (y * W + x) * 4; rgba[i] = r; rgba[i + 1] = g; rgba[i + 2] = b; rgba[i + 3] = a; } function fillTriangle(rgba, x0, y0, x1, y1, x2, y2, r, g, b, a) { const ymin = Math.max(0, Math.min(y0, y1, y2)); const ymax = Math.min(H - 1, Math.max(y0, y1, y2)); for (let y = ymin; y <= ymax; y++) { const xs = []; const edge = (xa, ya, xb, yb) => { if (ya === yb) { if (y === ya) { const lo = Math.min(xa, xb); const hi = Math.max(xa, xb); xs.push(lo, hi); } return; } if (y < Math.min(ya, yb) || y > Math.max(ya, yb)) return; const t = (y - ya) / (yb - ya); xs.push(Math.round(xa + t * (xb - xa))); }; edge(x0, y0, x1, y1); edge(x1, y1, x2, y2); edge(x2, y2, x0, y0); if (xs.length < 2) continue; const xMin = Math.max(0, Math.min(...xs)); const xMax = Math.min(W - 1, Math.max(...xs)); for (let x = xMin; x <= xMax; x++) setPixel(rgba, x, y, r, g, b, a); } } function fillRect(rgba, x0, y0, x1, y1, r, g, b, a) { const xa = Math.max(0, Math.min(x0, x1)); const xb = Math.min(W - 1, Math.max(x0, x1)); const ya = Math.max(0, Math.min(y0, y1)); const yb = Math.min(H - 1, Math.max(y0, y1)); for (let y = ya; y <= yb; y++) { for (let x = xa; x <= xb; x++) setPixel(rgba, x, y, r, g, b, a); } } function fillCircle(rgba, cx, cy, rad, r, g, b, a) { const r2 = rad * rad; const x0 = Math.max(0, Math.floor(cx - rad - 1)); const x1 = Math.min(W - 1, Math.ceil(cx + rad + 1)); const y0 = Math.max(0, Math.floor(cy - rad - 1)); const y1 = Math.min(H - 1, Math.ceil(cy + rad + 1)); for (let y = y0; y <= y1; y++) { for (let x = x0; x <= x1; x++) { const dx = x - cx; const dy = y - cy; if (dx * dx + dy * dy <= r2) setPixel(rgba, x, y, r, g, b, a); } } } function fillEllipse(rgba, cx, cy, rx, ry, r, g, b, a, yMin = 0, yMax = H - 1) { const x0 = Math.max(0, Math.floor(cx - rx - 1)); const x1 = Math.min(W - 1, Math.ceil(cx + rx + 1)); const ya = Math.max(0, yMin); const yb = Math.min(H - 1, yMax); const rx2 = rx * rx; const ry2 = ry * ry; for (let y = ya; y <= yb; y++) { for (let x = x0; x <= x1; x++) { const dx = x - cx; const dy = y - cy; if (dx * dx * ry2 + dy * dy * rx2 <= rx2 * ry2) setPixel(rgba, x, y, r, g, b, a); } } } function drawHome(rgba, rgb) { const { r, g, b } = rgb; const a = 255; // Roof: triangle fillTriangle(rgba, 40, 15, 20, 34, 60, 34, r, g, b, a); // Body fillRect(rgba, 26, 34, 54, 56, r, g, b, a); } function drawAge(rgba, rgb) { const { r, g, b } = rgb; const a = 255; fillCircle(rgba, 40, 24, 10, r, g, b, a); fillRect(rgba, 32, 35, 48, 59, r, g, b, a); } function drawHeart(rgba, rgb) { const { r, g, b } = rgb; const a = 255; const cx = 40; const cy = 38; const s = 19; for (let py = 0; py < H; py++) { for (let px = 0; px < W; px++) { const x = (px - cx) / s; const y = -(py - cy) / s; const x2 = x * x; const y2 = y * y; const t = x2 + y2 - 1; if (t * t * t - x2 * y * y * y <= 0) setPixel(rgba, px, py, r, g, b, a); } } } function drawProfile(rgba, rgb) { const { r, g, b } = rgb; const a = 255; fillCircle(rgba, 40, 25, 10, r, g, b, a); // Wider shoulder ellipse (lower half reads as arc) fillEllipse(rgba, 40, 52, 27, 13, r, g, b, a, 38, H - 1); } function writePng(path, png) { mkdirSync(dirname(path), { recursive: true }); return new Promise((resolve, reject) => { const ws = createWriteStream(path); ws.on("error", reject); ws.on("finish", resolve); ws.end(png); }); } const COLOR_NORMAL = parseHexRgb("#999999"); const COLOR_ACTIVE = parseHexRgb("#333333"); const jobs = [ { name: "icon-home.png", draw: drawHome, color: COLOR_NORMAL }, { name: "icon-home-active.png", draw: drawHome, color: COLOR_ACTIVE }, { name: "icon-age.png", draw: drawAge, color: COLOR_NORMAL }, { name: "icon-age-active.png", draw: drawAge, color: COLOR_ACTIVE }, { name: "icon-fav.png", draw: drawHeart, color: COLOR_NORMAL }, { name: "icon-fav-active.png", draw: drawHeart, color: COLOR_ACTIVE }, { name: "icon-profile.png", draw: drawProfile, color: COLOR_NORMAL }, { name: "icon-profile-active.png", draw: drawProfile, color: COLOR_ACTIVE }, ]; async function main() { for (const { name, draw, color } of jobs) { const rgba = Buffer.alloc(W * H * 4, 0); draw(rgba, color); const png = encodePng(rgba); const out = join(OUT_DIR, name); await writePng(out, png); console.log("Wrote", out, `(${png.length} bytes)`); } } main().catch((e) => { console.error(e); process.exit(1); });