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,762 @@
1
+ #!/usr/bin/env node
2
+ // Furniture sprite sheet: desk tile + monitor + 3 decor pieces.
3
+ // Output: pixel-office/public/furniture.png
4
+ //
5
+ // Sheet (32 wide):
6
+ // y=0..15 32x16 desk tile — oak (default), tileable, has 2 legs per tile
7
+ // y=16..39 32x24 monitor
8
+ // y=40..71 32x32 window-wide (generic sky, fallback)
9
+ // y=72..103 32x32 window-tall (unused, narrow 3-pane)
10
+ // y=104..135 32x32 door
11
+ // y=136..167 32x32 window-eagle (mountains + grass)
12
+ // y=168..199 32x32 window-market (city highrise night)
13
+ // y=200..231 32x32 window-sage (zen garden / bamboo dawn)
14
+ // y=232..263 32x32 window-senku (starfield + planet)
15
+ // y=264..295 32x32 window-shikamaru (dusk clouds + hills)
16
+ // y=296..327 32x32 window-l (rainy city night)
17
+ // y=328..399 32x72 chair (mesh-back office chair w/ pedestal + 5-leg star)
18
+ // y=400..415 32x16 floor plank tile (warm oak)
19
+ // y=416..431 32x16 floor plank tile (pale ash)
20
+ // y=432..447 32x16 floor plank tile (cherry)
21
+ // y=448..463 32x16 floor plank tile (graphite)
22
+ // y=464..479 32x16 desk tile — steel + glass (hairpin legs)
23
+ // y=480..495 32x16 desk tile — walnut exec (chunky dark legs)
24
+ // y=496..511 32x16 desk tile — white modern (chrome cylinder legs)
25
+ // Sheet total: 32 x 512
26
+
27
+ const fs = require('fs');
28
+ const zlib = require('zlib');
29
+ const path = require('path');
30
+
31
+ // ---------- PNG encoder ----------
32
+ const CRC_TABLE = (() => {
33
+ const t = new Uint32Array(256);
34
+ for (let n = 0; n < 256; n++) {
35
+ let c = n;
36
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
37
+ t[n] = c >>> 0;
38
+ }
39
+ return t;
40
+ })();
41
+ function crc32(buf) {
42
+ let c = 0xffffffff;
43
+ for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
44
+ return (c ^ 0xffffffff) >>> 0;
45
+ }
46
+ function chunk(type, data) {
47
+ const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0);
48
+ const typeBuf = Buffer.from(type, 'ascii');
49
+ const crcBuf = Buffer.alloc(4);
50
+ crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
51
+ return Buffer.concat([len, typeBuf, data, crcBuf]);
52
+ }
53
+ function encodePNG(width, height, rgba) {
54
+ const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
55
+ const ihdr = Buffer.alloc(13);
56
+ ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4);
57
+ ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
58
+ const stride = width * 4;
59
+ const raw = Buffer.alloc((stride + 1) * height);
60
+ for (let y = 0; y < height; y++) {
61
+ raw[y * (stride + 1)] = 0;
62
+ rgba.copy(raw, y * (stride + 1) + 1, y * stride, y * stride + stride);
63
+ }
64
+ const idat = zlib.deflateSync(raw, { level: 9 });
65
+ return Buffer.concat([sig, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', Buffer.alloc(0))]);
66
+ }
67
+
68
+ // ---------- Canvas ----------
69
+ function hex(s) {
70
+ const h = s.replace('#', '');
71
+ return [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16), 255];
72
+ }
73
+ class Canvas {
74
+ constructor(w, h) { this.w = w; this.h = h; this.buf = Buffer.alloc(w * h * 4); }
75
+ px(x, y, c) {
76
+ if (!c) return;
77
+ if (x < 0 || y < 0 || x >= this.w || y >= this.h) return;
78
+ const i = (y * this.w + x) * 4;
79
+ 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;
80
+ }
81
+ 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); }
82
+ hline(x, y, w, c) { for (let i = 0; i < w; i++) this.px(x+i, y, c); }
83
+ vline(x, y, h, c) { for (let i = 0; i < h; i++) this.px(x, y+i, c); }
84
+ outlineRect(x, y, w, h, c) {
85
+ this.hline(x, y, w, c); this.hline(x, y+h-1, w, c);
86
+ this.vline(x, y, h, c); this.vline(x+w-1, y, h, c);
87
+ }
88
+ }
89
+
90
+ const OUTLINE = hex('#1d1622');
91
+
92
+ // ---------- Painters ----------
93
+
94
+ // Desk tile (32x16, tileable) — desktop + apron only. Legs are drawn via CSS
95
+ // ::before/::after pseudo-elements so they appear at the actual desk endpoints
96
+ // rather than repeating through every tile.
97
+ //
98
+ // Layout within tile:
99
+ // y=0..7 desktop surface (top edge, highlight, surface, transition, apron)
100
+ // y=8..15 transparent (legs are CSS)
101
+ function paintDeskTilePalette(c, ox, oy, p) {
102
+ c.hline(ox, oy + 0, 32, OUTLINE); // top edge
103
+ c.hline(ox, oy + 1, 32, p.highlight); // light top
104
+ c.hline(ox, oy + 2, 32, p.surface); // surface
105
+ c.hline(ox, oy + 3, 32, OUTLINE); // top->apron transition
106
+ for (let yy = 4; yy <= 6; yy++) c.hline(ox, oy + yy, 32, p.apron); // apron face
107
+ c.hline(ox, oy + 7, 32, p.apronDark); // apron bottom shadow
108
+ if (p.grain) {
109
+ c.rect(ox + 5, oy + 2, 6, 1, p.grain);
110
+ c.rect(ox + 19, oy + 2, 4, 1, p.grain);
111
+ }
112
+ // y=8..15 intentionally left transparent — legs are CSS
113
+ }
114
+ const DESK_PALETTES = {
115
+ oak: {
116
+ highlight: hex('#c89867'), surface: hex('#a07449'),
117
+ apron: hex('#7a532e'), apronDark: hex('#5b3c1f'),
118
+ grain: hex('#4a2e15'),
119
+ leg: hex('#7a532e'), legH: hex('#a07449'), legD: hex('#4a2e15'),
120
+ legShape: 'block', shadow: hex('#241a0e'),
121
+ },
122
+ steel: {
123
+ highlight: hex('#cdd5e8'), surface: hex('#9aa3bf'),
124
+ apron: hex('#3a4156'), apronDark: hex('#1f2533'),
125
+ grain: null,
126
+ leg: hex('#1a1d2a'), legH: hex('#3a4156'), legD: hex('#0d0f17'),
127
+ legShape: 'hairpin', shadow: hex('#0c0e16'),
128
+ },
129
+ walnut: {
130
+ highlight: hex('#8a5638'), surface: hex('#5e3520'),
131
+ apron: hex('#3e2114'), apronDark: hex('#26140a'),
132
+ grain: hex('#1a0d06'),
133
+ leg: hex('#3e2114'), legH: hex('#5e3520'), legD: hex('#1a0d06'),
134
+ legShape: 'block', shadow: hex('#0e0703'),
135
+ },
136
+ white: {
137
+ highlight: hex('#ffffff'), surface: hex('#e8ecf4'),
138
+ apron: hex('#c4cad6'), apronDark: hex('#9aa1b3'),
139
+ grain: null,
140
+ leg: hex('#cdd2dc'), legH: hex('#ffffff'), legD: hex('#7d8395'),
141
+ legShape: 'cylinder', shadow: hex('#5a607080'),
142
+ },
143
+ };
144
+ function paintDeskTile(c, ox, oy) { paintDeskTilePalette(c, ox, oy, DESK_PALETTES.oak); }
145
+ function paintDeskTileSteel(c, ox, oy) { paintDeskTilePalette(c, ox, oy, DESK_PALETTES.steel); }
146
+ function paintDeskTileWalnut(c, ox, oy) { paintDeskTilePalette(c, ox, oy, DESK_PALETTES.walnut); }
147
+ function paintDeskTileWhite(c, ox, oy) { paintDeskTilePalette(c, ox, oy, DESK_PALETTES.white); }
148
+
149
+ // Monitor (32x24) — bezel + screen with scanlines + stand.
150
+ function paintMonitor(c, ox, oy) {
151
+ const bezel = hex('#1c2129');
152
+ const bezelD = hex('#0c0f15');
153
+ const screenBg = hex('#0d1c2a');
154
+ const glow = hex('#1d3d5a');
155
+ const scan = hex('#2a4f72');
156
+ const stand = hex('#2a2f3a');
157
+ const standD = hex('#15181f');
158
+ const power = hex('#3ee27e');
159
+
160
+ c.rect(ox, oy, 32, 16, bezel);
161
+ c.outlineRect(ox, oy, 32, 16, OUTLINE);
162
+ c.hline(ox + 1, oy + 14, 30, bezelD);
163
+ c.rect(ox + 3, oy + 2, 26, 11, screenBg);
164
+ c.outlineRect(ox + 3, oy + 2, 26, 11, OUTLINE);
165
+ c.rect(ox + 4, oy + 3, 24, 9, screenBg);
166
+ for (let yy = 4; yy <= 11; yy += 2) c.hline(ox + 4, oy + yy, 24, scan);
167
+ c.rect(ox + 5, oy + 4, 6, 3, glow);
168
+ c.rect(ox + 27, oy + 14, 1, 1, power);
169
+
170
+ c.rect(ox + 14, oy + 16, 4, 4, stand);
171
+ c.outlineRect(ox + 14, oy + 16, 4, 4, OUTLINE);
172
+ c.rect(ox + 6, oy + 20, 20, 4, stand);
173
+ c.outlineRect(ox + 6, oy + 20, 20, 4, OUTLINE);
174
+ c.hline(ox + 7, oy + 22, 18, standD);
175
+ }
176
+
177
+ // Decor: 4-pane window (32x24 painted inside a 32x32 cell at y=4..27).
178
+ function paintWindowWide(c, ox, oy) {
179
+ const frame = hex('#7c5b3c');
180
+ const frameH = hex('#a67d52');
181
+ const frameD = hex('#5a3f25');
182
+ const sky = hex('#9fdcff');
183
+ const skyD = hex('#5fa8d8');
184
+ const cloud = hex('#ecf6ff');
185
+ const sill = hex('#3a2818');
186
+
187
+ const x = ox, y = oy + 4;
188
+ // outer frame
189
+ c.rect(x, y, 32, 24, frame);
190
+ c.outlineRect(x, y, 32, 24, OUTLINE);
191
+ // sill (thicker bottom)
192
+ c.rect(x - 1, y + 22, 34, 4, frame);
193
+ c.outlineRect(x - 1, y + 22, 34, 4, OUTLINE);
194
+ c.hline(x, y + 25, 32, sill);
195
+ c.hline(x, y + 23, 32, frameH);
196
+ // glass area inset 3px
197
+ c.rect(x + 3, y + 3, 26, 17, sky);
198
+ c.outlineRect(x + 3, y + 3, 26, 17, OUTLINE);
199
+ // sky gradient (lower half darker)
200
+ c.rect(x + 4, y + 12, 24, 7, skyD);
201
+ // clouds
202
+ c.rect(x + 6, y + 6, 5, 2, cloud);
203
+ c.rect(x + 8, y + 5, 4, 2, cloud);
204
+ c.rect(x + 18, y + 8, 6, 2, cloud);
205
+ // cross divider (one vertical, one horizontal)
206
+ c.vline(x + 15, y + 3, 17, frameD);
207
+ c.vline(x + 16, y + 3, 17, frame);
208
+ c.hline(x + 3, y + 11, 26, frameD);
209
+ c.hline(x + 3, y + 12, 26, frame);
210
+ }
211
+
212
+ // Decor: tall narrow window (18x30 painted inside 32x32 cell, x-centered, y=1..30).
213
+ function paintWindowTall(c, ox, oy) {
214
+ const frame = hex('#5a4a6a');
215
+ const frameH = hex('#7e6a92');
216
+ const frameD = hex('#3a2c4f');
217
+ const sky = hex('#7d9fc5');
218
+ const skyD = hex('#4d6c95');
219
+ const sill = hex('#2a1f3a');
220
+
221
+ const w = 18, h = 30;
222
+ const x = ox + Math.floor((32 - w) / 2); // 7
223
+ const y = oy + 1;
224
+ // frame
225
+ c.rect(x, y, w, h, frame);
226
+ c.outlineRect(x, y, w, h, OUTLINE);
227
+ c.hline(x, y + 1, w, frameH);
228
+ // sill (slightly wider)
229
+ c.rect(x - 1, y + h - 3, w + 2, 3, frame);
230
+ c.outlineRect(x - 1, y + h - 3, w + 2, 3, OUTLINE);
231
+ c.hline(x, y + h - 1, w, sill);
232
+ // glass inset 3px
233
+ c.rect(x + 3, y + 3, w - 6, h - 9, sky);
234
+ c.outlineRect(x + 3, y + 3, w - 6, h - 9, OUTLINE);
235
+ // sky shading
236
+ c.rect(x + 4, y + 12, w - 8, h - 19, skyD);
237
+ // 2 vertical mullions creating 3 panes
238
+ c.vline(x + 7, y + 3, h - 9, frameD);
239
+ c.vline(x + 8, y + 3, h - 9, frame);
240
+ c.vline(x + 11, y + 3, h - 9, frameD);
241
+ c.vline(x + 12, y + 3, h - 9, frame);
242
+ // top arch detail
243
+ c.hline(x + 3, y + 5, w - 6, frameD);
244
+ c.hline(x + 3, y + 6, w - 6, frame);
245
+ }
246
+
247
+ // Decor: solid door (18x30 painted inside 32x32 cell, x-centered, y=2..31).
248
+ function paintDoor(c, ox, oy) {
249
+ const wood = hex('#6e4423');
250
+ const woodH = hex('#8d5a30');
251
+ const woodD = hex('#4a2c14');
252
+ const panelD = hex('#3a210e');
253
+ const knob = hex('#d4a64a');
254
+ const knobD = hex('#8a6a23');
255
+ const frame = hex('#3d2814');
256
+
257
+ const w = 18, h = 30;
258
+ const x = ox + Math.floor((32 - w) / 2); // 7
259
+ const y = oy + 2;
260
+ // door frame (darker rim)
261
+ c.rect(x - 1, y - 1, w + 2, h + 2, frame);
262
+ c.outlineRect(x - 1, y - 1, w + 2, h + 2, OUTLINE);
263
+ // door body
264
+ c.rect(x, y, w, h, wood);
265
+ c.outlineRect(x, y, w, h, OUTLINE);
266
+ // top highlight
267
+ c.hline(x + 1, y + 1, w - 2, woodH);
268
+ c.vline(x + 1, y + 1, h - 2, woodH);
269
+ // bottom + right shadow
270
+ c.hline(x + 1, y + h - 2, w - 2, woodD);
271
+ c.vline(x + w - 2, y + 1, h - 2, woodD);
272
+ // upper panel
273
+ c.outlineRect(x + 3, y + 3, w - 6, 10, panelD);
274
+ c.hline(x + 4, y + 4, w - 8, woodD);
275
+ // lower panel
276
+ c.outlineRect(x + 3, y + 16, w - 6, 11, panelD);
277
+ c.hline(x + 4, y + 17, w - 8, woodD);
278
+ // doorknob (right side, mid-height)
279
+ c.rect(x + w - 4, y + 14, 2, 2, knob);
280
+ c.outlineRect(x + w - 4, y + 14, 2, 2, OUTLINE);
281
+ c.px(x + w - 4, y + 14, knobD);
282
+ }
283
+
284
+ // Decor: Eagle's window — mountain peaks + grass (32x32, frame matches window-wide).
285
+ function paintWindowEagle(c, ox, oy) {
286
+ const frame = hex('#6a7a8a'); // steel-blue frame (commander's room)
287
+ const frameH = hex('#8ea0b0');
288
+ const frameD = hex('#4a5a6a');
289
+ const sill = hex('#2a3340');
290
+ const skyTop = hex('#5ba3d0'); // deep blue sky
291
+ const skyMid = hex('#78bce0');
292
+ const skyHrz = hex('#aad8f0'); // horizon haze
293
+ const mtnFar = hex('#5c6e82'); // far ridge (lighter, hazy)
294
+ const mtnMid = hex('#3d5165'); // mid mountain
295
+ const mtnNear= hex('#2a3f52'); // near foreground ridge
296
+ const snow = hex('#ddeeff');
297
+ const snowD = hex('#b8d0e8');
298
+ const grass = hex('#4a9042');
299
+ const grassD = hex('#2f6b2b');
300
+ const grassH = hex('#6ab85e');
301
+
302
+ const x = ox, y = oy + 4;
303
+
304
+ // Frame (same shape as window-wide)
305
+ c.rect(x, y, 32, 24, frame);
306
+ c.outlineRect(x, y, 32, 24, OUTLINE);
307
+ c.rect(x - 1, y + 22, 34, 4, frame);
308
+ c.outlineRect(x - 1, y + 22, 34, 4, OUTLINE);
309
+ c.hline(x, y + 25, 32, sill);
310
+ c.hline(x, y + 23, 32, frameH);
311
+
312
+ // Glass
313
+ c.rect(x + 3, y + 3, 26, 17, skyTop);
314
+ c.outlineRect(x + 3, y + 3, 26, 17, OUTLINE);
315
+ c.rect(x + 4, y + 4, 24, 4, skyTop);
316
+ c.rect(x + 4, y + 8, 24, 3, skyMid);
317
+ c.rect(x + 4, y + 11, 24, 2, skyHrz); // horizon haze
318
+
319
+ // Far ridge (subtle, low, hazy)
320
+ for (let i = 0; i < 26; i++) {
321
+ const h = 2 + Math.round(Math.sin(i * 0.45) * 1.2 + Math.sin(i * 0.9) * 0.8);
322
+ c.vline(x + 3 + i, y + 12 - h, h, mtnFar);
323
+ }
324
+
325
+ // Mid mountain — left peak
326
+ for (let r = 0; r < 6; r++) {
327
+ const w = Math.max(1, 11 - r * 2);
328
+ c.hline(x + 4 + r, y + 15 - r, w, mtnMid);
329
+ }
330
+ // Mid mountain — right peak
331
+ for (let r = 0; r < 5; r++) {
332
+ const w = Math.max(1, 9 - r * 2);
333
+ c.hline(x + 18 + r, y + 15 - r, w, mtnMid);
334
+ }
335
+
336
+ // Center main peak (tallest, dark)
337
+ for (let r = 0; r < 9; r++) {
338
+ const w = Math.max(1, 16 - r * 2);
339
+ c.hline(x + 3 + r + 4, y + 16 - r, w, mtnNear);
340
+ }
341
+ // Snow cap center
342
+ c.px(x + 15, y + 7, snow);
343
+ c.hline(x + 14, y + 8, 3, snow);
344
+ c.hline(x + 13, y + 9, 5, snow);
345
+ c.hline(x + 13, y + 10, 5, snowD);
346
+ // Snow cap left mid
347
+ c.px(x + 9, y + 10, snow);
348
+ c.hline(x + 8, y + 11, 3, snowD);
349
+
350
+ // Grass strip
351
+ c.rect(x + 4, y + 16, 24, 3, grass);
352
+ c.hline(x + 4, y + 16, 24, grassH);
353
+ c.hline(x + 4, y + 18, 24, grassD);
354
+
355
+ // Cross dividers
356
+ c.vline(x + 15, y + 3, 17, frameD);
357
+ c.vline(x + 16, y + 3, 17, frame);
358
+ c.hline(x + 3, y + 11, 26, frameD);
359
+ c.hline(x + 3, y + 12, 26, frame);
360
+ }
361
+
362
+ // Shared helpers for the per-agent window scenes.
363
+ function paintWindowShell(c, ox, oy, frame, frameH, sill) {
364
+ const x = ox, y = oy + 4;
365
+ c.rect(x, y, 32, 24, frame);
366
+ c.outlineRect(x, y, 32, 24, OUTLINE);
367
+ c.rect(x - 1, y + 22, 34, 4, frame);
368
+ c.outlineRect(x - 1, y + 22, 34, 4, OUTLINE);
369
+ c.hline(x, y + 25, 32, sill);
370
+ c.hline(x, y + 23, 32, frameH);
371
+ }
372
+ function paintWindowDividers(c, ox, oy, frame, frameD) {
373
+ const x = ox, y = oy + 4;
374
+ c.vline(x + 15, y + 3, 17, frameD);
375
+ c.vline(x + 16, y + 3, 17, frame);
376
+ c.hline(x + 3, y + 11, 26, frameD);
377
+ c.hline(x + 3, y + 12, 26, frame);
378
+ }
379
+
380
+ // MarketHunting — city skyline at night, lit windows in skyscrapers.
381
+ function paintWindowMarket(c, ox, oy) {
382
+ const frame = hex('#3a3f4a');
383
+ const frameH = hex('#5a5f6a');
384
+ const frameD = hex('#1a1f24');
385
+ const sill = hex('#0a0e1a');
386
+ const skyTop = hex('#1a2342');
387
+ const skyMid = hex('#2a3a5a');
388
+ const skyHrz = hex('#3a5078');
389
+ const bldg = hex('#0e131e');
390
+ const bldg2 = hex('#1a2030');
391
+ const win1 = hex('#ffd44a');
392
+ const win2 = hex('#62d0ff');
393
+ const moon = hex('#f0f4ff');
394
+
395
+ paintWindowShell(c, ox, oy, frame, frameH, sill);
396
+ const x = ox, y = oy + 4;
397
+ c.rect(x + 3, y + 3, 26, 17, skyTop);
398
+ c.outlineRect(x + 3, y + 3, 26, 17, OUTLINE);
399
+ c.rect(x + 4, y + 7, 24, 4, skyMid);
400
+ c.rect(x + 4, y + 11, 24, 2, skyHrz);
401
+ // Moon (top-right of glass)
402
+ c.rect(x + 23, y + 5, 2, 2, moon);
403
+ c.px(x + 24, y + 5, hex('#c8d0e0'));
404
+ // Skyline
405
+ const buildings = [
406
+ { x: 4, h: 8, col: bldg2 },
407
+ { x: 7, h: 12, col: bldg },
408
+ { x: 11, h: 6, col: bldg2 },
409
+ { x: 14, h: 14, col: bldg },
410
+ { x: 18, h: 9, col: bldg2 },
411
+ { x: 21, h: 11, col: bldg },
412
+ { x: 25, h: 7, col: bldg2 },
413
+ ];
414
+ buildings.forEach(b => c.rect(x + b.x, y + 19 - b.h, 3, b.h, b.col));
415
+ // Lit windows
416
+ [[8,10,win1],[8,13,win2],[9,11,win1],
417
+ [15,7,win1],[15,10,win2],[16,9,win1],[15,13,win1],[16,16,win2],
418
+ [22,11,win1],[23,13,win2],[22,15,win1]].forEach(([px, py, col]) => c.px(x + px, y + py, col));
419
+ paintWindowDividers(c, ox, oy, frame, frameD);
420
+ }
421
+
422
+ // Sage — zen garden at dawn, bamboo + cherry blossoms.
423
+ function paintWindowSage(c, ox, oy) {
424
+ const frame = hex('#7c5b3c');
425
+ const frameH = hex('#a67d52');
426
+ const frameD = hex('#5a3f25');
427
+ const sill = hex('#3a2818');
428
+ const skyTop = hex('#ffd8a8');
429
+ const skyMid = hex('#ffe8c8');
430
+ const ground = hex('#d4b888');
431
+ const groundD= hex('#a08858');
432
+ const bamboo = hex('#5a8a3a');
433
+ const bambooD= hex('#3d6a25');
434
+ const blossom= hex('#ffb0c8');
435
+ const blossomD= hex('#ff8aa8');
436
+ const trunkD = hex('#3a2818');
437
+
438
+ paintWindowShell(c, ox, oy, frame, frameH, sill);
439
+ const x = ox, y = oy + 4;
440
+ c.rect(x + 3, y + 3, 26, 17, skyTop);
441
+ c.outlineRect(x + 3, y + 3, 26, 17, OUTLINE);
442
+ c.rect(x + 4, y + 8, 24, 6, skyMid);
443
+ c.rect(x + 4, y + 14, 24, 5, ground);
444
+ c.hline(x + 4, y + 14, 24, groundD);
445
+ // Bamboo
446
+ [6, 11, 22].forEach(bx => {
447
+ c.vline(x + bx, y + 4, 14, bambooD);
448
+ c.vline(x + bx + 1, y + 4, 14, bamboo);
449
+ for (let s = 0; s < 4; s++) c.hline(x + bx, y + 6 + s * 3, 2, trunkD);
450
+ });
451
+ // Cherry blossoms
452
+ [{ bx: 16, by: 7 }, { bx: 19, by: 5 }, { bx: 14, by: 10 }, { bx: 25, by: 8 }].forEach(p => {
453
+ c.rect(x + p.bx, y + p.by, 2, 2, blossom);
454
+ c.px(x + p.bx, y + p.by, blossomD);
455
+ });
456
+ paintWindowDividers(c, ox, oy, frame, frameD);
457
+ }
458
+
459
+ // Senku — starfield with planet + rings.
460
+ function paintWindowSenku(c, ox, oy) {
461
+ const frame = hex('#4a5a78');
462
+ const frameH = hex('#6a7a98');
463
+ const frameD = hex('#2a3a58');
464
+ const sill = hex('#1a2030');
465
+ const skyTop = hex('#0a0e22');
466
+ const skyMid = hex('#15203a');
467
+ const star1 = hex('#ffffff');
468
+ const star2 = hex('#a0c8ff');
469
+ const planet = hex('#a878ff');
470
+ const planetD= hex('#683da8');
471
+ const ring = hex('#d0a878');
472
+ const ringD = hex('#a07848');
473
+
474
+ paintWindowShell(c, ox, oy, frame, frameH, sill);
475
+ const x = ox, y = oy + 4;
476
+ c.rect(x + 3, y + 3, 26, 17, skyTop);
477
+ c.outlineRect(x + 3, y + 3, 26, 17, OUTLINE);
478
+ c.rect(x + 4, y + 12, 24, 7, skyMid);
479
+ // Stars
480
+ [[6,5,star1],[9,4,star2],[11,7,star1],[13,5,star2],[25,6,star2],[27,5,star1],
481
+ [7,11,star2],[12,13,star1],[10,16,star2],[25,14,star1],[6,17,star1]].forEach(([sx,sy,col]) => c.px(x+sx, y+sy, col));
482
+ // Planet
483
+ c.rect(x + 17, y + 6, 6, 5, planet);
484
+ c.outlineRect(x + 17, y + 6, 6, 5, OUTLINE);
485
+ c.rect(x + 21, y + 7, 1, 3, planetD);
486
+ c.px(x + 22, y + 8, planetD);
487
+ // Ring (horizontal)
488
+ c.hline(x + 15, y + 8, 10, ring);
489
+ c.px(x + 14, y + 8, ringD);
490
+ c.px(x + 25, y + 8, ringD);
491
+ paintWindowDividers(c, ox, oy, frame, frameD);
492
+ }
493
+
494
+ // Shikamaru — dusk clouds and rolling hills (lazy "cloud-watching" view).
495
+ function paintWindowShikamaru(c, ox, oy) {
496
+ const frame = hex('#5a4a6a');
497
+ const frameH = hex('#7e6a92');
498
+ const frameD = hex('#3a2c4f');
499
+ const sill = hex('#2a1f3a');
500
+ const skyTop = hex('#7c5a8a');
501
+ const skyMid = hex('#e89878');
502
+ const skyBot = hex('#ffb878');
503
+ const cloud = hex('#ffd0c0');
504
+ const cloudD = hex('#c89878');
505
+ const hill = hex('#3a2848');
506
+ const hillH = hex('#5a3868');
507
+
508
+ paintWindowShell(c, ox, oy, frame, frameH, sill);
509
+ const x = ox, y = oy + 4;
510
+ c.rect(x + 3, y + 3, 26, 17, skyTop);
511
+ c.outlineRect(x + 3, y + 3, 26, 17, OUTLINE);
512
+ c.rect(x + 4, y + 7, 24, 4, skyMid);
513
+ c.rect(x + 4, y + 11, 24, 4, skyBot);
514
+ // Clouds
515
+ c.rect(x + 6, y + 5, 6, 2, cloud);
516
+ c.rect(x + 5, y + 6, 8, 1, cloudD);
517
+ c.rect(x + 18, y + 8, 7, 2, cloud);
518
+ c.rect(x + 17, y + 9, 9, 1, cloudD);
519
+ c.rect(x + 9, y + 10, 5, 1, cloud);
520
+ // Hills
521
+ for (let i = 0; i < 26; i++) {
522
+ const h = 2 + Math.round(Math.sin(i * 0.5) * 1.5 + Math.sin(i * 1.2) * 0.8);
523
+ const hh = Math.max(1, h);
524
+ c.vline(x + 3 + i, y + 19 - hh, hh, hill);
525
+ }
526
+ c.hline(x + 4, y + 16, 24, hillH);
527
+ paintWindowDividers(c, ox, oy, frame, frameD);
528
+ }
529
+
530
+ // L — rainy night cityscape (audit bunker view).
531
+ function paintWindowL(c, ox, oy) {
532
+ const frame = hex('#3a3a4a');
533
+ const frameH = hex('#5a5a6a');
534
+ const frameD = hex('#1a1a2a');
535
+ const sill = hex('#0a0a14');
536
+ const skyTop = hex('#101422');
537
+ const skyMid = hex('#1a1f30');
538
+ const rain = hex('#5a7088');
539
+ const rainH = hex('#8aa0b8');
540
+ const bldg = hex('#0a0e1a');
541
+ const win = hex('#5a8aa8');
542
+
543
+ paintWindowShell(c, ox, oy, frame, frameH, sill);
544
+ const x = ox, y = oy + 4;
545
+ c.rect(x + 3, y + 3, 26, 17, skyTop);
546
+ c.outlineRect(x + 3, y + 3, 26, 17, OUTLINE);
547
+ c.rect(x + 4, y + 11, 24, 8, skyMid);
548
+ // Rain droplets (small diagonal streaks)
549
+ const drops = [
550
+ [5, 4], [9, 3], [14, 5], [18, 4], [22, 3], [26, 5],
551
+ [6, 9], [11, 8], [16, 10], [20, 9], [24, 8],
552
+ [4, 14], [8, 13], [13, 15], [17, 14], [21, 13], [25, 15],
553
+ ];
554
+ drops.forEach(([dx, dy]) => {
555
+ c.px(x + dx, y + dy, rain);
556
+ c.px(x + dx + 1, y + dy + 1, rainH);
557
+ });
558
+ // Skyline silhouette
559
+ const bldgs = [
560
+ { x: 4, h: 4 }, { x: 7, h: 6 }, { x: 11, h: 3 },
561
+ { x: 14, h: 7 }, { x: 18, h: 5 }, { x: 21, h: 8 }, { x: 25, h: 4 },
562
+ ];
563
+ bldgs.forEach(b => c.rect(x + b.x, y + 19 - b.h, 3, b.h, bldg));
564
+ // One lit window on the tallest tower
565
+ c.px(x + 22, y + 14, win);
566
+ paintWindowDividers(c, ox, oy, frame, frameD);
567
+ }
568
+
569
+ // Office chair (32x72) — BACK VIEW. Camera is behind the seated character
570
+ // looking over the chair toward the monitor. We see the rear of the backrest
571
+ // panel (no seat cushion — it's hidden in front of the backrest from this
572
+ // angle), the armrest tops poking out at the sides, and the pedestal/base.
573
+ // Rows 0..7 are intentionally transparent so the character's head pokes
574
+ // above the backrest top.
575
+ function paintChair(c, ox, oy) {
576
+ const back = hex('#3a4258'); // backrest body
577
+ const backH = hex('#5d6680'); // top edge highlight
578
+ const backD = hex('#1c2030'); // seam + panel divisions
579
+ const arm = hex('#4a5266');
580
+ const armH = hex('#6a7388');
581
+ const armD = hex('#22273a');
582
+ const post = hex('#525a72'); // pedestal / base
583
+ const postH = hex('#7a8398');
584
+ const postD = hex('#262c3d');
585
+ const wheel = hex('#1f2433');
586
+ const wheelH = hex('#525a72');
587
+
588
+ // ---- Backrest top bow (slight outward curve at the very top edge) ----
589
+ // Lowered to expose the full top half of the seated character's head from
590
+ // behind. Bottom of backrest stays at y=42 so armrests/tilt block don't
591
+ // shift; the panel just gets shorter.
592
+ c.rect(ox + 5, oy + 22, 22, 2, back);
593
+ c.outlineRect(ox + 5, oy + 22, 22, 2, OUTLINE);
594
+ c.px(ox + 5, oy + 22, [0, 0, 0, 0]);
595
+ c.px(ox + 26, oy + 22, [0, 0, 0, 0]);
596
+
597
+ // ---- Main backrest panel (shorter: y=24..41, height 18) ----
598
+ c.rect(ox + 4, oy + 24, 24, 18, back);
599
+ c.outlineRect(ox + 4, oy + 24, 24, 18, OUTLINE);
600
+ c.hline(ox + 5, oy + 25, 22, backH); // top edge highlight
601
+ // Center vertical stitch seam
602
+ c.vline(ox + 15, oy + 26, 14, backD);
603
+ c.vline(ox + 16, oy + 26, 14, backD);
604
+ // Single lumbar panel division (mid-panel)
605
+ c.hline(ox + 5, oy + 33, 22, backD);
606
+ // Subtle side shading
607
+ c.vline(ox + 5, oy + 26, 14, backD);
608
+ c.vline(ox + 26, oy + 26, 14, backD);
609
+
610
+ // ---- Armrest tops (poke out at the sides, just below backrest) ----
611
+ c.rect(ox + 0, oy + 38, 4, 6, arm);
612
+ c.outlineRect(ox + 0, oy + 38, 4, 6, OUTLINE);
613
+ c.hline(ox + 1, oy + 39, 2, armH);
614
+ c.hline(ox + 1, oy + 42, 2, armD);
615
+ c.rect(ox + 28, oy + 38, 4, 6, arm);
616
+ c.outlineRect(ox + 28, oy + 38, 4, 6, OUTLINE);
617
+ c.hline(ox + 29, oy + 39, 2, armH);
618
+ c.hline(ox + 29, oy + 42, 2, armD);
619
+
620
+ // ---- Tilt mechanism block (visible under backrest, between arms) ----
621
+ c.rect(ox + 11, oy + 44, 10, 4, post);
622
+ c.outlineRect(ox + 11, oy + 44, 10, 4, OUTLINE);
623
+ c.hline(ox + 12, oy + 45, 8, postH);
624
+
625
+ // ---- Pedestal post ----
626
+ c.rect(ox + 13, oy + 48, 6, 6, post);
627
+ c.outlineRect(ox + 13, oy + 48, 6, 6, OUTLINE);
628
+ c.vline(ox + 14, oy + 49, 4, postH);
629
+ c.vline(ox + 17, oy + 49, 4, postD);
630
+
631
+ // ---- Star base hub ----
632
+ c.rect(ox + 12, oy + 54, 8, 4, post);
633
+ c.outlineRect(ox + 12, oy + 54, 8, 4, OUTLINE);
634
+ c.hline(ox + 13, oy + 55, 6, postH);
635
+
636
+ // ---- 5-leg star ----
637
+ c.rect(ox + 4, oy + 58, 9, 3, post);
638
+ c.outlineRect(ox + 4, oy + 58, 9, 3, OUTLINE);
639
+ c.hline(ox + 5, oy + 59, 7, postH);
640
+ c.rect(ox + 19, oy + 58, 9, 3, post);
641
+ c.outlineRect(ox + 19, oy + 58, 9, 3, OUTLINE);
642
+ c.hline(ox + 20, oy + 59, 7, postH);
643
+ c.rect(ox + 13, oy + 60, 6, 3, post);
644
+ c.outlineRect(ox + 13, oy + 60, 6, 3, OUTLINE);
645
+
646
+ // ---- Casters ----
647
+ c.rect(ox + 3, oy + 61, 4, 3, wheel);
648
+ c.outlineRect(ox + 3, oy + 61, 4, 3, OUTLINE);
649
+ c.px(ox + 4, oy + 62, wheelH);
650
+ c.rect(ox + 25, oy + 61, 4, 3, wheel);
651
+ c.outlineRect(ox + 25, oy + 61, 4, 3, OUTLINE);
652
+ c.px(ox + 27, oy + 62, wheelH);
653
+ c.rect(ox + 13, oy + 63, 6, 2, wheel);
654
+ c.outlineRect(ox + 13, oy + 63, 6, 2, OUTLINE);
655
+
656
+ // ---- Floor shadow (narrower to match smaller footprint) ----
657
+ c.hline(ox + 4, oy + 67, 24, [0, 0, 0, 90]);
658
+ }
659
+
660
+ // Floor plank tile (32x16, tiles horizontally — seamless wood).
661
+ function paintFloorTilePalette(c, ox, oy, p) {
662
+ // top highlight stripe
663
+ c.hline(ox, oy, 32, p.surfaceH);
664
+ // body
665
+ for (let yy = 1; yy <= 12; yy++) c.hline(ox, oy + yy, 32, p.surface);
666
+ // grain marks (asymmetric so adjacent tiles read as different planks)
667
+ c.hline(ox + 3, oy + 3, 7, p.grain);
668
+ c.hline(ox + 22, oy + 5, 5, p.grain);
669
+ c.hline(ox + 12, oy + 8, 8, p.grain);
670
+ c.hline(ox + 5, oy + 10, 4, p.grain);
671
+ // bottom shading
672
+ c.hline(ox, oy + 13, 32, p.surfaceM);
673
+ c.hline(ox, oy + 14, 32, p.surfaceD);
674
+ // plank seam between rows
675
+ c.hline(ox, oy + 15, 32, p.seam);
676
+ // vertical board ends — break the strip into two planks at x=15..16
677
+ c.vline(ox + 15, oy + 1, 14, p.seam);
678
+ c.vline(ox + 16, oy + 1, 14, p.surfaceM);
679
+ }
680
+ const FLOOR_PALETTES = {
681
+ warmOak: {
682
+ surface: hex('#7a532e'), surfaceH: hex('#a07449'), surfaceD: hex('#5b3c1f'),
683
+ surfaceM: hex('#6a4524'), grain: hex('#4a2e15'), seam: hex('#1a1208'),
684
+ },
685
+ paleAsh: {
686
+ surface: hex('#a8855a'), surfaceH: hex('#cba87a'), surfaceD: hex('#7a5e3a'),
687
+ surfaceM: hex('#8c6d45'), grain: hex('#6a4f30'), seam: hex('#3a2818'),
688
+ },
689
+ cherry: {
690
+ surface: hex('#7a3a2a'), surfaceH: hex('#a85540'), surfaceD: hex('#5a2618'),
691
+ surfaceM: hex('#6a2f20'), grain: hex('#421810'), seam: hex('#1a0a06'),
692
+ },
693
+ graphite: {
694
+ surface: hex('#3f3a3a'), surfaceH: hex('#5a5454'), surfaceD: hex('#2a2626'),
695
+ surfaceM: hex('#332f2f'), grain: hex('#1c1a1a'), seam: hex('#0d0c0c'),
696
+ },
697
+ };
698
+ function paintFloorTile(c, ox, oy) { paintFloorTilePalette(c, ox, oy, FLOOR_PALETTES.warmOak); }
699
+ function paintFloorTilePale(c, ox, oy) { paintFloorTilePalette(c, ox, oy, FLOOR_PALETTES.paleAsh); }
700
+ function paintFloorTileCherry(c, ox, oy) { paintFloorTilePalette(c, ox, oy, FLOOR_PALETTES.cherry); }
701
+ function paintFloorTileGraphite(c, ox, oy) { paintFloorTilePalette(c, ox, oy, FLOOR_PALETTES.graphite); }
702
+
703
+ // ---------- Build ----------
704
+
705
+ function build() {
706
+ const W = 32;
707
+ const H = 512;
708
+ const c = new Canvas(W, H);
709
+
710
+ paintDeskTile(c, 0, 0);
711
+ paintMonitor(c, 0, 16);
712
+ paintWindowWide(c, 0, 40);
713
+ paintWindowTall(c, 0, 72);
714
+ paintDoor(c, 0, 104);
715
+ paintWindowEagle(c, 0, 136);
716
+ paintWindowMarket(c, 0, 168);
717
+ paintWindowSage(c, 0, 200);
718
+ paintWindowSenku(c, 0, 232);
719
+ paintWindowShikamaru(c, 0, 264);
720
+ paintWindowL(c, 0, 296);
721
+ paintChair(c, 0, 328);
722
+ paintFloorTile(c, 0, 400);
723
+ paintFloorTilePale(c, 0, 416);
724
+ paintFloorTileCherry(c, 0, 432);
725
+ paintFloorTileGraphite(c, 0, 448);
726
+ paintDeskTileSteel(c, 0, 464);
727
+ paintDeskTileWalnut(c, 0, 480);
728
+ paintDeskTileWhite(c, 0, 496);
729
+
730
+ return { png: encodePNG(W, H, c.buf), W, H };
731
+ }
732
+
733
+ const { png, W, H } = build();
734
+ const outPath = path.join(__dirname, '..', 'public', 'furniture.png');
735
+ fs.writeFileSync(outPath, png);
736
+ fs.writeFileSync(
737
+ path.join(__dirname, '..', 'public', 'furniture.json'),
738
+ JSON.stringify({
739
+ desk: { x: 0, y: 0, w: 32, h: 16, tileable: true },
740
+ monitor: { x: 0, y: 16, w: 32, h: 24 },
741
+ windowWide: { x: 0, y: 40, w: 32, h: 32 },
742
+ windowTall: { x: 0, y: 72, w: 32, h: 32 },
743
+ door: { x: 0, y: 104, w: 32, h: 32 },
744
+ windowEagle: { x: 0, y: 136, w: 32, h: 32 },
745
+ windowMarket: { x: 0, y: 168, w: 32, h: 32 },
746
+ windowSage: { x: 0, y: 200, w: 32, h: 32 },
747
+ windowSenku: { x: 0, y: 232, w: 32, h: 32 },
748
+ windowShikamaru: { x: 0, y: 264, w: 32, h: 32 },
749
+ windowL: { x: 0, y: 296, w: 32, h: 32 },
750
+ chair: { x: 0, y: 328, w: 32, h: 72 },
751
+ floorTile: { x: 0, y: 400, w: 32, h: 16, tileable: true },
752
+ floorTilePale: { x: 0, y: 416, w: 32, h: 16, tileable: true },
753
+ floorTileCherry: { x: 0, y: 432, w: 32, h: 16, tileable: true },
754
+ floorTileGraphite:{ x: 0, y: 448, w: 32, h: 16, tileable: true },
755
+ deskSteel: { x: 0, y: 464, w: 32, h: 16, tileable: true },
756
+ deskWalnut: { x: 0, y: 480, w: 32, h: 16, tileable: true },
757
+ deskWhite: { x: 0, y: 496, w: 32, h: 16, tileable: true },
758
+ sheet: { w: W, h: H },
759
+ }, null, 2),
760
+ );
761
+ console.log(`Wrote ${png.length} bytes -> ${outPath}`);
762
+ console.log(`Sheet: ${W}x${H}`);