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.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/bin/clawhouse.js +29 -0
- package/lib/usage.js +568 -0
- package/package.json +30 -0
- package/public/furniture.json +128 -0
- package/public/furniture.png +0 -0
- package/public/icon-180.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon-maskable-512.png +0 -0
- package/public/index.html +3335 -0
- package/public/manifest.webmanifest +16 -0
- package/public/monitors.json +20 -0
- package/public/monitors.png +0 -0
- package/public/props.json +20 -0
- package/public/props.png +0 -0
- package/public/sprites.json +27 -0
- package/public/sprites.png +0 -0
- package/scripts/build-furniture.js +762 -0
- package/scripts/build-monitors.js +496 -0
- package/scripts/build-props.js +675 -0
- package/scripts/build-sprites.js +1065 -0
- package/scripts/export-profile-pics.js +180 -0
- package/scripts/generate-icons.js +124 -0
- package/server.js +944 -0
|
@@ -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`);
|