clawhouse 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,496 @@
1
+ #!/usr/bin/env node
2
+ // Per-agent desk monitor sheet — shared bezel/stand (matches MarketHunting trading-chart
3
+ // style), agent-specific screen content. 1 column x 9 rows of 32x32.
4
+ // Output: pixel-office/public/monitors.png
5
+ //
6
+ // Row order (must match SPRITE_ROWS in index.html):
7
+ // 0 main | 1 markethunting | 2 sage | 3 senku | 4 shikamaru
8
+ // 5 tyrion | 6 harvey | 7 l | 8 d
9
+
10
+ const fs = require('fs');
11
+ const zlib = require('zlib');
12
+ const path = require('path');
13
+
14
+ // ---------- PNG encoder ----------
15
+ const CRC_TABLE = (() => {
16
+ const t = new Uint32Array(256);
17
+ for (let n = 0; n < 256; n++) {
18
+ let c = n;
19
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
20
+ t[n] = c >>> 0;
21
+ }
22
+ return t;
23
+ })();
24
+ function crc32(buf) {
25
+ let c = 0xffffffff;
26
+ for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
27
+ return (c ^ 0xffffffff) >>> 0;
28
+ }
29
+ function chunk(type, data) {
30
+ const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0);
31
+ const typeBuf = Buffer.from(type, 'ascii');
32
+ const crcBuf = Buffer.alloc(4);
33
+ crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
34
+ return Buffer.concat([len, typeBuf, data, crcBuf]);
35
+ }
36
+ function encodePNG(width, height, rgba) {
37
+ const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
38
+ const ihdr = Buffer.alloc(13);
39
+ ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4);
40
+ ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
41
+ const stride = width * 4;
42
+ const raw = Buffer.alloc((stride + 1) * height);
43
+ for (let y = 0; y < height; y++) {
44
+ raw[y * (stride + 1)] = 0;
45
+ rgba.copy(raw, y * (stride + 1) + 1, y * stride, y * stride + stride);
46
+ }
47
+ const idat = zlib.deflateSync(raw, { level: 9 });
48
+ return Buffer.concat([sig, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', Buffer.alloc(0))]);
49
+ }
50
+
51
+ // ---------- Canvas ----------
52
+ function hex(s) {
53
+ const h = s.replace('#', '');
54
+ return [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16), 255];
55
+ }
56
+ class Canvas {
57
+ constructor(w, h) { this.w = w; this.h = h; this.buf = Buffer.alloc(w * h * 4); }
58
+ px(x, y, c) {
59
+ if (!c) return;
60
+ if (x < 0 || y < 0 || x >= this.w || y >= this.h) return;
61
+ const i = (y * this.w + x) * 4;
62
+ this.buf[i] = c[0]; this.buf[i+1] = c[1]; this.buf[i+2] = c[2]; this.buf[i+3] = c[3] != null ? c[3] : 255;
63
+ }
64
+ rect(x, y, w, h, c) { for (let yy = 0; yy < h; yy++) for (let xx = 0; xx < w; xx++) this.px(x+xx, y+yy, c); }
65
+ hline(x, y, w, c) { for (let i = 0; i < w; i++) this.px(x+i, y, c); }
66
+ vline(x, y, h, c) { for (let i = 0; i < h; i++) this.px(x, y+i, c); }
67
+ outlineRect(x, y, w, h, c) {
68
+ this.hline(x, y, w, c); this.hline(x, y+h-1, w, c);
69
+ this.vline(x, y, h, c); this.vline(x+w-1, y, h, c);
70
+ }
71
+ }
72
+
73
+ const OUTLINE = hex('#0a0e1a');
74
+ const SIZE = 32;
75
+ // First 9 rows: per-agent monitors (rows 0..8 match SPRITE_ROWS).
76
+ // Extra row 9 = MarketHunting secondary monitor (P&L line + volume bars).
77
+ const AGENTS = ['main', 'markethunting', 'sage', 'senku', 'shikamaru', 'tyrion', 'harvey', 'l', 'd', 'ephraim', 'house', 'markethunting2'];
78
+
79
+ // ---------- Shared bezel + stand (matches paintTradingChart in build-props.js) ----------
80
+ // Screen interior: x+4..x+27 inclusive, y+6..y+21 inclusive => 24 wide x 16 tall.
81
+ function paintBezel(c, ox, oy) {
82
+ const bezel = hex('#1c2129');
83
+ const bezelD = hex('#0c0f15');
84
+ const stand = hex('#2a2f3a');
85
+ const power = hex('#3ee27e');
86
+ // bezel
87
+ c.rect(ox+2, oy+4, 28, 20, bezel);
88
+ c.outlineRect(ox+2, oy+4, 28, 20, OUTLINE);
89
+ c.hline(ox+3, oy+22, 26, bezelD);
90
+ // tiny power LED
91
+ c.px(ox+27, oy+22, power);
92
+ // stand neck
93
+ c.rect(ox+14, oy+24, 4, 4, stand);
94
+ c.outlineRect(ox+14, oy+24, 4, 4, OUTLINE);
95
+ // base
96
+ c.rect(ox+10, oy+27, 12, 2, stand);
97
+ c.outlineRect(ox+10, oy+27, 12, 2, OUTLINE);
98
+ }
99
+
100
+ // ---------- Per-agent screens (24x16 area starting at ox+4, oy+6) ----------
101
+
102
+ // Eagle — radar with target blip
103
+ function paintRadar(c, sx, sy) {
104
+ const bg = hex('#0a1c2a');
105
+ const grid = hex('#1d3a52');
106
+ const ring = hex('#3a6680');
107
+ const sweep= hex('#56b8e8');
108
+ const dot = hex('#ff5050');
109
+ c.rect(sx, sy, 24, 16, bg);
110
+ c.hline(sx, sy+8, 24, grid); // horizontal axis
111
+ c.vline(sx+12, sy, 16, grid); // vertical axis
112
+ c.outlineRect(sx+8, sy+5, 9, 7, ring); // inner ring
113
+ c.outlineRect(sx+3, sy+1, 19, 14, ring); // outer ring
114
+ // diagonal sweep
115
+ for (let i = 0; i < 9; i++) c.px(sx+12+i, sy+8-Math.floor(i*0.6), sweep);
116
+ // target blip (top-right quadrant)
117
+ c.rect(sx+18, sy+4, 2, 2, dot);
118
+ c.px(sx+19, sy+5, hex('#ffb0b0'));
119
+ }
120
+
121
+ // MarketHunting — green candlestick chart (mirrors paintTradingChart)
122
+ function paintCandles(c, sx, sy) {
123
+ const screen = hex('#0a1a14');
124
+ const grid = hex('#143b2a');
125
+ const green = hex('#3ee27e');
126
+ const greenD = hex('#1f8a4a');
127
+ const red = hex('#e85b5b');
128
+ const redD = hex('#8b3838');
129
+ c.rect(sx, sy, 24, 16, screen);
130
+ for (let x = sx+2; x < sx+24; x += 4) c.vline(x, sy+1, 14, grid);
131
+ for (let y = sy+3; y < sy+16; y += 3) c.hline(sx+1, y, 22, grid);
132
+ // candles (rising trend)
133
+ const candles = [
134
+ { x: 1, t: 11, b: 14, up: false },
135
+ { x: 4, t: 8, b: 12, up: true },
136
+ { x: 7, t: 6, b: 10, up: true },
137
+ { x: 10, t: 7, b: 9, up: false },
138
+ { x: 13, t: 4, b: 8, up: true },
139
+ { x: 16, t: 2, b: 6, up: true },
140
+ { x: 19, t: 3, b: 5, up: true },
141
+ ];
142
+ candles.forEach(k => {
143
+ const col = k.up ? green : red;
144
+ const cold = k.up ? greenD : redD;
145
+ c.vline(sx + k.x + 1, sy + k.t - 1, k.b - k.t + 3, cold);
146
+ c.rect(sx + k.x, sy + k.t, 3, k.b - k.t + 1, col);
147
+ });
148
+ }
149
+
150
+ // Sage — flowing prose lines (text scroll)
151
+ function paintTextScroll(c, sx, sy) {
152
+ const screen = hex('#1a2a25');
153
+ const text1 = hex('#cfe8d8');
154
+ const text2 = hex('#7fb59c');
155
+ const accent = hex('#f0c97a');
156
+ c.rect(sx, sy, 24, 16, screen);
157
+ // lines of "text" — alternating lengths, top header line in accent
158
+ c.hline(sx+2, sy+1, 16, accent); // title bar
159
+ c.hline(sx+2, sy+3, 20, text1);
160
+ c.hline(sx+2, sy+5, 16, text2);
161
+ c.hline(sx+2, sy+7, 19, text1);
162
+ c.hline(sx+2, sy+9, 14, text2);
163
+ c.hline(sx+2, sy+11, 21, text1);
164
+ c.hline(sx+2, sy+13, 12, text2);
165
+ }
166
+
167
+ // Senku — molecular structure (atoms + bonds)
168
+ function paintMolecule(c, sx, sy) {
169
+ const screen = hex('#0d1822');
170
+ const bond = hex('#5a7a9a');
171
+ const atomC = hex('#ffffff'); // carbon
172
+ const atomO = hex('#ff6464'); // oxygen
173
+ const atomN = hex('#64a0ff'); // nitrogen
174
+ const atomH = hex('#7fffb0'); // bonus
175
+ c.rect(sx, sy, 24, 16, screen);
176
+ // backbone bonds
177
+ // hexagon-ish layout
178
+ const atoms = [
179
+ { x: 5, y: 4, c: atomC },
180
+ { x: 11, y: 2, c: atomO },
181
+ { x: 17, y: 4, c: atomC },
182
+ { x: 19, y: 10, c: atomN },
183
+ { x: 13, y: 12, c: atomC },
184
+ { x: 7, y: 10, c: atomC },
185
+ { x: 3, y: 13, c: atomH },
186
+ { x: 21, y: 13, c: atomH },
187
+ ];
188
+ // bonds (lines between sequential atoms, ring)
189
+ const ringIdx = [0,1,2,3,4,5,0];
190
+ for (let i = 0; i < ringIdx.length - 1; i++) {
191
+ const a = atoms[ringIdx[i]], b = atoms[ringIdx[i+1]];
192
+ drawLine(c, sx + a.x, sy + a.y, sx + b.x, sy + b.y, bond);
193
+ }
194
+ // outer pendants
195
+ drawLine(c, sx + atoms[5].x, sy + atoms[5].y, sx + atoms[6].x, sy + atoms[6].y, bond);
196
+ drawLine(c, sx + atoms[3].x, sy + atoms[3].y, sx + atoms[7].x, sy + atoms[7].y, bond);
197
+ // atoms (circles → 3x3 squares)
198
+ atoms.forEach(a => {
199
+ c.rect(sx + a.x - 1, sy + a.y - 1, 3, 3, a.c);
200
+ c.outlineRect(sx + a.x - 1, sy + a.y - 1, 3, 3, OUTLINE);
201
+ });
202
+ }
203
+
204
+ // Shikamaru — shogi/go board overlay
205
+ function paintShogi(c, sx, sy) {
206
+ const screen = hex('#1a1530');
207
+ const grid = hex('#5a4f8c');
208
+ const piece1 = hex('#e8dcb5');
209
+ const piece2 = hex('#3a2a1a');
210
+ c.rect(sx, sy, 24, 16, screen);
211
+ // 7x5 grid lines
212
+ for (let i = 0; i <= 7; i++) c.vline(sx+1+i*3, sy+1, 14, grid);
213
+ for (let j = 0; j <= 5; j++) c.hline(sx+1, sy+1+j*3-(j>5?1:0), 22, grid);
214
+ // pieces (alternating)
215
+ const pieces = [[1,1,1],[3,2,2],[5,1,1],[2,3,2],[4,4,1],[6,3,2]];
216
+ pieces.forEach(([gx, gy, side]) => {
217
+ const px = sx + 1 + gx * 3 - 1;
218
+ const py = sy + 1 + gy * 3 - 1;
219
+ const col = side === 1 ? piece1 : piece2;
220
+ c.rect(px, py, 3, 2, col);
221
+ c.outlineRect(px, py, 3, 2, OUTLINE);
222
+ });
223
+ }
224
+
225
+ // Tyrion — ledger / treasury balances
226
+ function paintLedgerScreen(c, sx, sy) {
227
+ const screen = hex('#1a1208');
228
+ const gold = hex('#d4a64a');
229
+ const ink = hex('#e8dcb5');
230
+ const cell = hex('#3a2818');
231
+ c.rect(sx, sy, 24, 16, screen);
232
+ // header bar
233
+ c.hline(sx+1, sy+1, 22, gold);
234
+ // 4 rows of "ledger entries": label + bar
235
+ for (let r = 0; r < 4; r++) {
236
+ const ry = sy + 3 + r * 3;
237
+ c.hline(sx+2, ry, 5, ink); // label
238
+ c.hline(sx+9, ry, 12, cell); // bar bg
239
+ const fill = [11, 8, 13, 6][r];
240
+ c.hline(sx+9, ry, fill, gold); // bar fill (varies)
241
+ }
242
+ }
243
+
244
+ // Harvey — case docs / paper stack
245
+ function paintCaseDocs(c, sx, sy) {
246
+ const screen = hex('#1d1825');
247
+ const paper = hex('#ecdcca');
248
+ const paperS = hex('#b9a685');
249
+ const ink = hex('#3a2a1a');
250
+ const stamp = hex('#b8454a');
251
+ c.rect(sx, sy, 24, 16, screen);
252
+ // Paper 1 (back)
253
+ c.rect(sx+3, sy+2, 14, 11, paper);
254
+ c.outlineRect(sx+3, sy+2, 14, 11, OUTLINE);
255
+ c.hline(sx+4, sy+3, 12, ink);
256
+ c.hline(sx+4, sy+5, 10, paperS);
257
+ c.hline(sx+4, sy+7, 11, paperS);
258
+ c.hline(sx+4, sy+9, 9, paperS);
259
+ // Paper 2 (front)
260
+ c.rect(sx+8, sy+5, 13, 9, paper);
261
+ c.outlineRect(sx+8, sy+5, 13, 9, OUTLINE);
262
+ c.hline(sx+9, sy+6, 11, ink);
263
+ c.hline(sx+9, sy+8, 10, paperS);
264
+ c.hline(sx+9, sy+10, 8, paperS);
265
+ // red stamp
266
+ c.rect(sx+14, sy+11, 5, 2, stamp);
267
+ c.outlineRect(sx+14, sy+11, 5, 2, OUTLINE);
268
+ }
269
+
270
+ // L — spreadsheet / audit tables
271
+ function paintSpreadsheet(c, sx, sy) {
272
+ const screen = hex('#0e1422');
273
+ const grid = hex('#2a3a52');
274
+ const cell = hex('#1a2438');
275
+ const hi = hex('#4ae0a8');
276
+ const text = hex('#9ab8d8');
277
+ c.rect(sx, sy, 24, 16, screen);
278
+ // grid lines
279
+ for (let i = 0; i <= 4; i++) c.vline(sx+i*5, sy, 16, grid);
280
+ for (let j = 0; j <= 5; j++) c.hline(sx, sy+j*3, 24, grid);
281
+ // header row (top)
282
+ c.rect(sx+1, sy+1, 23, 2, cell);
283
+ c.hline(sx+1, sy+1, 23, text);
284
+ // sample data dashes
285
+ for (let r = 1; r < 5; r++) {
286
+ for (let col = 0; col < 4; col++) {
287
+ const cx = sx + col*5 + 1;
288
+ const cy = sy + r*3 + 1;
289
+ c.hline(cx, cy, 3, text);
290
+ }
291
+ }
292
+ // highlighted cell (audit hit)
293
+ c.rect(sx+11, sy+7, 4, 2, hi);
294
+ }
295
+
296
+ // MarketHunting secondary — P&L line chart + volume histogram
297
+ function paintOrderFlow(c, sx, sy) {
298
+ const screen = hex('#0a1a14');
299
+ const grid = hex('#143b2a');
300
+ const line = hex('#62d0ff');
301
+ const lineHi = hex('#bfeaff');
302
+ const green = hex('#3ee27e');
303
+ const red = hex('#e85b5b');
304
+ const divider = hex('#1f4a35');
305
+ c.rect(sx, sy, 24, 16, screen);
306
+ // grid
307
+ for (let x = sx+2; x < sx+24; x += 4) c.vline(x, sy+1, 14, grid);
308
+ c.hline(sx+1, sy+8, 22, divider);
309
+ // P&L line in upper half (y range 0..7)
310
+ const yvals = [5,4,5,3,4,2,3,4,2,1,3,2,4,3,2,1,3,2,1,3,2,1,2,1];
311
+ for (let x = 0; x < 24 - 1; x++) {
312
+ drawLine(c, sx+x, sy+yvals[x], sx+x+1, sy+yvals[x+1], line);
313
+ }
314
+ // shimmer dot at end
315
+ c.px(sx+23, sy+yvals[23], lineHi);
316
+ // volume bars in lower half (y range 9..15)
317
+ const vols = [3,5,4,2,6,5,3,7,4,2,5,6,3,4,5,2,6,4,3,5,7,5,4,6];
318
+ const ups = [false,true,true,false,true,true,false,true,true,false,true,true,
319
+ false,false,true,false,true,true,false,true,true,true,false,true];
320
+ for (let x = 0; x < 24; x++) {
321
+ const v = Math.min(vols[x], 6);
322
+ const col = ups[x] ? green : red;
323
+ c.vline(sx+x, sy+15-v+1, v, col);
324
+ }
325
+ }
326
+
327
+ // D — audio waveform / VU meters
328
+ function paintWaveform(c, sx, sy) {
329
+ const screen = hex('#1a0e22');
330
+ const grid = hex('#3a2052');
331
+ const wave = hex('#ff8ccd');
332
+ const waveD = hex('#a04080');
333
+ const peak = hex('#ffe07a');
334
+ c.rect(sx, sy, 24, 16, screen);
335
+ // center axis
336
+ c.hline(sx, sy+8, 24, grid);
337
+ // waveform — sine-ish
338
+ for (let x = 0; x < 24; x++) {
339
+ const ph = Math.sin((x / 24) * Math.PI * 4);
340
+ const amp = Math.round(ph * 5);
341
+ if (amp >= 0) {
342
+ c.vline(sx + x, sy + 8 - amp, amp + 1, wave);
343
+ } else {
344
+ c.vline(sx + x, sy + 8, -amp + 1, waveD);
345
+ }
346
+ }
347
+ // peak ticks at top
348
+ for (let i = 2; i < 24; i += 4) c.px(sx + i, sy + 1, peak);
349
+ }
350
+
351
+ // Ephraim — training plan readout: stacked weekly bars (volume) + RPE dots
352
+ function paintTrainingPlan(c, sx, sy) {
353
+ const screen = hex('#0f1614');
354
+ const grid = hex('#1f3328');
355
+ const bar = hex('#3ec27e');
356
+ const barHi = hex('#7df9d0');
357
+ const rpe = hex('#f0c060');
358
+ const rpeHi = hex('#fff0b0');
359
+ const text = hex('#9ad8b8');
360
+ c.rect(sx, sy, 24, 16, screen);
361
+ // top header strip (week label)
362
+ c.hline(sx, sy, 24, grid);
363
+ c.hline(sx+1, sy+1, 22, text);
364
+ // 7 vertical day-bars (volume blocks), heights 1..6
365
+ const heights = [3, 4, 0, 5, 2, 6, 1];
366
+ for (let d = 0; d < 7; d++) {
367
+ const bx = sx + 1 + d * 3;
368
+ const h = heights[d];
369
+ if (h === 0) continue;
370
+ c.vline(bx, sy + 14 - h, h, bar);
371
+ c.px(bx + 1, sy + 14 - h, barHi);
372
+ }
373
+ // RPE row across the top of the chart area (one dot per day, height encodes RPE)
374
+ const rpes = [6, 7, 0, 8, 5, 9, 4];
375
+ for (let d = 0; d < 7; d++) {
376
+ if (rpes[d] === 0) continue;
377
+ const bx = sx + 1 + d * 3;
378
+ const ry = sy + 12 - Math.max(0, rpes[d] - 4);
379
+ c.px(bx, ry, rpe);
380
+ if (rpes[d] >= 8) c.px(bx, ry - 1, rpeHi);
381
+ }
382
+ // baseline
383
+ c.hline(sx, sy + 15, 24, grid);
384
+ }
385
+
386
+ // House — patient monitor: ECG trace + vitals (HR / SpO2) readouts
387
+ function paintMedicalChart(c, sx, sy) {
388
+ const screen = hex('#0a1620');
389
+ const grid = hex('#163048');
390
+ const ecg = hex('#3ee27e'); // green ECG trace
391
+ const ecgHi = hex('#a9ffce');
392
+ const cyan = hex('#7fdcff'); // SpO2 wave
393
+ const cyanHi = hex('#d9f6ff');
394
+ const red = hex('#ff6464'); // HR / alert color
395
+ const amber = hex('#f0c97a');
396
+ const text = hex('#cfe8ff');
397
+ c.rect(sx, sy, 24, 16, screen);
398
+ // grid (dot pattern)
399
+ for (let x = sx + 1; x < sx + 24; x += 4) c.vline(x, sy + 1, 14, grid);
400
+ for (let y = sy + 3; y < sy + 16; y += 3) c.hline(sx + 1, y, 22, grid);
401
+
402
+ // Top vitals strip (HR, SpO2 numeric labels)
403
+ // "HR" pip + value pip + "SpO2" pip + value pip — schematic only at this scale
404
+ c.px(sx + 1, sy + 1, red); // HR indicator dot
405
+ c.hline(sx + 3, sy + 1, 4, text); // "HR" digits placeholder
406
+ c.px(sx + 13, sy + 1, cyan); // SpO2 indicator dot
407
+ c.hline(sx + 15, sy + 1, 6, text); // SpO2 digits placeholder
408
+
409
+ // ECG trace baseline (rows 5..8) — flat line interrupted by a QRS spike
410
+ const baseY = sy + 7;
411
+ c.hline(sx + 1, baseY, 22, ecg);
412
+ // P wave (small bump)
413
+ c.px(sx + 5, baseY - 1, ecg);
414
+ // QRS complex — sharp downstroke then big upstroke then return
415
+ c.px(sx + 8, baseY + 1, ecg);
416
+ c.px(sx + 9, baseY - 2, ecgHi);
417
+ c.px(sx + 9, baseY - 3, ecgHi);
418
+ c.px(sx + 9, baseY - 4, ecgHi);
419
+ c.px(sx + 10, baseY - 1, ecg);
420
+ c.px(sx + 11, baseY + 2, ecg);
421
+ c.px(sx + 12, baseY + 1, ecg);
422
+ // T wave (rounded bump)
423
+ c.px(sx + 15, baseY - 1, ecg);
424
+ c.px(sx + 16, baseY - 2, ecg);
425
+ c.px(sx + 17, baseY - 1, ecg);
426
+
427
+ // SpO2 plethysmograph trace lower (rows 11..13) — gentler sine-ish wave
428
+ const baseY2 = sy + 12;
429
+ c.hline(sx + 1, baseY2, 22, cyan);
430
+ c.px(sx + 4, baseY2 - 1, cyan);
431
+ c.px(sx + 5, baseY2 - 2, cyanHi);
432
+ c.px(sx + 6, baseY2 - 1, cyan);
433
+ c.px(sx + 12, baseY2 - 1, cyan);
434
+ c.px(sx + 13, baseY2 - 2, cyanHi);
435
+ c.px(sx + 14, baseY2 - 1, cyan);
436
+ c.px(sx + 20, baseY2 - 1, cyan);
437
+
438
+ // Bottom alarm strip — a subtle amber "VENT/RR" bar
439
+ c.hline(sx + 1, sy + 14, 6, amber);
440
+ }
441
+
442
+ // ---------- Helpers ----------
443
+
444
+ function drawLine(c, x0, y0, x1, y1, color) {
445
+ // Bresenham
446
+ let dx = Math.abs(x1 - x0), dy = -Math.abs(y1 - y0);
447
+ let sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
448
+ let err = dx + dy;
449
+ let x = x0, y = y0;
450
+ while (true) {
451
+ c.px(x, y, color);
452
+ if (x === x1 && y === y1) break;
453
+ const e2 = 2 * err;
454
+ if (e2 >= dy) { err += dy; x += sx; }
455
+ if (e2 <= dx) { err += dx; y += sy; }
456
+ }
457
+ }
458
+
459
+ const SCREENS = {
460
+ main: paintRadar,
461
+ markethunting: paintCandles,
462
+ sage: paintTextScroll,
463
+ senku: paintMolecule,
464
+ shikamaru: paintShogi,
465
+ tyrion: paintLedgerScreen,
466
+ harvey: paintCaseDocs,
467
+ l: paintSpreadsheet,
468
+ d: paintWaveform,
469
+ markethunting2: paintOrderFlow,
470
+ ephraim: paintTrainingPlan,
471
+ house: paintMedicalChart,
472
+ };
473
+
474
+ // ---------- Build ----------
475
+
476
+ function build() {
477
+ const W = SIZE;
478
+ const H = SIZE * AGENTS.length;
479
+ const c = new Canvas(W, H);
480
+ AGENTS.forEach((id, row) => {
481
+ const oy = row * SIZE;
482
+ paintBezel(c, 0, oy);
483
+ SCREENS[id](c, 4, oy + 6);
484
+ });
485
+ return { png: encodePNG(W, H, c.buf), W, H };
486
+ }
487
+
488
+ const { png, W, H } = build();
489
+ const outPath = path.join(__dirname, '..', 'public', 'monitors.png');
490
+ fs.writeFileSync(outPath, png);
491
+ fs.writeFileSync(
492
+ path.join(__dirname, '..', 'public', 'monitors.json'),
493
+ JSON.stringify({ cellWidth: SIZE, cellHeight: SIZE, rows: AGENTS, sheetWidth: W, sheetHeight: H }, null, 2),
494
+ );
495
+ console.log(`Wrote ${png.length} bytes -> ${outPath}`);
496
+ console.log(`Sheet: ${W}x${H}, ${AGENTS.length} monitors`);