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,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}`);
|