Files
doodle-mini/tools/generate-tabbar-icons.js
T
2026-03-26 17:33:04 +08:00

233 lines
6.6 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.
/**
* 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);
});