Initial commit

This commit is contained in:
goyban
2026-04-26 11:52:07 +00:00
commit 460c9d57ed
14 changed files with 3927 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
// 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.');