summaryrefslogtreecommitdiff
path: root/scripts/gen-icons.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/gen-icons.mjs')
-rw-r--r--scripts/gen-icons.mjs98
1 files changed, 98 insertions, 0 deletions
diff --git a/scripts/gen-icons.mjs b/scripts/gen-icons.mjs
new file mode 100644
index 0000000..ea23326
--- /dev/null
+++ b/scripts/gen-icons.mjs
@@ -0,0 +1,98 @@
+import { deflateSync } from "zlib";
+import { writeFileSync, mkdirSync } from "fs";
+
+const CRC_TABLE = (() => {
+ const t = new Uint32Array(256);
+ for (let n = 0; n < 256; n++) {
+ let c = n;
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
+ t[n] = c;
+ }
+ return t;
+})();
+
+function crc32(buf) {
+ let crc = 0xffffffff;
+ for (let i = 0; i < buf.length; i++) crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
+ return (crc ^ 0xffffffff) >>> 0;
+}
+
+function chunk(type, data) {
+ const typeBytes = Buffer.from(type, "ascii");
+ const crcVal = crc32(Buffer.concat([typeBytes, data]));
+ const out = Buffer.allocUnsafe(4 + 4 + data.length + 4);
+ out.writeUInt32BE(data.length, 0);
+ typeBytes.copy(out, 4);
+ data.copy(out, 8);
+ out.writeUInt32BE(crcVal, 8 + data.length);
+ return out;
+}
+
+// Purple #6c63ff = rgb(108, 99, 255)
+// With a white checkmark drawn on it
+function createIcon(size) {
+ const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
+
+ const ihdr = Buffer.allocUnsafe(13);
+ ihdr.writeUInt32BE(size, 0);
+ ihdr.writeUInt32BE(size, 4);
+ ihdr[8] = 8; // bit depth
+ ihdr[9] = 2; // color type RGB
+ ihdr[10] = ihdr[11] = ihdr[12] = 0;
+
+ // Draw pixels: purple bg with white checkmark
+ const pixels = Buffer.alloc(size * size * 3);
+ const scale = size / 64;
+
+ // Fill purple
+ for (let i = 0; i < size * size; i++) {
+ pixels[i * 3] = 108;
+ pixels[i * 3 + 1] = 99;
+ pixels[i * 3 + 2] = 255;
+ }
+
+ // Draw white checkmark: ✓
+ // Checkmark path: (14,34)→(26,46)→(50,18) scaled to icon size
+ function setPixel(x, y) {
+ const xi = Math.round(x * scale);
+ const yi = Math.round(y * scale);
+ const thick = Math.max(2, Math.round(4 * scale));
+ for (let dy = -thick; dy <= thick; dy++) {
+ for (let dx = -thick; dx <= thick; dx++) {
+ const px = xi + dx;
+ const py = yi + dy;
+ if (px >= 0 && px < size && py >= 0 && py < size) {
+ pixels[(py * size + px) * 3] = 255;
+ pixels[(py * size + px) * 3 + 1] = 255;
+ pixels[(py * size + px) * 3 + 2] = 255;
+ }
+ }
+ }
+ }
+
+ // Left leg: (14,34) → (26,46)
+ const steps = 40;
+ for (let i = 0; i <= steps; i++) {
+ setPixel(14 + (26 - 14) * (i / steps), 34 + (46 - 34) * (i / steps));
+ }
+ // Right leg: (26,46) → (50,18)
+ for (let i = 0; i <= steps; i++) {
+ setPixel(26 + (50 - 26) * (i / steps), 46 + (18 - 46) * (i / steps));
+ }
+
+ // Build scanlines with filter=0 prefix
+ const raw = Buffer.allocUnsafe(size * (1 + size * 3));
+ for (let y = 0; y < size; y++) {
+ raw[y * (1 + size * 3)] = 0;
+ pixels.copy(raw, y * (1 + size * 3) + 1, y * size * 3, (y + 1) * size * 3);
+ }
+
+ const compressed = deflateSync(raw);
+
+ return Buffer.concat([sig, chunk("IHDR", ihdr), chunk("IDAT", compressed), chunk("IEND", Buffer.alloc(0))]);
+}
+
+mkdirSync("public/icons", { recursive: true });
+writeFileSync("public/icons/icon-192.png", createIcon(192));
+writeFileSync("public/icons/icon-512.png", createIcon(512));
+console.log("Icons generated: public/icons/icon-{192,512}.png");