// 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.');