Files
hearts/gen-icons.js
T
2026-04-26 11:52:07 +00:00

127 lines
4.2 KiB
JavaScript
Raw Permalink 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 simple PNG icons using pure Node.js (no external packages needed).
'use strict';
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const outDir = path.join(__dirname, 'public', 'icons');
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
function writePNG(filePath, size) {
const channels = 4; // RGBA
const row = size * channels;
const raw = Buffer.alloc(size * row);
// Background: dark green #145228
const bgR = 0x14, bgG = 0x52, bgB = 0x28;
// Heart color: red #e53935
const hR = 0xe5, hG = 0x39, hB = 0x35;
// Draw pixel by pixel: background + centered heart shape
// Heart expressed as two overlapping circles + a downward triangle
const cx = size / 2;
const cy = size / 2 + size * 0.05;
const r = size * 0.22;
// Two circle centers
const lx = cx - r * 0.6, ly = cy - r * 0.35;
const rx = cx + r * 0.6, ry = cy - r * 0.35;
// Triangle tip
const tipY = cy + r * 1.1;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const px = (y * size + x) * channels;
const nx = x + 0.5, ny = y + 0.5;
// Rounded corners (radius ~20% of size)
const cr = size * 0.2;
const inCorner = (nx < cr && ny < cr && Math.hypot(nx - cr, ny - cr) > cr) ||
(nx > size - cr && ny < cr && Math.hypot(nx - (size - cr), ny - cr) > cr) ||
(nx < cr && ny > size - cr && Math.hypot(nx - cr, ny - (size - cr)) > cr) ||
(nx > size - cr && ny > size - cr && Math.hypot(nx - (size - cr), ny - (size - cr)) > cr);
if (inCorner) { raw[px+3] = 0; continue; }
// Heart: point in left circle OR right circle OR downward triangle region
const inL = Math.hypot(nx - lx, ny - ly) <= r;
const inR = Math.hypot(nx - rx, ny - ry) <= r;
// Triangle: below the circle union and above the tip
// Approximate with two lines from the outer circle edges to the tip
const inT = ny >= Math.min(ly, ry) && nx >= lx - r + (nx - (lx - r)) * 0 &&
(ny - (cy - r * 0.35)) / (tipY - (cy - r * 0.35)) <=
1 - Math.abs(nx - cx) / (r * 1.2);
const inHeart = inL || inR || inT;
if (inHeart) {
raw[px] = hR;
raw[px+1] = hG;
raw[px+2] = hB;
raw[px+3] = 255;
} else {
raw[px] = bgR;
raw[px+1] = bgG;
raw[px+2] = bgB;
raw[px+3] = 255;
}
}
}
// Encode to PNG
const chunks = [];
// PNG signature
chunks.push(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
function crc32(buf) {
let c = 0xffffffff;
const table = crc32.table || (crc32.table = (() => {
const t = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let v = i;
for (let k = 0; k < 8; k++) v = v & 1 ? 0xedb88320 ^ (v >>> 1) : v >>> 1;
t[i] = v;
}
return t;
})());
for (let i = 0; i < buf.length; i++) c = 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 tp = Buffer.from(type);
const crc = Buffer.alloc(4);
crc.writeUInt32BE(crc32(Buffer.concat([tp, data])));
return Buffer.concat([len, tp, data, crc]);
}
// IHDR
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(size, 0);
ihdr.writeUInt32BE(size, 4);
ihdr[8] = 8; // bit depth
ihdr[9] = 6; // RGBA
ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
chunks.push(chunk('IHDR', ihdr));
// IDAT: add filter byte (0) before each row
const filtered = Buffer.alloc(size * (row + 1));
for (let y = 0; y < size; y++) {
filtered[y * (row + 1)] = 0; // None filter
raw.copy(filtered, y * (row + 1) + 1, y * row, (y + 1) * row);
}
const compressed = zlib.deflateSync(filtered);
chunks.push(chunk('IDAT', compressed));
// IEND
chunks.push(chunk('IEND', Buffer.alloc(0)));
fs.writeFileSync(filePath, Buffer.concat(chunks));
console.log(`${path.basename(filePath)} written (${size}×${size})`);
}
writePNG(path.join(outDir, 'icon-192.png'), 192);
writePNG(path.join(outDir, 'icon-512.png'), 512);
console.log('Icons generated.');