summaryrefslogtreecommitdiff
path: root/scripts/gen-icons.mjs
blob: ea2332662619201ec1d37b4a8e860a21caa7cb8f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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");