@blkc-code/render-canvas 0.0.0-stage → 0.3.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/dist/draw.js ADDED
@@ -0,0 +1,760 @@
1
+ /**
2
+ * Draws the office. Every piece of art here is original to this project, made in code at
3
+ * 16 pixels a tile. The building is drawn once to a layer (drawBuilding); people, screens
4
+ * and anything else that moves are drawn on top each frame (drawWorld).
5
+ */
6
+ import { FURNITURE, isSeated } from "./world.js";
7
+ export const TILE = 16;
8
+ const LABEL_FONT = "bold 7px ui-monospace, Menlo, Consolas, monospace";
9
+ const BODY_FONT = "7px ui-monospace, Menlo, Consolas, monospace";
10
+ const OUTLINE = "#14141f";
11
+ const rect = (c, color, x, y, w, h) => {
12
+ c.fillStyle = color;
13
+ c.fillRect(Math.round(x), Math.round(y), w, h);
14
+ };
15
+ function shade(color, amount) {
16
+ if (!color.startsWith("#"))
17
+ return color;
18
+ const hex = color.length === 4 ? `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}` : color;
19
+ const n = parseInt(hex.slice(1, 7), 16);
20
+ const channel = (shift) => Math.max(0, Math.min(255, ((n >> shift) & 255) + amount));
21
+ return `rgb(${channel(16)},${channel(8)},${channel(0)})`;
22
+ }
23
+ export function daylightAt(hour) {
24
+ if (hour >= 7 && hour < 17)
25
+ return { sky: "#8ed0ff", sun: "rgba(255,244,214,0.10)", night: 0 };
26
+ if (hour >= 17 && hour < 20)
27
+ return { sky: "#ffae70", sun: "rgba(255,170,110,0.13)", night: 0.14 };
28
+ if (hour >= 5 && hour < 7)
29
+ return { sky: "#ffc9a3", sun: "rgba(255,200,170,0.08)", night: 0.16 };
30
+ return { sky: "#16204a", sun: "rgba(0,0,0,0)", night: 0.24 };
31
+ }
32
+ /** Parts draw with one outline around the whole figure, so characters read clearly on any floor. */
33
+ function drawParts(c, parts) {
34
+ c.fillStyle = OUTLINE;
35
+ for (const [, x, y, w, h] of parts)
36
+ c.fillRect(Math.round(x) - 1, Math.round(y) - 1, w + 2, h + 2);
37
+ for (const [color, x, y, w, h] of parts)
38
+ rect(c, color, x, y, w, h);
39
+ }
40
+ function drawAgent(c, agent, time, selected) {
41
+ const left = agent.x * TILE + 2; // sprite is 12 wide inside a 16 tile
42
+ const feet = agent.y * TILE + 15;
43
+ const seated = isSeated(agent);
44
+ const walking = agent.walking;
45
+ const side = agent.facing === "left" || agent.facing === "right";
46
+ const back = agent.facing === "up";
47
+ const flip = agent.facing === "left";
48
+ const phase = Math.floor(time * 9 + (agent.seed % 5));
49
+ const step = walking ? phase % 4 : 0; // 0 pass, 1 left forward, 2 pass, 3 right forward
50
+ const bob = walking && step % 2 === 1 ? -1 : 0;
51
+ const breathe = !walking && !seated && Math.floor(time * 1.4 + (agent.seed % 7)) % 2 === 0 ? 1 : 0;
52
+ const party = agent.party > 0 ? -Math.abs(Math.round(Math.sin(time * 12) * 3)) : 0;
53
+ const skin = agent.skin;
54
+ const hair = agent.hair;
55
+ const shirt = agent.shirt;
56
+ const dark = shade(shirt, -40);
57
+ const light = shade(shirt, 24);
58
+ const legs = "#39415a";
59
+ const shoe = "#1f2433";
60
+ const style = agent.seed % 5; // short, long, bun, spiky, cap
61
+ const outfit = (agent.seed >>> 5) % 3; // tee, hoodie, shirt and tie
62
+ // Shadow, and a highlight under whoever is selected.
63
+ c.fillStyle = "rgba(0,0,0,0.28)";
64
+ c.fillRect(Math.round(left + 1), Math.round(feet - 1), 10, 2);
65
+ if (selected) {
66
+ c.fillStyle = "rgba(255,255,255,0.45)";
67
+ c.fillRect(Math.round(left - 2), Math.round(feet - 2), 16, 4);
68
+ }
69
+ if (agent.state === "blocked") {
70
+ c.fillStyle = Math.floor(time * 3) % 2 === 0 ? "rgba(239,68,68,0.55)" : "rgba(239,68,68,0.22)";
71
+ c.fillRect(Math.round(left - 2), Math.round(feet - 2), 16, 4);
72
+ }
73
+ const lift = bob + party;
74
+ const parts = [];
75
+ const bodyTop = feet - (seated ? 13 : 13) + lift + breathe * 0;
76
+ const headTop = bodyTop - 8 + breathe;
77
+ // Chair behind someone sitting at a desk.
78
+ if (seated) {
79
+ rect(c, "#1f2937", left + 1, agent.y * TILE + 3, 10, 9);
80
+ rect(c, "#374151", left + 2, agent.y * TILE + 11, 8, 3);
81
+ rect(c, "#111827", left + 5, agent.y * TILE + 14, 2, 2);
82
+ }
83
+ // Legs
84
+ if (!seated) {
85
+ const legTop = bodyTop + 7;
86
+ if (side) {
87
+ const swing = step === 1 ? 2 : step === 3 ? -2 : 0;
88
+ parts.push([legs, left + 4 + swing, legTop, 3, 5], [shoe, left + 4 + swing + (flip ? -1 : 0), legTop + 5, 4, 1]);
89
+ parts.push([shade(legs, -12), left + 5 - swing, legTop, 3, 5], [shoe, left + 5 - swing + (flip ? -1 : 0), legTop + 5, 4, 1]);
90
+ }
91
+ else {
92
+ const leftUp = step === 1 ? 1 : 0;
93
+ const rightUp = step === 3 ? 1 : 0;
94
+ parts.push([legs, left + 2, legTop, 3, 5 - leftUp], [shoe, left + 2, legTop + 5 - leftUp, 3, 1]);
95
+ parts.push([legs, left + 7, legTop, 3, 5 - rightUp], [shoe, left + 7, legTop + 5 - rightUp, 3, 1]);
96
+ }
97
+ }
98
+ // Body and arms
99
+ const swingArm = walking ? (step === 1 ? 1 : step === 3 ? -1 : 0) : 0;
100
+ if (side) {
101
+ parts.push([shirt, left + 3, bodyTop, 6, 7], [dark, left + 3, bodyTop + 6, 6, 1]);
102
+ parts.push([dark, left + 5 + swingArm, bodyTop + 1, 2, 5], [skin, left + 5 + swingArm, bodyTop + 6, 2, 1]);
103
+ }
104
+ else {
105
+ parts.push([shirt, left + 1, bodyTop, 10, 7], [dark, left + 1, bodyTop + 6, 10, 1]);
106
+ const typing = seated && agent.state === "working";
107
+ const type = Math.floor(time * 10 + agent.seed) % 2;
108
+ parts.push([dark, left - 1, bodyTop + 1 + swingArm + (typing ? -type : 0), 2, 5]);
109
+ parts.push([dark, left + 11, bodyTop + 1 - swingArm + (typing ? type - 1 : 0), 2, 5]);
110
+ parts.push([skin, left - 1, bodyTop + 6 + swingArm + (typing ? -type : 0), 2, 1]);
111
+ parts.push([skin, left + 11, bodyTop + 6 - swingArm + (typing ? type - 1 : 0), 2, 1]);
112
+ if (!back && outfit === 2)
113
+ parts.push(["#f3f4f6", left + 4, bodyTop, 4, 3], [agent.seed % 2 ? "#ef4444" : "#1d4ed8", left + 5, bodyTop, 2, 5]);
114
+ if (!back && outfit === 1)
115
+ parts.push([light, left + 3, bodyTop, 6, 2], [dark, left + 5, bodyTop + 2, 2, 4]);
116
+ if (back && outfit === 1)
117
+ parts.push([light, left + 2, bodyTop, 8, 3]);
118
+ }
119
+ // Head
120
+ parts.push([skin, left + 1, headTop, 10, 8]);
121
+ if (back) {
122
+ parts.push([hair, left + 1, headTop, 10, 7]);
123
+ }
124
+ else if (side) {
125
+ const backX = flip ? left + 7 : left + 1;
126
+ parts.push([hair, left + 1, headTop, 10, 3], [hair, backX, headTop, 4, 6]);
127
+ const blink = Math.floor(time * 0.7 + agent.seed % 11) % 7 === 0;
128
+ parts.push([blink ? shade(skin, -40) : "#1a1a24", flip ? left + 3 : left + 8, headTop + 4, 1, blink ? 1 : 2]);
129
+ }
130
+ else {
131
+ parts.push([hair, left + 1, headTop, 10, 3], [hair, left + 1, headTop + 3, 1, 2], [hair, left + 10, headTop + 3, 1, 2]);
132
+ const blink = Math.floor(time * 0.7 + agent.seed % 11) % 7 === 0;
133
+ parts.push([blink ? shade(skin, -40) : "#1a1a24", left + 3, headTop + 4, 2, blink ? 1 : 2]);
134
+ parts.push([blink ? shade(skin, -40) : "#1a1a24", left + 7, headTop + 4, 2, blink ? 1 : 2]);
135
+ parts.push([shade(skin, -28), left + 5, headTop + 6, 2, 1]);
136
+ }
137
+ if (style === 1)
138
+ parts.push([hair, left, headTop + 2, 2, 8], [hair, left + 10, headTop + 2, 2, 8]); // long
139
+ if (style === 2)
140
+ parts.push([hair, left + 4, headTop - 3, 4, 3]); // bun
141
+ if (style === 3)
142
+ parts.push([hair, left + 2, headTop - 2, 2, 2], [hair, left + 5, headTop - 3, 2, 3], [hair, left + 8, headTop - 2, 2, 2]); // spiky
143
+ if (style === 4) { // cap
144
+ parts.push([light, left, headTop - 1, 12, 3]);
145
+ if (!back && !side)
146
+ parts.push([dark, left + 1, headTop + 2, 10, 1]);
147
+ if (side)
148
+ parts.push([dark, flip ? left - 2 : left + 9, headTop + 1, 5, 1]);
149
+ }
150
+ drawParts(c, parts);
151
+ if (agent.party > 0) {
152
+ const colors = ["#f59e0b", "#ec4899", "#22c55e", "#3b82f6", "#a855f7"];
153
+ for (let i = 0; i < 10; i++) {
154
+ const t = (time * 1.1 + i * 0.137) % 1;
155
+ const angle = i * 2.4 + agent.seed;
156
+ rect(c, colors[i % colors.length], left + 6 + Math.cos(angle) * (4 + t * 14), feet - 22 - t * 6 + t * t * 26, 2, 2);
157
+ }
158
+ }
159
+ }
160
+ function bubble(c, x, y, draw) {
161
+ rect(c, OUTLINE, x - 1, y - 1, 13, 11);
162
+ rect(c, "#ffffff", x, y, 11, 9);
163
+ rect(c, "#ffffff", x + 4, y + 9, 3, 1);
164
+ rect(c, OUTLINE, x + 4, y + 10, 3, 1);
165
+ draw(x, y);
166
+ }
167
+ function drawStatus(c, agent, time) {
168
+ const x = Math.round(agent.x * TILE + 2);
169
+ const y = Math.round(agent.y * TILE - 22 + (isSeated(agent) ? 2 : -2));
170
+ const pulse = Math.floor(time * 2 + (agent.seed % 3)) % 2 === 0;
171
+ if (agent.state === "blocked") {
172
+ bubble(c, x, y, (bx, by) => {
173
+ rect(c, pulse ? "#dc2626" : "#991b1b", bx + 5, by + 1, 2, 5);
174
+ rect(c, pulse ? "#dc2626" : "#991b1b", bx + 5, by + 7, 2, 1);
175
+ });
176
+ }
177
+ else if (agent.state === "thinking") {
178
+ const dots = Math.floor(time * 3) % 4;
179
+ bubble(c, x, y, (bx, by) => {
180
+ for (let i = 0; i < 3; i++)
181
+ rect(c, i < dots ? "#111827" : "#d1d5db", bx + 2 + i * 3, by + 4, 2, 2);
182
+ });
183
+ }
184
+ else if (agent.state === "reviewing" && !agent.walking) {
185
+ bubble(c, x, y, (bx, by) => {
186
+ rect(c, "#2563eb", bx + 4, by + 1, 4, 1);
187
+ rect(c, "#2563eb", bx + 7, by + 2, 1, 2);
188
+ rect(c, "#2563eb", bx + 5, by + 4, 2, 1);
189
+ rect(c, "#2563eb", bx + 5, by + 5, 1, 1);
190
+ rect(c, "#2563eb", bx + 5, by + 7, 1, 1);
191
+ });
192
+ }
193
+ else if (agent.state === "done") {
194
+ const color = pulse ? "#f59e0b" : "#fbbf24";
195
+ bubble(c, x, y, (bx, by) => {
196
+ rect(c, color, bx + 5, by + 1, 1, 7);
197
+ rect(c, color, bx + 2, by + 3, 7, 2);
198
+ rect(c, color, bx + 4, by + 2, 3, 5);
199
+ });
200
+ }
201
+ else if (agent.state === "idle" && !agent.walking && agent.seed % 3 === 0) {
202
+ bubble(c, x, y, (bx, by) => {
203
+ rect(c, "#92400e", bx + 3, by + 3, 4, 4);
204
+ rect(c, "#d97706", bx + 3, by + 3, 4, 1);
205
+ rect(c, "#92400e", bx + 7, by + 4, 1, 2);
206
+ if (pulse)
207
+ rect(c, "#9ca3af", bx + 4, by + 1, 1, 1);
208
+ });
209
+ }
210
+ }
211
+ // --- The building ----------------------------------------------------------------------------------------
212
+ function drawFloor(c, room) {
213
+ for (let ty = room.y; ty < room.y + room.h; ty++) {
214
+ for (let tx = room.x; tx < room.x + room.w; tx++) {
215
+ const x = tx * TILE;
216
+ const y = ty * TILE;
217
+ const noise = (tx * 73 + ty * 151) % 17;
218
+ switch (room.floor) {
219
+ case "carpet":
220
+ rect(c, "#4c4f7a", x, y, TILE, TILE);
221
+ for (let i = 0; i < 8; i++)
222
+ rect(c, i % 2 ? "#55588a" : "#454874", x + ((tx * 7 + i * 5) % 15), y + ((ty * 3 + i * 7) % 15), 1, 1);
223
+ break;
224
+ case "tile":
225
+ rect(c, (tx + ty) % 2 === 0 ? "#d6dee8" : "#c2ccd8", x, y, TILE, TILE);
226
+ rect(c, "#aab5c2", x, y + 15, TILE, 1);
227
+ rect(c, "#aab5c2", x + 15, y, 1, TILE);
228
+ rect(c, "#e8eef5", x + 1, y + 1, 5, 1);
229
+ break;
230
+ case "stone":
231
+ rect(c, noise % 3 === 0 ? "#8d93a3" : "#9aa0b0", x, y, TILE, TILE);
232
+ rect(c, "#7a8090", x, y + 15, TILE, 1);
233
+ rect(c, "#7a8090", x + (ty % 2 ? 7 : 15), y, 1, 15);
234
+ rect(c, "#adb3c2", x + 2, y + 2, 3, 1);
235
+ break;
236
+ case "concrete":
237
+ rect(c, "#6b7280", x, y, TILE, TILE);
238
+ for (let i = 0; i < 5; i++)
239
+ rect(c, i % 2 ? "#747b89" : "#636a78", x + ((tx * 11 + i * 3) % 15), y + ((ty * 5 + i * 5) % 15), 1, 1);
240
+ if (noise === 3)
241
+ rect(c, "#596070", x + 3, y + 8, 8, 1);
242
+ break;
243
+ case "grass":
244
+ rect(c, "#3f8f4f", x, y, TILE, TILE);
245
+ for (let i = 0; i < 7; i++)
246
+ rect(c, i % 3 ? "#4aa35c" : "#347a43", x + ((tx * 5 + i * 9) % 15), y + ((ty * 7 + i * 3) % 14), 1, 2);
247
+ break;
248
+ default: {
249
+ const plank = ty % 2 === 0 ? "#a57448" : "#9c6c42";
250
+ rect(c, plank, x, y, TILE, TILE);
251
+ rect(c, "#8a5d36", x, y + 7, TILE, 1);
252
+ rect(c, "#8a5d36", x, y + 15, TILE, 1);
253
+ rect(c, "#8a5d36", x + ((tx * 8 + (ty % 2 ? 8 : 0)) % 16), y, 1, 7);
254
+ rect(c, "#8a5d36", x + ((tx * 8 + (ty % 2 ? 0 : 8)) % 16), y + 8, 1, 7);
255
+ rect(c, "#b4835a", x + (noise % 9) + 2, y + 3, 4, 1);
256
+ rect(c, "#b4835a", x + (noise % 7) + 4, y + 11, 3, 1);
257
+ }
258
+ }
259
+ }
260
+ }
261
+ }
262
+ const WALL_TOP = "#2a2d48";
263
+ const WALL_FACE = "#5c628f";
264
+ const WALL_DARK = "#4a4f78";
265
+ const WALL_LIGHT = "#6d74a4";
266
+ function drawWalls(c, world, palette, daylight) {
267
+ const open = new Set();
268
+ for (const room of world.rooms) {
269
+ for (let y = room.y; y < room.y + room.h; y++)
270
+ for (let x = room.x; x < room.x + room.w; x++)
271
+ open.add(`${x},${y}`);
272
+ }
273
+ const doors = new Set(world.doors.map((door) => `${door.x},${door.y}`));
274
+ for (let ty = 0; ty < world.rows; ty++) {
275
+ for (let tx = 0; tx < world.cols; tx++) {
276
+ const key = `${tx},${ty}`;
277
+ if (open.has(key) || doors.has(key))
278
+ continue;
279
+ const x = tx * TILE;
280
+ const y = ty * TILE;
281
+ if (open.has(`${tx},${ty + 1}`)) {
282
+ rect(c, WALL_FACE, x, y, TILE, TILE);
283
+ rect(c, WALL_LIGHT, x, y, TILE, 1);
284
+ rect(c, WALL_DARK, x, y + 12, TILE, 4);
285
+ rect(c, palette.primary, x, y + 11, TILE, 1);
286
+ if (tx % 4 === 3 && !world.furniture.some((item) => FURNITURE[item.type].wall && item.x === tx && item.y === ty)) {
287
+ // A window, in the colour of the sky right now.
288
+ rect(c, "#1f2937", x + 1, y + 1, 14, 9);
289
+ rect(c, daylight.sky, x + 2, y + 2, 12, 7);
290
+ rect(c, shade(daylight.sky, 40), x + 3, y + 3, 4, 2);
291
+ rect(c, "#1f2937", x + 8, y + 2, 1, 7);
292
+ if (daylight.night > 0.3) {
293
+ rect(c, "#fef3c7", x + 4, y + 3, 1, 1);
294
+ rect(c, "#fef3c7", x + 11, y + 5, 1, 1);
295
+ }
296
+ }
297
+ }
298
+ else {
299
+ rect(c, WALL_TOP, x, y, TILE, TILE);
300
+ if (open.has(`${tx},${ty - 1}`))
301
+ rect(c, "#20233a", x, y, TILE, 3);
302
+ if (open.has(`${tx - 1},${ty}`))
303
+ rect(c, "#20233a", x, y, 2, TILE);
304
+ if (open.has(`${tx + 1},${ty}`))
305
+ rect(c, "#20233a", x + 14, y, 2, TILE);
306
+ }
307
+ }
308
+ }
309
+ for (const door of world.doors) {
310
+ const x = door.x * TILE;
311
+ const y = door.y * TILE;
312
+ const horizontal = open.has(`${door.x},${door.y - 1}`) || open.has(`${door.x},${door.y + 1}`);
313
+ rect(c, "#7c5637", x, y, TILE, TILE);
314
+ for (let i = 2; i < 16; i += 4)
315
+ rect(c, "#6b4a2f", horizontal ? x : x + i, horizontal ? y + i : y, horizontal ? TILE : 1, horizontal ? 1 : TILE);
316
+ if (horizontal) {
317
+ rect(c, WALL_DARK, x, y, 2, TILE);
318
+ rect(c, WALL_DARK, x + 14, y, 2, TILE);
319
+ }
320
+ else {
321
+ rect(c, WALL_DARK, x, y, TILE, 2);
322
+ rect(c, WALL_DARK, x, y + 14, TILE, 2);
323
+ }
324
+ }
325
+ }
326
+ function drawLabels(c, world) {
327
+ c.font = LABEL_FONT;
328
+ c.textBaseline = "top";
329
+ for (const room of world.rooms) {
330
+ if (!room.name)
331
+ continue;
332
+ const label = room.name.toUpperCase();
333
+ const width = Math.ceil(c.measureText(label).width) + 6;
334
+ const x = room.x * TILE + 3;
335
+ const y = room.y * TILE - 13;
336
+ rect(c, "#1c1f33", x, y, width, 10);
337
+ rect(c, "#2f3350", x + 1, y + 1, width - 2, 8);
338
+ c.fillStyle = "#f8fafc";
339
+ c.fillText(label, x + 3, y + 1.5);
340
+ }
341
+ }
342
+ /** Everything that doesn't move. Redrawn only when the building or the hour changes. */
343
+ export function drawBuilding(c, world, palette, daylight) {
344
+ rect(c, WALL_TOP, 0, 0, world.cols * TILE, world.rows * TILE);
345
+ for (const room of world.rooms)
346
+ drawFloor(c, room);
347
+ drawWalls(c, world, palette, daylight);
348
+ for (const item of world.furniture)
349
+ if (item.type === "rug")
350
+ drawFurniture(c, item, world, 0, palette);
351
+ for (const item of world.furniture)
352
+ if (FURNITURE[item.type].wall)
353
+ drawFurniture(c, item, world, 0, palette);
354
+ // Light from the windows falling across the floor.
355
+ if (daylight.sun !== "rgba(0,0,0,0)") {
356
+ c.fillStyle = daylight.sun;
357
+ for (const room of world.rooms) {
358
+ for (let tx = room.x; tx < room.x + room.w; tx++) {
359
+ if (tx % 4 !== 3)
360
+ continue;
361
+ c.fillRect(tx * TILE + 2, room.y * TILE, 12, Math.min(room.h, 3) * TILE);
362
+ }
363
+ }
364
+ }
365
+ }
366
+ function drawDesk(c, x, y, on, time, seed, palette, blocked) {
367
+ rect(c, "#55351d", x + 1, y + 22, 46, 8);
368
+ rect(c, "#6b4427", x + 1, y + 13, 46, 10);
369
+ rect(c, "#8a5a33", x + 1, y + 10, 46, 12);
370
+ rect(c, "#a87545", x + 1, y + 10, 46, 2);
371
+ rect(c, "#3b2413", x + 3, y + 22, 3, 9);
372
+ rect(c, "#3b2413", x + 42, y + 22, 3, 9);
373
+ rect(c, "#55351d", x + 30, y + 23, 12, 7);
374
+ rect(c, "#7c5637", x + 31, y + 24, 10, 2);
375
+ rect(c, "#d4a24e", x + 35, y + 24, 2, 1);
376
+ rect(c, "#7c5637", x + 31, y + 27, 10, 2);
377
+ rect(c, "#d4a24e", x + 35, y + 27, 2, 1);
378
+ // Monitor
379
+ rect(c, OUTLINE, x + 14, y - 3, 20, 15);
380
+ rect(c, "#374151", x + 22, y + 12, 4, 2);
381
+ rect(c, "#4b5563", x + 19, y + 14, 10, 1);
382
+ if (blocked) {
383
+ rect(c, "#3f0f14", x + 15, y - 2, 18, 13);
384
+ if (Math.floor(time * 2) % 2 === 0) {
385
+ rect(c, "#ef4444", x + 23, y, 2, 6);
386
+ rect(c, "#ef4444", x + 23, y + 7, 2, 2);
387
+ }
388
+ }
389
+ else if (on) {
390
+ rect(c, "#0f2a2e", x + 15, y - 2, 18, 13);
391
+ const scroll = Math.floor(time * 3 + (seed % 13));
392
+ for (let line = 0; line < 5; line++) {
393
+ const width = 3 + (((seed >> line) & 7) + (scroll + line) * 7) % 12;
394
+ rect(c, line % 3 === 0 ? palette.primary : line % 3 === 1 ? "#7dd3fc" : "#fcd34d", x + 16 + (line % 2) * 2, y - 1 + line * 2.2, width, 1);
395
+ }
396
+ c.fillStyle = "rgba(125,211,252,0.12)";
397
+ c.fillRect(x + 10, y + 11, 28, 8);
398
+ }
399
+ else {
400
+ rect(c, "#1f2937", x + 15, y - 2, 18, 13);
401
+ rect(c, "#374151", x + 17, y, 3, 1);
402
+ }
403
+ rect(c, "#d1d5db", x + 16, y + 15, 16, 5);
404
+ for (let row = 0; row < 2; row++)
405
+ for (let key = 0; key < 7; key++)
406
+ rect(c, "#9ca3af", x + 17 + key * 2, y + 16 + row * 2, 1, 1);
407
+ rect(c, "#e5e7eb", x + 34, y + 16, 3, 4);
408
+ rect(c, "#f3f4f6", x + 40, y + 13, 5, 5);
409
+ rect(c, palette.primary, x + 40, y + 15, 5, 1);
410
+ rect(c, "#fde68a", x + 4, y + 13, 7, 6);
411
+ rect(c, "#fbbf24", x + 5, y + 15, 5, 1);
412
+ }
413
+ function drawBoard(c, x, y, world) {
414
+ rect(c, "#3a2c1e", x, y - 12, 64, 27);
415
+ rect(c, "#d9c29c", x + 2, y - 10, 60, 23);
416
+ for (let i = 0; i < 40; i++)
417
+ rect(c, "#c9b08a", x + 3 + (i * 37) % 58, y - 9 + (i * 13) % 21, 1, 1);
418
+ const colors = { todo: "#fde68a", doing: "#93c5fd", done: "#86efac" };
419
+ const per = 8;
420
+ world.tasks.slice(0, per * 2).forEach((task, index) => {
421
+ const col = index % per;
422
+ const row = Math.floor(index / per);
423
+ rect(c, "#00000033", x + 5 + col * 7, y - 7 + row * 10, 6, 8);
424
+ rect(c, colors[task.state], x + 4 + col * 7, y - 8 + row * 10, 6, 8);
425
+ rect(c, "#ef4444", x + 6 + col * 7, y - 8 + row * 10, 2, 1);
426
+ rect(c, "#00000040", x + 5 + col * 7, y - 5 + row * 10, 4, 1);
427
+ rect(c, "#00000040", x + 5 + col * 7, y - 3 + row * 10, 3, 1);
428
+ });
429
+ }
430
+ function drawFurniture(c, item, world, time, palette) {
431
+ const x = item.x * TILE;
432
+ const y = item.y * TILE;
433
+ switch (item.type) {
434
+ case "desk": {
435
+ const seatX = item.x + 1;
436
+ const seatY = item.y + 2;
437
+ const user = world.agents.find((a) => a.seat?.x === seatX && a.seat?.y === seatY && isSeated(a));
438
+ drawDesk(c, x, y, Boolean(user), time, user?.seed ?? 0, palette, user?.state === "blocked");
439
+ break;
440
+ }
441
+ case "coffee":
442
+ rect(c, "#374151", x, y + 2, 32, 14);
443
+ rect(c, "#6b7280", x, y + 2, 32, 2);
444
+ rect(c, OUTLINE, x + 4, y - 9, 15, 15);
445
+ rect(c, "#27272a", x + 5, y - 8, 13, 13);
446
+ rect(c, Math.floor(time * 2) % 2 ? "#ef4444" : "#7f1d1d", x + 15, y - 6, 2, 2);
447
+ rect(c, "#52525b", x + 7, y - 6, 6, 4);
448
+ rect(c, "#f3f4f6", x + 8, y + 3, 5, 4);
449
+ rect(c, "#f3f4f6", x + 23, y - 1, 5, 5);
450
+ rect(c, "#92400e", x + 24, y, 3, 1);
451
+ for (let i = 0; i < 3; i++) {
452
+ c.fillStyle = "rgba(255,255,255,0.5)";
453
+ c.fillRect(x + 9 + Math.round(Math.sin(time * 2 + i) * 1.5), y - 1 - ((time * 8 + i * 4) % 9), 1, 2);
454
+ }
455
+ break;
456
+ case "water":
457
+ rect(c, "#d1d5db", x + 3, y + 3, 10, 13);
458
+ rect(c, "#9ca3af", x + 3, y + 14, 10, 2);
459
+ rect(c, "#60a5fa", x + 4, y - 8, 8, 11);
460
+ rect(c, "#bae6fd", x + 5, y - 7, 2, 9);
461
+ rect(c, "#3b82f6", x + 5, y + 6, 2, 2);
462
+ rect(c, "#ef4444", x + 9, y + 6, 2, 2);
463
+ if (Math.floor(time * 0.4 + item.x) % 5 === 0)
464
+ rect(c, "#e0f2fe", x + 8, y - 2 - ((time * 6) % 7), 1, 1);
465
+ break;
466
+ case "sofa":
467
+ rect(c, shade(palette.primary, -60), x + 1, y - 7, 46, 11);
468
+ rect(c, shade(palette.primary, -30), x + 2, y - 6, 44, 3);
469
+ rect(c, "#4b5563", x, y + 3, 48, 11);
470
+ rect(c, "#6b7280", x + 3, y + 3, 20, 7);
471
+ rect(c, "#6b7280", x + 25, y + 3, 20, 7);
472
+ rect(c, "#374151", x, y + 12, 48, 3);
473
+ rect(c, "#374151", x - 1, y - 2, 4, 16);
474
+ rect(c, "#374151", x + 45, y - 2, 4, 16);
475
+ break;
476
+ case "plant":
477
+ rect(c, "#92400e", x + 4, y + 9, 8, 7);
478
+ rect(c, "#b45309", x + 3, y + 8, 10, 2);
479
+ for (const [lx, ly, lw, lh] of [[6, -3, 4, 11], [2, 0, 5, 6], [9, 1, 5, 6], [4, -6, 3, 6], [9, -5, 3, 5]])
480
+ rect(c, "#15803d", x + lx, y + ly, lw, lh);
481
+ rect(c, "#22c55e", x + 7, y - 3, 2, 8);
482
+ rect(c, "#22c55e", x + 3, y + 1, 2, 3);
483
+ rect(c, "#4ade80", x + 10, y - 4, 1, 3);
484
+ break;
485
+ case "fern":
486
+ rect(c, "#57534e", x + 3, y + 10, 10, 6);
487
+ rect(c, "#78716c", x + 2, y + 9, 12, 2);
488
+ for (let i = 0; i < 7; i++) {
489
+ const lean = i - 3;
490
+ for (let j = 0; j < 5; j++)
491
+ rect(c, j % 2 ? "#16a34a" : "#15803d", x + 7 + lean * (j / 2 + 1), y + 8 - j * 2 + Math.abs(lean), 2, 2);
492
+ }
493
+ break;
494
+ case "board":
495
+ drawBoard(c, x, y, world);
496
+ break;
497
+ case "whiteboard":
498
+ rect(c, "#9ca3af", x, y - 10, 48, 22);
499
+ rect(c, "#f9fafb", x + 2, y - 8, 44, 18);
500
+ rect(c, "#3b82f6", x + 5, y - 5, 14, 1);
501
+ rect(c, "#3b82f6", x + 5, y - 2, 9, 1);
502
+ rect(c, "#ef4444", x + 24, y - 6, 8, 6);
503
+ rect(c, "#f9fafb", x + 25, y - 5, 6, 4);
504
+ rect(c, "#10b981", x + 8, y + 3, 20, 1);
505
+ rect(c, "#10b981", x + 27, y + 2, 1, 3);
506
+ rect(c, "#6b7280", x + 2, y + 10, 44, 2);
507
+ break;
508
+ case "shelf":
509
+ rect(c, "#5b3a21", x, y - 12, 32, 28);
510
+ for (let shelf = 0; shelf < 2; shelf++) {
511
+ rect(c, "#3b2413", x + 2, y - 10 + shelf * 13, 28, 11);
512
+ for (let book = 0; book < 8; book++) {
513
+ const color = ["#ef4444", "#3b82f6", "#10b981", "#f59e0b", "#8b5cf6"][(book * 3 + shelf + item.x) % 5];
514
+ rect(c, color, x + 3 + book * 3, y - 8 + shelf * 13 + (book % 3), 2, 9 - (book % 3));
515
+ rect(c, shade(color, 40), x + 3 + book * 3, y - 6 + shelf * 13, 2, 1);
516
+ }
517
+ }
518
+ break;
519
+ case "server":
520
+ rect(c, OUTLINE, x, y - 13, 16, 29);
521
+ rect(c, "#1f2937", x + 1, y - 12, 14, 27);
522
+ for (let i = 0; i < 5; i++) {
523
+ rect(c, "#111827", x + 2, y - 11 + i * 5, 12, 4);
524
+ const on = Math.floor(time * 3 + i * 2 + item.x) % 3 !== 0;
525
+ rect(c, on ? "#22c55e" : "#14532d", x + 3, y - 10 + i * 5, 1, 1);
526
+ rect(c, i % 2 === 0 ? "#f59e0b" : "#3b82f6", x + 5, y - 10 + i * 5, 1, 1);
527
+ rect(c, "#374151", x + 8, y - 10 + i * 5, 5, 1);
528
+ }
529
+ break;
530
+ case "table":
531
+ rect(c, "#44403c", x + 2, y + 16, 44, 12);
532
+ rect(c, "#78716c", x + 2, y + 3, 44, 14);
533
+ rect(c, "#a8a29e", x + 2, y + 3, 44, 2);
534
+ rect(c, "#f9fafb", x + 10, y + 6, 8, 6);
535
+ rect(c, "#f9fafb", x + 28, y + 8, 8, 6);
536
+ rect(c, "#1f2937", x + 12, y + 8, 4, 1);
537
+ rect(c, "#3b82f6", x + 30, y + 10, 4, 1);
538
+ break;
539
+ case "rug":
540
+ rect(c, palette.primary, x, y, 64, 48);
541
+ c.fillStyle = "rgba(0,0,0,0.42)";
542
+ c.fillRect(x + 3, y + 3, 58, 42);
543
+ rect(c, palette.primary, x + 6, y + 6, 52, 1);
544
+ rect(c, palette.primary, x + 6, y + 41, 52, 1);
545
+ rect(c, palette.primary, x + 6, y + 6, 1, 36);
546
+ rect(c, palette.primary, x + 57, y + 6, 1, 36);
547
+ for (let i = 0; i < 6; i++)
548
+ rect(c, palette.primary, x + 12 + i * 8, y + 22, 4, 4);
549
+ break;
550
+ case "arcade":
551
+ rect(c, OUTLINE, x, y - 14, 16, 30);
552
+ rect(c, "#4c1d95", x + 1, y - 13, 14, 28);
553
+ rect(c, "#111827", x + 3, y - 10, 10, 9);
554
+ rect(c, ["#22c55e", "#ec4899", "#38bdf8"][Math.floor(time * 2) % 3], x + 4 + (Math.floor(time * 4) % 7), y - 8 + (Math.floor(time * 3) % 5), 2, 2);
555
+ rect(c, "#f59e0b", x + 3, y - 13, 10, 2);
556
+ rect(c, "#27272a", x + 2, y + 1, 12, 4);
557
+ rect(c, "#ef4444", x + 4, y + 2, 2, 2);
558
+ rect(c, "#3b82f6", x + 9, y + 2, 2, 2);
559
+ break;
560
+ case "fridge":
561
+ rect(c, OUTLINE, x + 1, y - 12, 14, 28);
562
+ rect(c, "#e5e7eb", x + 2, y - 11, 12, 26);
563
+ rect(c, "#9ca3af", x + 2, y - 3, 12, 1);
564
+ rect(c, "#6b7280", x + 11, y - 8, 1, 3);
565
+ rect(c, "#6b7280", x + 11, y, 1, 5);
566
+ rect(c, "#fde68a", x + 4, y - 9, 3, 3);
567
+ rect(c, "#fca5a5", x + 5, y + 3, 3, 3);
568
+ break;
569
+ case "printer":
570
+ rect(c, "#4b5563", x + 2, y + 8, 12, 8);
571
+ rect(c, OUTLINE, x + 1, y - 2, 14, 11);
572
+ rect(c, "#d1d5db", x + 2, y - 1, 12, 9);
573
+ rect(c, "#f9fafb", x + 4, y - 5 - (Math.floor(time) % 4 === 0 ? 1 : 0), 8, 5);
574
+ rect(c, "#22c55e", x + 11, y + 1, 2, 1);
575
+ rect(c, "#9ca3af", x + 3, y + 5, 10, 1);
576
+ break;
577
+ case "lamp":
578
+ rect(c, "#374151", x + 5, y + 13, 6, 3);
579
+ rect(c, "#4b5563", x + 7, y - 4, 2, 17);
580
+ rect(c, OUTLINE, x + 2, y - 10, 12, 8);
581
+ rect(c, "#fde68a", x + 3, y - 9, 10, 6);
582
+ rect(c, "#fef3c7", x + 4, y - 8, 4, 2);
583
+ break;
584
+ case "beanbag":
585
+ rect(c, OUTLINE, x, y + 3, 16, 13);
586
+ rect(c, shade(palette.primary, -50), x + 1, y + 4, 14, 11);
587
+ rect(c, shade(palette.primary, -20), x + 3, y + 5, 8, 4);
588
+ rect(c, "#00000040", x + 1, y + 13, 14, 2);
589
+ break;
590
+ case "tv":
591
+ rect(c, "#3b2413", x + 2, y + 8, 44, 8);
592
+ rect(c, "#5b3a21", x + 2, y + 8, 44, 2);
593
+ rect(c, OUTLINE, x + 4, y - 14, 40, 22);
594
+ rect(c, "#0b1120", x + 5, y - 13, 38, 20);
595
+ for (let i = 0; i < 9; i++) {
596
+ const h = 2 + ((i * 5 + Math.floor(time * 3)) % 12);
597
+ rect(c, i % 3 === 0 ? palette.primary : i % 3 === 1 ? "#38bdf8" : "#f472b6", x + 8 + i * 4, y + 5 - h, 3, h);
598
+ }
599
+ break;
600
+ case "vending":
601
+ rect(c, OUTLINE, x, y - 14, 16, 30);
602
+ rect(c, "#b91c1c", x + 1, y - 13, 14, 28);
603
+ rect(c, "#0f172a", x + 3, y - 11, 8, 16);
604
+ for (let row = 0; row < 3; row++)
605
+ for (let col = 0; col < 2; col++)
606
+ rect(c, ["#fbbf24", "#34d399", "#60a5fa", "#f472b6"][(row + col * 2) % 4], x + 4 + col * 3, y - 10 + row * 5, 2, 4);
607
+ rect(c, "#d1d5db", x + 12, y - 9, 2, 6);
608
+ rect(c, "#111827", x + 3, y + 8, 10, 4);
609
+ break;
610
+ case "trophy":
611
+ rect(c, "#6b4427", x + 2, y + 8, 12, 8);
612
+ rect(c, "#8a5a33", x + 2, y + 8, 12, 2);
613
+ rect(c, "#78350f", x + 5, y + 5, 6, 3);
614
+ rect(c, "#f59e0b", x + 7, y + 1, 2, 4);
615
+ rect(c, OUTLINE, x + 3, y - 8, 10, 10);
616
+ rect(c, "#fbbf24", x + 4, y - 7, 8, 8);
617
+ rect(c, "#fde68a", x + 5, y - 6, 2, 5);
618
+ rect(c, "#fbbf24", x + 1, y - 6, 3, 1);
619
+ rect(c, "#fbbf24", x + 12, y - 6, 3, 1);
620
+ break;
621
+ case "clock": {
622
+ rect(c, OUTLINE, x + 3, y + 1, 10, 10);
623
+ rect(c, "#f9fafb", x + 4, y + 2, 8, 8);
624
+ rect(c, "#1f2937", x + 8, y + 3, 1, 3);
625
+ rect(c, "#1f2937", x + 8, y + 6, 3, 1);
626
+ break;
627
+ }
628
+ case "poster": {
629
+ const colors = ["#7c3aed", "#0ea5e9", "#db2777", "#059669"];
630
+ const color = colors[(item.x + item.y) % colors.length];
631
+ rect(c, OUTLINE, x + 2, y, 12, 11);
632
+ rect(c, color, x + 3, y + 1, 10, 9);
633
+ rect(c, shade(color, 60), x + 5, y + 3, 6, 3);
634
+ rect(c, "#f9fafb", x + 5, y + 7, 6, 1);
635
+ break;
636
+ }
637
+ }
638
+ }
639
+ function nameTag(c, agent, selected, width) {
640
+ c.font = LABEL_FONT;
641
+ c.textBaseline = "top";
642
+ const w = Math.ceil(c.measureText(agent.name).width) + 5;
643
+ const x = Math.round(Math.max(1, Math.min(width - w - 1, agent.x * TILE + 8 - w / 2)));
644
+ const y = Math.round(agent.y * TILE + 16);
645
+ c.fillStyle = selected ? "#ffffff" : "rgba(17,24,39,0.82)";
646
+ c.fillRect(x, y, w, 10);
647
+ c.fillStyle = selected ? "#111827" : "#ffffff";
648
+ c.fillText(agent.name, x + 2.5, y + 1.5);
649
+ }
650
+ function wrap(c, text, max) {
651
+ const lines = [];
652
+ let line = "";
653
+ for (const word of text.split(/\s+/)) {
654
+ const next = line ? `${line} ${word}` : word;
655
+ if (c.measureText(next).width > max && line) {
656
+ lines.push(line);
657
+ line = word;
658
+ }
659
+ else {
660
+ line = next;
661
+ }
662
+ }
663
+ if (line)
664
+ lines.push(line);
665
+ return lines.slice(0, 3);
666
+ }
667
+ const STATE_LABEL = { working: "Working", thinking: "Thinking", blocked: "Blocked", reviewing: "Reviewing", idle: "On a break", done: "Finished", away: "Away" };
668
+ function drawCard(c, agent, world) {
669
+ c.font = BODY_FONT;
670
+ c.textBaseline = "top";
671
+ const room = world.rooms.find((candidate) => candidate.id === agent.area);
672
+ const body = wrap(c, agent.task || STATE_LABEL[agent.state] || "", 110);
673
+ const w = 120;
674
+ const h = 18 + body.length * 9 + (room ? 9 : 0) + 3;
675
+ const x = Math.round(Math.max(2, Math.min(world.cols * TILE - w - 2, agent.x * TILE + 8 - w / 2)));
676
+ const y = Math.round(Math.max(2, agent.y * TILE - h - 24));
677
+ rect(c, OUTLINE, x - 1, y - 1, w + 2, h + 2);
678
+ rect(c, "#ffffff", x, y, w, h);
679
+ c.fillStyle = "#111827";
680
+ c.font = LABEL_FONT;
681
+ c.fillText(`${agent.name}: ${STATE_LABEL[agent.state] ?? ""}`.slice(0, 28), x + 4, y + 4);
682
+ c.fillStyle = "#374151";
683
+ c.font = BODY_FONT;
684
+ body.forEach((line, index) => c.fillText(line, x + 4, y + 15 + index * 9));
685
+ if (room) {
686
+ c.fillStyle = "#6b7280";
687
+ c.fillText(`In ${room.name}`.slice(0, 28), x + 4, y + 15 + body.length * 9);
688
+ }
689
+ }
690
+ function drawProgress(c, world, palette) {
691
+ if (world.tasks.length === 0)
692
+ return;
693
+ const done = world.tasks.filter((task) => task.state === "done").length;
694
+ c.font = LABEL_FONT;
695
+ c.textBaseline = "top";
696
+ const w = 96;
697
+ rect(c, "rgba(17,24,39,0.85)", 4, 4, w, 20);
698
+ c.fillStyle = "#ffffff";
699
+ c.fillText(`QUESTS ${done}/${world.tasks.length}`, 8, 6.5);
700
+ rect(c, "#374151", 8, 16, w - 8, 4);
701
+ rect(c, palette.primary, 8, 16, Math.round(((w - 8) * done) / world.tasks.length), 4);
702
+ }
703
+ /** Everything that moves. `building` is the layer from drawBuilding. */
704
+ export function drawWorld(c, world, time, palette, daylight, building, selected) {
705
+ const width = world.cols * TILE;
706
+ const height = world.rows * TILE;
707
+ if (building)
708
+ c.drawImage(building, 0, 0, width, height);
709
+ else
710
+ drawBuilding(c, world, palette, daylight);
711
+ const scene = [];
712
+ for (const item of world.furniture) {
713
+ const spec = FURNITURE[item.type];
714
+ if (spec.solid === false)
715
+ continue;
716
+ scene.push({ depth: (item.y + spec.h) * TILE - 2, draw: () => drawFurniture(c, item, world, time, palette) });
717
+ }
718
+ const shown = world.agents.filter((agent) => agent.state !== "away");
719
+ for (const agent of shown)
720
+ scene.push({ depth: agent.y * TILE + 14, draw: () => drawAgent(c, agent, time, agent.id === selected) });
721
+ scene.sort((a, b) => a.depth - b.depth);
722
+ for (const item of scene)
723
+ item.draw();
724
+ // Night: the room dims, and screens and lamps light it.
725
+ if (daylight.night > 0) {
726
+ c.fillStyle = `rgba(12,14,48,${daylight.night})`;
727
+ c.fillRect(0, 0, width, height);
728
+ c.save();
729
+ c.globalCompositeOperation = "lighter";
730
+ const glow = (x, y, radius, color) => {
731
+ const gradient = c.createRadialGradient(x, y, 1, x, y, radius);
732
+ gradient.addColorStop(0, color);
733
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
734
+ c.fillStyle = gradient;
735
+ c.fillRect(x - radius, y - radius, radius * 2, radius * 2);
736
+ };
737
+ for (const item of world.furniture) {
738
+ if (item.type === "lamp")
739
+ glow(item.x * TILE + 8, item.y * TILE, 36, "rgba(253,224,139,0.30)");
740
+ if (item.type === "arcade" || item.type === "vending" || item.type === "tv")
741
+ glow(item.x * TILE + 8, item.y * TILE, 26, "rgba(125,211,252,0.16)");
742
+ if (item.type === "desk" && world.agents.some((a) => a.seat?.x === item.x + 1 && a.seat?.y === item.y + 2 && isSeated(a))) {
743
+ glow(item.x * TILE + 24, item.y * TILE + 8, 30, "rgba(125,211,252,0.26)");
744
+ }
745
+ }
746
+ c.restore();
747
+ }
748
+ // On top, so tall furniture against a wall can't hide a room's name.
749
+ drawLabels(c, world);
750
+ for (const agent of shown)
751
+ nameTag(c, agent, agent.id === selected, width);
752
+ for (const agent of shown)
753
+ if (agent.id !== selected)
754
+ drawStatus(c, agent, time);
755
+ drawProgress(c, world, palette);
756
+ const picked = shown.find((agent) => agent.id === selected);
757
+ if (picked)
758
+ drawCard(c, picked, world);
759
+ }
760
+ //# sourceMappingURL=draw.js.map