feat:生产初版页面
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user