home / client / scripts / make-icons.mjs
 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
/**
 * Generates the hub's PWA icons — the catalog's six-diamond color rule on the
 * night paper — as raw PNGs (tiny hand-rolled encoder, no dependencies).
 * Run: node scripts/make-icons.mjs   (writes public/icons/)
 */

import { deflateSync } from "node:zlib";
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const OUT = join(dirname(fileURLToPath(import.meta.url)), "..", "public", "icons");
mkdirSync(OUT, { recursive: true });

const PAPER = [0x17, 0x15, 0x20];
const DIAMONDS = [
  [0xe8, 0xa0, 0x9a], // rose
  [0xf0, 0xb8, 0x77], // peach
  [0x93, 0xc1, 0xa5], // sage
  [0xb3, 0xa4, 0xd4], // lilac
  [0x8f, 0xb8, 0xd8], // sky
  [0xe6, 0xc2, 0x6e], // gold
];

function crc32(buf) {
  let c = ~0;
  for (const b of buf) {
    c ^= b;
    for (let i = 0; i < 8; i++) c = (c >>> 1) ^ (0xedb88320 & -(c & 1));
  }
  return ~c >>> 0;
}

function chunk(type, data) {
  const t = Buffer.from(type, "ascii");
  const len = Buffer.alloc(4);
  len.writeUInt32BE(data.length);
  const crc = Buffer.alloc(4);
  crc.writeUInt32BE(crc32(Buffer.concat([t, data])));
  return Buffer.concat([len, t, data, crc]);
}

function png(size, pixels /* RGB Buffer */) {
  const ihdr = Buffer.alloc(13);
  ihdr.writeUInt32BE(size, 0);
  ihdr.writeUInt32BE(size, 4);
  ihdr[8] = 8; // bit depth
  ihdr[9] = 2; // color type RGB
  const raw = Buffer.alloc(size * (size * 3 + 1));
  for (let y = 0; y < size; y++) {
    pixels.copy(raw, y * (size * 3 + 1) + 1, y * size * 3, (y + 1) * size * 3);
  }
  return Buffer.concat([
    Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
    chunk("IHDR", ihdr),
    chunk("IDAT", deflateSync(raw, { level: 9 })),
    chunk("IEND", Buffer.alloc(0)),
  ]);
}

/** The motif: two rows of three diamonds (|x|+|y| ≤ r), centered. */
function draw(size, shrink = 0) {
  const px = Buffer.alloc(size * size * 3);
  for (let i = 0; i < size * size; i++) px.set(PAPER, i * 3);
  const u = size / 100;
  const r = (13 - shrink) * u;
  const centers = [];
  for (const cy of [40, 62]) for (const cx of [30, 50, 70]) centers.push([cx * u, cy * u]);
  centers.forEach(([cx, cy], i) => {
    const col = DIAMONDS[i];
    for (let y = Math.floor(cy - r); y <= cy + r; y++) {
      for (let x = Math.floor(cx - r); x <= cx + r; x++) {
        if (x < 0 || y < 0 || x >= size || y >= size) continue;
        if (Math.abs(x - cx) + Math.abs(y - cy) <= r) px.set(col, (y * size + x) * 3);
      }
    }
  });
  return png(size, px);
}

writeFileSync(join(OUT, "icon-192.png"), draw(192));
writeFileSync(join(OUT, "icon-512.png"), draw(512));
writeFileSync(join(OUT, "icon-512-maskable.png"), draw(512, 4)); // safe-zone shrink
writeFileSync(join(OUT, "apple-touch-icon.png"), draw(180));
console.log("icons written to", OUT);

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/home.git