Recently Written · git

ideamine

An idea inbox for Claude Code: /idea saves ideas at zero tokens; Claude triages them and routes each to the cheapest model that can build it.

git clone https://github.com/equwal/ideamine

Log | Files | Refs


dashboard/icons.mjs (5511 bytes)

1 // The icons of the dashboard for a phone: a lamp on the dark of the board. `node dashboard/icons.mjs`
2 // writes them again. They are in the repository, so the server needs no drawing program.
3 //
4 // The drawing is plain arithmetic on a pixel buffer, at four times the size, and then averaged
5 // down. That gives smooth edges without a graphics library.
6 
7 import { deflateSync } from 'node:zlib';
8 import fs from 'node:fs';
9 import path from 'node:path';
10 import { fileURLToPath } from 'node:url';
11 
12 const DIR = path.dirname(fileURLToPath(import.meta.url));
13 const BG = [11, 11, 11]; // --bg of the dark theme
14 const LAMP = [57, 135, 229]; // --accent
15 const BASE = [195, 194, 183]; // --text-dim
16 const SUPER = 4;
17 
18 const mix = (under, over, alpha) => under.map((c, i) => Math.round(c * (1 - alpha) + over[i] * alpha));
19 
20 /** A rounded square that holds the whole icon, or the full square for a maskable icon. */
21 function inTile(x, y, size, radius) {
22   const near = (v, edge) => Math.min(v, edge - 1 - v);
23   const dx = near(x, size);
24   const dy = near(y, size);
25   if (dx >= radius || dy >= radius) return true;
26   return (radius - dx) ** 2 + (radius - dy) ** 2 <= radius ** 2;
27 }
28 
29 const inCircle = (x, y, cx, cy, r) => (x - cx) ** 2 + (y - cy) ** 2 <= r * r;
30 
31 /** Distance from the point to the line piece a→b, for the filament of the lamp. */
32 function distToSegment(x, y, ax, ay, bx, by) {
33   const vx = bx - ax;
34   const vy = by - ay;
35   const len = vx * vx + vy * vy;
36   const t = len ? Math.max(0, Math.min(1, ((x - ax) * vx + (y - ay) * vy) / len)) : 0;
37   return Math.hypot(x - (ax + t * vx), y - (ay + t * vy));
38 }
39 
40 /** One pixel of the icon: the tile, the glow, the lamp, its filament, and the screw base. */
41 function pixel(x, y, size, maskable) {
42   if (!inTile(x, y, size, maskable ? 0 : size * 0.1875)) return null; // outside the rounded square
43   const cx = size / 2;
44   const cy = size * 0.44;
45   const r = size * (maskable ? 0.17 : 0.21);
46   let color = BG;
47   const glow = Math.hypot(x - cx, y - cy) / (r * 2.1);
48   if (glow < 1) color = mix(color, LAMP, 0.45 * (1 - glow) ** 2);
49   const base = [
50     [cy + r * 1.02, r * 0.24],
51     [cy + r * 1.42, r * 0.24],
52     [cy + r * 1.82, r * 0.24],
53   ];
54   for (const [top, height] of base) {
55     if (y >= top && y <= top + height && Math.abs(x - cx) <= r * 0.45) color = BASE;
56   }
57   if (inCircle(x, y, cx, cy, r)) {
58     color = LAMP;
59     const wire = r * 0.09;
60     const points = [
61       [cx - r * 0.42, cy + r * 0.15],
62       [cx - r * 0.14, cy - r * 0.35],
63       [cx + r * 0.14, cy + r * 0.15],
64       [cx + r * 0.42, cy - r * 0.35],
65     ];
66     for (let i = 0; i < points.length - 1; i++) {
67       if (distToSegment(x, y, ...points[i], ...points[i + 1]) <= wire) color = BG;
68     }
69   }
70   return color;
71 }
72 
73 /** The pixels of one icon, averaged down from four times the size. */
74 function draw(size, maskable) {
75   const big = size * SUPER;
76   const out = Buffer.alloc(size * size * 4);
77   for (let y = 0; y < size; y++) {
78     for (let x = 0; x < size; x++) {
79       let r = 0;
80       let g = 0;
81       let b = 0;
82       let a = 0;
83       for (let sy = 0; sy < SUPER; sy++) {
84         for (let sx = 0; sx < SUPER; sx++) {
85           const color = pixel(x * SUPER + sx + 0.5, y * SUPER + sy + 0.5, big, maskable);
86           if (!color) continue;
87           r += color[0];
88           g += color[1];
89           b += color[2];
90           a += 255;
91         }
92       }
93       const n = SUPER * SUPER;
94       const i = (y * size + x) * 4;
95       const solid = a / 255 || 1;
96       out[i] = Math.round(r / solid);
97       out[i + 1] = Math.round(g / solid);
98       out[i + 2] = Math.round(b / solid);
99       out[i + 3] = Math.round(a / n);
100     }
101   }
102   return out;
103 }
104 
105 const CRC = Array.from({ length: 256 }, (_, n) => {
106   let c = n;
107   for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
108   return c >>> 0;
109 });
110 
111 function crc32(buf) {
112   let c = 0xffffffff;
113   for (const byte of buf) c = CRC[(c ^ byte) & 0xff] ^ (c >>> 8);
114   return (c ^ 0xffffffff) >>> 0;
115 }
116 
117 function chunk(type, data) {
118   const head = Buffer.alloc(8);
119   head.writeUInt32BE(data.length, 0);
120   head.write(type, 4, 'ascii');
121   const crc = Buffer.alloc(4);
122   crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);
123   return Buffer.concat([head, data, crc]);
124 }
125 
126 /** An RGBA pixel buffer as a PNG file. */
127 export function png(pixels, size) {
128   const head = Buffer.alloc(13);
129   head.writeUInt32BE(size, 0);
130   head.writeUInt32BE(size, 4);
131   head[8] = 8; // bits for each channel
132   head[9] = 6; // RGBA
133   const rows = Buffer.alloc(size * (size * 4 + 1));
134   for (let y = 0; y < size; y++) {
135     rows[y * (size * 4 + 1)] = 0; // filter: none
136     pixels.copy(rows, y * (size * 4 + 1) + 1, y * size * 4, (y + 1) * size * 4);
137   }
138   return Buffer.concat([
139     Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
140     chunk('IHDR', head),
141     chunk('IDAT', deflateSync(rows, { level: 9 })),
142     chunk('IEND', Buffer.alloc(0)),
143   ]);
144 }
145 
146 export const icon = (size, { maskable = false } = {}) => png(draw(size, maskable), size);
147 
148 export const ICONS = [
149   ['icon-192.png', 192, {}],
150   ['icon-512.png', 512, {}],
151   ['icon-maskable-512.png', 512, { maskable: true }],
152   ['apple-touch-icon.png', 180, {}],
153 ];
154 
155 if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
156   for (const [name, size, opts] of ICONS) {
157     const file = path.join(DIR, name);
158     fs.writeFileSync(file, icon(size, opts));
159     console.log(`${name}  ${size}x${size}  ${fs.statSync(file).size} bytes`);
160   }
161 }