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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116 | // Generates PWA icons (two interlocking rings on dark — "connection") as PNGs
// with zero dependencies: per-pixel SDF rendering + hand-rolled PNG encoding
// via node:zlib.
import { deflateSync } from "node:zlib";
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const outDir = join(dirname(fileURLToPath(import.meta.url)), "..", "public", "icons");
mkdirSync(outDir, { recursive: true });
const BG = [19, 17, 24]; // matches --bg / manifest background_color
const CORAL = [255, 138, 92];
const VIOLET = [143, 123, 255];
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
const smooth = (d) => clamp(0.5 - d, 0, 1); // 1px antialias band around SDF zero
function roundedRect(px, py, cx, cy, hw, hh, r) {
const qx = Math.abs(px - cx) - hw + r;
const qy = Math.abs(py - cy) - hh + r;
return Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) + Math.min(Math.max(qx, qy), 0) - r;
}
// ring (annulus): |distance-to-circle| minus half stroke width
const ring = (px, py, cx, cy, r, w) => Math.abs(Math.hypot(px - cx, py - cy) - r) - w / 2;
function draw(S, { maskable = false, opaque = false } = {}) {
const img = Buffer.alloc(S * S * 4);
const pad = maskable ? 0.1 * S : 0; // maskable safe zone
const bgRadius = maskable || opaque ? 0 : 0.22 * S;
const u = S - 2 * pad; // content scale
const off = pad;
// two interlocking rings, slightly overlapping, on the horizontal axis
const R = 0.185 * u;
const W = 0.075 * u;
const cy = off + 0.5 * u;
const axL = off + 0.365 * u;
const axR = off + 0.635 * u;
for (let y = 0; y < S; y++) {
for (let x = 0; x < S; x++) {
const px = x + 0.5, py = y + 0.5;
let r = 0, g = 0, b = 0, a = 0;
// background card
const dBg = roundedRect(px, py, S / 2, S / 2, S / 2, S / 2, bgRadius);
const cBg = maskable || opaque ? 1 : smooth(dBg);
if (cBg > 0) { [r, g, b] = BG; a = cBg; }
const dL = ring(px, py, axL, cy, R, W);
const dR = ring(px, py, axR, cy, R, W);
// weave: left ring passes OVER on the upper half, UNDER on the lower —
// paint left, then right, then re-paint left where it should stay on top
const paint = (d, col) => {
const c = smooth(d);
if (c > 0) {
r = r + (col[0] - r) * c;
g = g + (col[1] - g) * c;
b = b + (col[2] - b) * c;
a = Math.max(a, c);
}
};
paint(dL, CORAL);
paint(dR, VIOLET);
if (py < cy) paint(dL, CORAL); // upper crossing: coral on top
const i = (y * S + x) * 4;
img[i] = r; img[i + 1] = g; img[i + 2] = b; img[i + 3] = Math.round(a * 255);
}
}
return img;
}
function png(S, rgba) {
const crcTable = [];
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;
crcTable[n] = c >>> 0;
}
const crc = (buf) => {
let c = 0xffffffff;
for (const byte of buf) c = crcTable[(c ^ byte) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
};
const chunk = (type, data) => {
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length);
const td = Buffer.concat([Buffer.from(type), data]);
const cc = Buffer.alloc(4);
cc.writeUInt32BE(crc(td));
return Buffer.concat([len, td, cc]);
};
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(S, 0);
ihdr.writeUInt32BE(S, 4);
ihdr[8] = 8; ihdr[9] = 6; // 8-bit RGBA
const raw = Buffer.alloc(S * (S * 4 + 1));
for (let y = 0; y < S; y++) {
raw[y * (S * 4 + 1)] = 0; // filter: none
rgba.copy(raw, y * (S * 4 + 1) + 1, y * S * 4, (y + 1) * S * 4);
}
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)),
]);
}
const targets = [
["icon-192.png", 192, {}],
["icon-512.png", 512, {}],
["icon-512-maskable.png", 512, { maskable: true }],
["apple-touch-icon.png", 180, { opaque: true }],
];
for (const [name, size, opts] of targets) {
writeFileSync(join(outDir, name), png(size, draw(size, opts)));
console.log("wrote", name);
}
|