@blkc-code/render-canvas 0.1.0 → 0.6.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.d.ts +22 -12
- package/dist/draw.d.ts.map +1 -1
- package/dist/draw.js +374 -273
- package/dist/draw.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/links.d.ts +16 -0
- package/dist/links.d.ts.map +1 -0
- package/dist/links.js +75 -0
- package/dist/links.js.map +1 -0
- package/dist/office.d.ts +25 -2
- package/dist/office.d.ts.map +1 -1
- package/dist/office.js +134 -20
- package/dist/office.js.map +1 -1
- package/dist/pack.d.ts +31 -0
- package/dist/pack.d.ts.map +1 -0
- package/dist/pack.js +10 -0
- package/dist/pack.js.map +1 -0
- package/dist/paint.d.ts +15 -0
- package/dist/paint.d.ts.map +1 -0
- package/dist/paint.js +18 -0
- package/dist/paint.js.map +1 -0
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/runtime.txt +1 -1
- package/dist/types.d.ts +20 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/world.d.ts +29 -13
- package/dist/world.d.ts.map +1 -1
- package/dist/world.js +150 -72
- package/dist/world.js.map +1 -1
- package/dist/worlds/office-data.d.ts +14 -0
- package/dist/worlds/office-data.d.ts.map +1 -0
- package/dist/worlds/office-data.js +52 -0
- package/dist/worlds/office-data.js.map +1 -0
- package/dist/worlds/office-paint.d.ts +13 -0
- package/dist/worlds/office-paint.d.ts.map +1 -0
- package/dist/worlds/office-paint.js +292 -0
- package/dist/worlds/office-paint.js.map +1 -0
- package/package.json +7 -2
- package/src/draw.ts +352 -272
- package/src/index.ts +4 -1
- package/src/links.ts +76 -0
- package/src/office.ts +142 -21
- package/src/pack.ts +36 -0
- package/src/paint.ts +24 -0
- package/src/runtime.ts +4 -1
- package/src/types.ts +31 -5
- package/src/world.ts +161 -77
- package/src/worlds/office-data.ts +57 -0
- package/src/worlds/office-paint.ts +276 -0
package/src/index.ts
CHANGED
|
@@ -13,9 +13,12 @@ import { createOffice, type OfficeHandle } from "./office.js";
|
|
|
13
13
|
import type { OfficeScene } from "./types.js";
|
|
14
14
|
|
|
15
15
|
export { createOffice } from "./office.js";
|
|
16
|
+
export { registerPainter, type Painter } from "./draw.js";
|
|
17
|
+
export { drawLinks, linkRoutes, type LinkRoute } from "./links.js";
|
|
18
|
+
export { registerWorld, type ItemSpec, type WorldData } from "./pack.js";
|
|
16
19
|
export type { OfficeHandle, OfficeOptions } from "./office.js";
|
|
17
20
|
export * from "./types.js";
|
|
18
|
-
export { agentAt, applyUpdate, createWorld, findPath, step } from "./world.js";
|
|
21
|
+
export { FURNITURE, agentAt, applyLayout, applyUpdate, createWorld, findPath, step } from "./world.js";
|
|
19
22
|
export type { Agent, World } from "./world.js";
|
|
20
23
|
|
|
21
24
|
export interface MountedBlock {
|
package/src/links.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** How the code depends on itself, drawn as lines along the routes agents really walk. */
|
|
2
|
+
|
|
3
|
+
import { TILE, type Ctx } from "./paint.js";
|
|
4
|
+
import { findPath, isWalkable, type World } from "./world.js";
|
|
5
|
+
|
|
6
|
+
export interface LinkRoute { from: string; to: string; points: { x: number; y: number }[] }
|
|
7
|
+
|
|
8
|
+
const COLOURS = ["#38bdf8", "#f472b6", "#fbbf24", "#a78bfa", "#34d399", "#fb7185"];
|
|
9
|
+
|
|
10
|
+
/** A tile someone can stand on, as near the middle of the room as possible. */
|
|
11
|
+
function standingTile(world: World, roomId: string): { x: number; y: number } | undefined {
|
|
12
|
+
const room = world.rooms.find((candidate) => candidate.id === roomId);
|
|
13
|
+
if (!room) return undefined;
|
|
14
|
+
const cx = room.x + (room.w >> 1);
|
|
15
|
+
const cy = room.y + (room.h >> 1);
|
|
16
|
+
let best: { x: number; y: number } | undefined;
|
|
17
|
+
let bestDistance = Infinity;
|
|
18
|
+
for (let y = room.y; y < room.y + room.h; y++) {
|
|
19
|
+
for (let x = room.x; x < room.x + room.w; x++) {
|
|
20
|
+
const distance = Math.abs(x - cx) + Math.abs(y - cy);
|
|
21
|
+
if (distance < bestDistance && isWalkable(world, x, y)) {
|
|
22
|
+
best = { x, y };
|
|
23
|
+
bestDistance = distance;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return best;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Each link's walking route. A link whose rooms aren't on the map, or can't be reached, is left out. */
|
|
31
|
+
export function linkRoutes(world: World): LinkRoute[] {
|
|
32
|
+
const routes: LinkRoute[] = [];
|
|
33
|
+
for (const link of world.links) {
|
|
34
|
+
const start = standingTile(world, link.from);
|
|
35
|
+
const end = standingTile(world, link.to);
|
|
36
|
+
if (!start || !end) continue;
|
|
37
|
+
const path = findPath(world, start, end);
|
|
38
|
+
if (path && path.length > 0) routes.push({ from: link.from, to: link.to, points: [start, ...path] });
|
|
39
|
+
}
|
|
40
|
+
return routes;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Lines that move towards what is depended on. Shared stretches sit side by side. */
|
|
44
|
+
export function drawLinks(c: Ctx, routes: LinkRoute[], time: number): void {
|
|
45
|
+
routes.forEach((route, index) => {
|
|
46
|
+
const colour = COLOURS[index % COLOURS.length] as string;
|
|
47
|
+
const offset = ((index % 5) - 2) * 2;
|
|
48
|
+
const at = (point: { x: number; y: number }): [number, number] => [point.x * TILE + 8 + offset, point.y * TILE + 8 + offset];
|
|
49
|
+
c.save();
|
|
50
|
+
c.lineWidth = 2;
|
|
51
|
+
c.strokeStyle = colour;
|
|
52
|
+
c.globalAlpha = 0.85;
|
|
53
|
+
c.setLineDash([6, 5]);
|
|
54
|
+
c.lineDashOffset = -time * 14;
|
|
55
|
+
c.beginPath();
|
|
56
|
+
route.points.forEach((point, i) => {
|
|
57
|
+
const [x, y] = at(point);
|
|
58
|
+
if (i === 0) c.moveTo(x, y);
|
|
59
|
+
else c.lineTo(x, y);
|
|
60
|
+
});
|
|
61
|
+
c.stroke();
|
|
62
|
+
c.setLineDash([]);
|
|
63
|
+
const first = route.points[0];
|
|
64
|
+
const last = route.points[route.points.length - 1];
|
|
65
|
+
if (first && last) {
|
|
66
|
+
const [sx, sy] = at(first);
|
|
67
|
+
const [ex, ey] = at(last);
|
|
68
|
+
c.fillStyle = colour;
|
|
69
|
+
c.fillRect(sx - 2, sy - 2, 4, 4);
|
|
70
|
+
c.beginPath();
|
|
71
|
+
c.arc(ex, ey, 4, 0, Math.PI * 2);
|
|
72
|
+
c.fill();
|
|
73
|
+
}
|
|
74
|
+
c.restore();
|
|
75
|
+
});
|
|
76
|
+
}
|
package/src/office.ts
CHANGED
|
@@ -1,24 +1,39 @@
|
|
|
1
|
-
/** Puts a world on a canvas: sizing, the frame loop, and
|
|
1
|
+
/** Puts a world on a canvas: sizing, the camera, the frame loop, and input. */
|
|
2
2
|
|
|
3
|
-
import { drawWorld, TILE, type Palette } from "./draw.js";
|
|
4
|
-
import type { OfficeScene } from "./types.js";
|
|
5
|
-
import { agentAt, applyUpdate, createWorld, step, type World } from "./world.js";
|
|
3
|
+
import { daylightAt, drawBuilding, drawWorld, TILE, type Daylight, type Palette } from "./draw.js";
|
|
4
|
+
import type { OfficeLayout, OfficeScene } from "./types.js";
|
|
5
|
+
import { agentAt, applyLayout, applyUpdate, createWorld, step, type World } from "./world.js";
|
|
6
6
|
|
|
7
7
|
export interface OfficeHandle {
|
|
8
8
|
world: World;
|
|
9
9
|
/** Change agents or tasks without restarting anyone. */
|
|
10
10
|
update(scene: Pick<OfficeScene, "agents" | "tasks">): void;
|
|
11
|
+
/** Change the building while everyone is in it. */
|
|
12
|
+
setLayout(layout: OfficeLayout): void;
|
|
13
|
+
/** Show or hide the lines between rooms whose code is linked. */
|
|
14
|
+
setLinksVisible(on: boolean): void;
|
|
11
15
|
/** Pick an agent by id, or pass nothing to clear. */
|
|
12
16
|
select(id?: string): void;
|
|
17
|
+
/** The tile under a pointer event, allowing for the camera. */
|
|
18
|
+
tileAt(event: { clientX: number; clientY: number }): { x: number; y: number };
|
|
19
|
+
/** Draw something over the office each frame, in pixels at 16 a tile. For build mode. */
|
|
20
|
+
setOverlay(draw: ((c: CanvasRenderingContext2D) => void) | undefined): void;
|
|
21
|
+
/** Stop clicks picking agents and dragging the camera, while something else handles them. */
|
|
22
|
+
setInteractive(on: boolean): void;
|
|
23
|
+
/** Zoom by a factor around a point on the canvas, or reset with no arguments. */
|
|
24
|
+
zoom(factor?: number, event?: { clientX: number; clientY: number }): void;
|
|
13
25
|
destroy(): void;
|
|
14
26
|
}
|
|
15
27
|
|
|
16
28
|
export interface OfficeOptions {
|
|
17
29
|
/** Called when the selection changes, by click or by select(). */
|
|
18
30
|
onSelect?: (id: string | undefined) => void;
|
|
31
|
+
/** Wheel to zoom and drag to pan. For a page that is mainly the office: it takes the wheel. */
|
|
32
|
+
camera?: boolean;
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
const SAFE_COLOUR = /^(#[0-9a-f]{3,8}|rgba?\([\d\s.,%/]+\)|hsla?\([\d\s.,%/a-z]+\))$/i;
|
|
36
|
+
const MAX_ZOOM = 4;
|
|
22
37
|
|
|
23
38
|
function readPalette(canvas: HTMLCanvasElement): Palette {
|
|
24
39
|
const styles = getComputedStyle(canvas);
|
|
@@ -40,8 +55,6 @@ export function createOffice(canvas: HTMLCanvasElement, scene: OfficeScene = {},
|
|
|
40
55
|
const context = canvas.getContext("2d");
|
|
41
56
|
if (!context) throw new Error("This browser can't draw to a canvas.");
|
|
42
57
|
const c = context;
|
|
43
|
-
const width = world.cols * TILE;
|
|
44
|
-
const height = world.rows * TILE;
|
|
45
58
|
const palette = readPalette(canvas);
|
|
46
59
|
const still = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
47
60
|
|
|
@@ -52,22 +65,55 @@ export function createOffice(canvas: HTMLCanvasElement, scene: OfficeScene = {},
|
|
|
52
65
|
let frame = 0;
|
|
53
66
|
let visible = true;
|
|
54
67
|
let destroyed = false;
|
|
68
|
+
let interactive = true;
|
|
69
|
+
let overlay: ((c: CanvasRenderingContext2D) => void) | undefined;
|
|
70
|
+
const view = { x: 0, y: 0, zoom: 1 };
|
|
71
|
+
let hour = new Date().getHours();
|
|
72
|
+
let daylight: Daylight = daylightAt(hour);
|
|
73
|
+
let building: HTMLCanvasElement | undefined;
|
|
74
|
+
let buildingDirty = true;
|
|
75
|
+
|
|
76
|
+
const width = (): number => world.cols * TILE;
|
|
77
|
+
const height = (): number => world.rows * TILE;
|
|
78
|
+
|
|
79
|
+
const clampView = (): void => {
|
|
80
|
+
view.zoom = Math.min(MAX_ZOOM, Math.max(1, view.zoom));
|
|
81
|
+
view.x = Math.min(width() - width() / view.zoom, Math.max(0, view.x));
|
|
82
|
+
view.y = Math.min(height() - height() / view.zoom, Math.max(0, view.y));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const refreshBuilding = (): void => {
|
|
86
|
+
if (typeof document === "undefined") return;
|
|
87
|
+
building ??= document.createElement("canvas");
|
|
88
|
+
building.width = width() * scale;
|
|
89
|
+
building.height = height() * scale;
|
|
90
|
+
const layer = building.getContext("2d");
|
|
91
|
+
if (!layer) return;
|
|
92
|
+
layer.setTransform(scale, 0, 0, scale, 0, 0);
|
|
93
|
+
layer.imageSmoothingEnabled = false;
|
|
94
|
+
drawBuilding(layer, world, palette, daylight);
|
|
95
|
+
buildingDirty = false;
|
|
96
|
+
};
|
|
55
97
|
|
|
56
98
|
const paint = (): void => {
|
|
57
|
-
|
|
99
|
+
if (buildingDirty) refreshBuilding();
|
|
100
|
+
const zoomed = scale * view.zoom;
|
|
101
|
+
c.setTransform(zoomed, 0, 0, zoomed, -view.x * zoomed, -view.y * zoomed);
|
|
58
102
|
c.imageSmoothingEnabled = false;
|
|
59
|
-
drawWorld(c, world, time, palette, selected);
|
|
103
|
+
drawWorld(c, world, time, palette, daylight, building, selected);
|
|
104
|
+
overlay?.(c);
|
|
60
105
|
};
|
|
61
106
|
|
|
62
107
|
// The backing store is a whole multiple of the art, so every pixel stays square and text stays sharp.
|
|
63
108
|
const resize = (): void => {
|
|
64
|
-
const shown = canvas.getBoundingClientRect().width || width;
|
|
109
|
+
const shown = canvas.getBoundingClientRect().width || width();
|
|
65
110
|
const ratio = typeof devicePixelRatio === "number" ? devicePixelRatio : 1;
|
|
66
|
-
const next = Math.max(1, Math.ceil((shown * ratio) / width));
|
|
67
|
-
if (next === scale) return;
|
|
111
|
+
const next = Math.max(1, Math.ceil((shown * ratio) / width()));
|
|
112
|
+
if (next === scale && canvas.width === width() * scale && canvas.height === height() * scale) return;
|
|
68
113
|
scale = next;
|
|
69
|
-
canvas.width = width * scale;
|
|
70
|
-
canvas.height = height * scale;
|
|
114
|
+
canvas.width = width() * scale;
|
|
115
|
+
canvas.height = height() * scale;
|
|
116
|
+
buildingDirty = true;
|
|
71
117
|
paint();
|
|
72
118
|
};
|
|
73
119
|
|
|
@@ -82,30 +128,90 @@ export function createOffice(canvas: HTMLCanvasElement, scene: OfficeScene = {},
|
|
|
82
128
|
last = now;
|
|
83
129
|
time += dt;
|
|
84
130
|
step(world, dt);
|
|
131
|
+
const nowHour = new Date().getHours();
|
|
132
|
+
if (nowHour !== hour) {
|
|
133
|
+
hour = nowHour;
|
|
134
|
+
daylight = daylightAt(hour);
|
|
135
|
+
buildingDirty = true;
|
|
136
|
+
}
|
|
85
137
|
paint();
|
|
86
138
|
};
|
|
87
139
|
|
|
88
140
|
const select = (id?: string): void => {
|
|
89
|
-
|
|
90
|
-
selected = next;
|
|
141
|
+
selected = id === selected ? undefined : id;
|
|
91
142
|
options.onSelect?.(selected);
|
|
92
143
|
if (still) paint();
|
|
93
144
|
};
|
|
94
145
|
|
|
95
|
-
const
|
|
146
|
+
const tileAt = (event: { clientX: number; clientY: number }): { x: number; y: number } => {
|
|
96
147
|
const box = canvas.getBoundingClientRect();
|
|
97
|
-
|
|
148
|
+
const px = ((event.clientX - box.left) / box.width) * (width() / view.zoom) + view.x;
|
|
149
|
+
const py = ((event.clientY - box.top) / box.height) * (height() / view.zoom) + view.y;
|
|
150
|
+
return { x: px / TILE, y: py / TILE };
|
|
98
151
|
};
|
|
152
|
+
|
|
153
|
+
let drag: { x: number; y: number; moved: boolean } | undefined;
|
|
99
154
|
const onDown = (event: PointerEvent): void => {
|
|
100
|
-
|
|
101
|
-
|
|
155
|
+
if (!interactive) return;
|
|
156
|
+
drag = { x: event.clientX, y: event.clientY, moved: false };
|
|
102
157
|
};
|
|
103
158
|
const onMove = (event: PointerEvent): void => {
|
|
104
|
-
|
|
105
|
-
|
|
159
|
+
if (!interactive) return;
|
|
160
|
+
if (drag && options.camera && view.zoom > 1) {
|
|
161
|
+
const box = canvas.getBoundingClientRect();
|
|
162
|
+
const dx = event.clientX - drag.x;
|
|
163
|
+
const dy = event.clientY - drag.y;
|
|
164
|
+
if (drag.moved || Math.abs(dx) + Math.abs(dy) > 3) {
|
|
165
|
+
drag.moved = true;
|
|
166
|
+
view.x -= (dx / box.width) * (width() / view.zoom);
|
|
167
|
+
view.y -= (dy / box.height) * (height() / view.zoom);
|
|
168
|
+
drag.x = event.clientX;
|
|
169
|
+
drag.y = event.clientY;
|
|
170
|
+
clampView();
|
|
171
|
+
canvas.style.cursor = "grabbing";
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const { x, y } = tileAt(event);
|
|
176
|
+
canvas.style.cursor = agentAt(world, x, y) ? "pointer" : options.camera && view.zoom > 1 ? "grab" : "default";
|
|
106
177
|
};
|
|
178
|
+
const onUp = (event: PointerEvent): void => {
|
|
179
|
+
if (!interactive) return;
|
|
180
|
+
const wasDrag = drag?.moved;
|
|
181
|
+
drag = undefined;
|
|
182
|
+
if (wasDrag) return;
|
|
183
|
+
const { x, y } = tileAt(event);
|
|
184
|
+
const agent = agentAt(world, x, y);
|
|
185
|
+
if (agent || selected) select(agent?.id ?? selected);
|
|
186
|
+
};
|
|
187
|
+
const zoom = (factor?: number, event?: { clientX: number; clientY: number }): void => {
|
|
188
|
+
if (factor === undefined) {
|
|
189
|
+
view.zoom = 1;
|
|
190
|
+
view.x = 0;
|
|
191
|
+
view.y = 0;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const before = event ? tileAt(event) : { x: view.x / TILE + width() / view.zoom / TILE / 2, y: view.y / TILE + height() / view.zoom / TILE / 2 };
|
|
195
|
+
view.zoom = Math.min(MAX_ZOOM, Math.max(1, view.zoom * factor));
|
|
196
|
+
if (event) {
|
|
197
|
+
const after = tileAt(event);
|
|
198
|
+
view.x += (before.x - after.x) * TILE;
|
|
199
|
+
view.y += (before.y - after.y) * TILE;
|
|
200
|
+
} else {
|
|
201
|
+
view.x = before.x * TILE - width() / view.zoom / 2;
|
|
202
|
+
view.y = before.y * TILE - height() / view.zoom / 2;
|
|
203
|
+
}
|
|
204
|
+
clampView();
|
|
205
|
+
};
|
|
206
|
+
const onWheel = (event: WheelEvent): void => {
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
zoom(event.deltaY < 0 ? 1.2 : 1 / 1.2, event);
|
|
209
|
+
};
|
|
210
|
+
|
|
107
211
|
canvas.addEventListener("pointerdown", onDown);
|
|
108
212
|
canvas.addEventListener("pointermove", onMove);
|
|
213
|
+
canvas.addEventListener("pointerup", onUp);
|
|
214
|
+
if (options.camera) canvas.addEventListener("wheel", onWheel, { passive: false });
|
|
109
215
|
|
|
110
216
|
const resizer = typeof ResizeObserver === "function" ? new ResizeObserver(resize) : undefined;
|
|
111
217
|
resizer?.observe(canvas);
|
|
@@ -124,7 +230,20 @@ export function createOffice(canvas: HTMLCanvasElement, scene: OfficeScene = {},
|
|
|
124
230
|
applyUpdate(world, next);
|
|
125
231
|
if (still) paint();
|
|
126
232
|
},
|
|
233
|
+
setLayout(layout) {
|
|
234
|
+
applyLayout(world, layout);
|
|
235
|
+
buildingDirty = true;
|
|
236
|
+
view.zoom = Math.min(view.zoom, MAX_ZOOM);
|
|
237
|
+
clampView();
|
|
238
|
+
scale = 0;
|
|
239
|
+
resize();
|
|
240
|
+
},
|
|
127
241
|
select,
|
|
242
|
+
tileAt,
|
|
243
|
+
setOverlay(draw) { overlay = draw; },
|
|
244
|
+
setLinksVisible(on) { world.showLinks = on; if (still) paint(); },
|
|
245
|
+
setInteractive(on) { interactive = on; drag = undefined; if (!on) canvas.style.cursor = "crosshair"; },
|
|
246
|
+
zoom,
|
|
128
247
|
destroy() {
|
|
129
248
|
destroyed = true;
|
|
130
249
|
cancelAnimationFrame(frame);
|
|
@@ -132,6 +251,8 @@ export function createOffice(canvas: HTMLCanvasElement, scene: OfficeScene = {},
|
|
|
132
251
|
viewer?.disconnect();
|
|
133
252
|
canvas.removeEventListener("pointerdown", onDown);
|
|
134
253
|
canvas.removeEventListener("pointermove", onMove);
|
|
254
|
+
canvas.removeEventListener("pointerup", onUp);
|
|
255
|
+
canvas.removeEventListener("wheel", onWheel);
|
|
135
256
|
},
|
|
136
257
|
};
|
|
137
258
|
}
|
package/src/pack.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What makes one world different from another. The engine handles walking, seats, time of day
|
|
3
|
+
* and agents, for any world. A world brings this data, plus a painter (see registerPainter).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OfficeLayout } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export interface ItemSpec {
|
|
9
|
+
w: number;
|
|
10
|
+
h: number;
|
|
11
|
+
solid?: boolean;
|
|
12
|
+
wall?: boolean;
|
|
13
|
+
/** Where someone stands to use it, relative to its corner. */
|
|
14
|
+
spot?: { x: number; y: number };
|
|
15
|
+
/** seat: where an agent works. lounge: where it rests. board: where it reviews. */
|
|
16
|
+
use?: "seat" | "lounge" | "board";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WorldData {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
words: {
|
|
23
|
+
/** The progress label over the map. */
|
|
24
|
+
quests: string;
|
|
25
|
+
};
|
|
26
|
+
items: Record<string, ItemSpec>;
|
|
27
|
+
layout: Required<Pick<OfficeLayout, "cols" | "rows" | "rooms" | "doors" | "furniture">>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const worlds = new Map<string, WorldData>();
|
|
31
|
+
|
|
32
|
+
export function registerWorld(data: WorldData): void {
|
|
33
|
+
worlds.set(data.id, data);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const worldData = (id: string | undefined, fallback: WorldData): WorldData => (id && worlds.get(id)) || fallback;
|
package/src/paint.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The small tools every painter uses. */
|
|
2
|
+
|
|
3
|
+
export const TILE = 16;
|
|
4
|
+
|
|
5
|
+
export interface Palette { primary: string; background: string; text: string; accent: string }
|
|
6
|
+
|
|
7
|
+
export type Ctx = CanvasRenderingContext2D;
|
|
8
|
+
|
|
9
|
+
export const LABEL_FONT = "bold 7px ui-monospace, Menlo, Consolas, monospace";
|
|
10
|
+
export const BODY_FONT = "7px ui-monospace, Menlo, Consolas, monospace";
|
|
11
|
+
export const OUTLINE = "#14141f";
|
|
12
|
+
|
|
13
|
+
export const rect = (c: Ctx, color: string, x: number, y: number, w: number, h: number): void => {
|
|
14
|
+
c.fillStyle = color;
|
|
15
|
+
c.fillRect(Math.round(x), Math.round(y), w, h);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function shade(color: string, amount: number): string {
|
|
19
|
+
if (!color.startsWith("#")) return color;
|
|
20
|
+
const hex = color.length === 4 ? `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}` : color;
|
|
21
|
+
const n = parseInt(hex.slice(1, 7), 16);
|
|
22
|
+
const channel = (shift: number): number => Math.max(0, Math.min(255, ((n >> shift) & 255) + amount));
|
|
23
|
+
return `rgb(${channel(16)},${channel(8)},${channel(0)})`;
|
|
24
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** The script tag version: starts every canvas Block on the page by itself. */
|
|
2
|
-
import { mountCanvasBlocks } from "./index.js";
|
|
2
|
+
import { FURNITURE, createOffice, findPath, mountCanvasBlocks } from "./index.js";
|
|
3
|
+
|
|
4
|
+
// For pages that drive an office themselves, such as the blkc dev app.
|
|
5
|
+
(globalThis as unknown as { BLKCCanvas: unknown }).BLKCCanvas = { FURNITURE, createOffice, findPath, mountCanvasBlocks };
|
|
3
6
|
|
|
4
7
|
if (document.readyState === "loading") {
|
|
5
8
|
document.addEventListener("DOMContentLoaded", () => mountCanvasBlocks());
|
package/src/types.ts
CHANGED
|
@@ -2,11 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
export type AgentState = "working" | "thinking" | "blocked" | "reviewing" | "idle" | "done" | "away";
|
|
4
4
|
export type TaskState = "todo" | "doing" | "done";
|
|
5
|
-
export type FloorStyle = "wood" | "carpet" | "tile";
|
|
6
|
-
export type
|
|
7
|
-
| "desk" | "coffee" | "sofa" | "plant" | "board" | "
|
|
5
|
+
export type FloorStyle = "wood" | "carpet" | "tile" | "stone" | "concrete" | "grass" | "hall";
|
|
6
|
+
export type OfficeFurnitureType =
|
|
7
|
+
| "desk" | "coffee" | "sofa" | "plant" | "fern" | "board" | "whiteboard" | "shelf" | "server" | "table" | "rug"
|
|
8
|
+
| "water" | "arcade" | "fridge" | "printer" | "lamp" | "beanbag" | "tv" | "vending" | "clock" | "poster" | "trophy";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
/** An office item, or whatever else the world defines. */
|
|
11
|
+
export type FurnitureType = OfficeFurnitureType | (string & {});
|
|
12
|
+
|
|
13
|
+
export interface OfficeRoom {
|
|
14
|
+
id?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
w: number;
|
|
19
|
+
h: number;
|
|
20
|
+
floor?: FloorStyle;
|
|
21
|
+
/** Project folders this room stands for, so agents go there when they work in them. */
|
|
22
|
+
paths?: string[];
|
|
23
|
+
}
|
|
10
24
|
export interface OfficeDoor { x: number; y: number }
|
|
11
25
|
export interface OfficeFurniture { type: FurnitureType; x: number; y: number }
|
|
12
26
|
export interface OfficeAgent {
|
|
@@ -15,6 +29,8 @@ export interface OfficeAgent {
|
|
|
15
29
|
role?: string;
|
|
16
30
|
state?: AgentState;
|
|
17
31
|
task?: string;
|
|
32
|
+
/** The id of the room this agent is working in. */
|
|
33
|
+
area?: string;
|
|
18
34
|
/** Character colours. Anything left out is picked from the id. */
|
|
19
35
|
hair?: string;
|
|
20
36
|
skin?: string;
|
|
@@ -22,12 +38,22 @@ export interface OfficeAgent {
|
|
|
22
38
|
}
|
|
23
39
|
export interface OfficeTask { title: string; state?: TaskState; agent?: string }
|
|
24
40
|
|
|
25
|
-
|
|
41
|
+
/** `from` depends on `to`. */
|
|
42
|
+
export interface OfficeLink { from: string; to: string }
|
|
43
|
+
|
|
44
|
+
export interface OfficeLayout {
|
|
26
45
|
cols?: number;
|
|
27
46
|
rows?: number;
|
|
28
47
|
rooms?: OfficeRoom[];
|
|
29
48
|
doors?: OfficeDoor[];
|
|
30
49
|
furniture?: OfficeFurniture[];
|
|
50
|
+
/** How the rooms' code depends on each other. Drawn as lines between them. */
|
|
51
|
+
links?: OfficeLink[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface OfficeScene extends OfficeLayout {
|
|
55
|
+
/** Which world to draw. The office if left out or unknown. */
|
|
56
|
+
world?: string;
|
|
31
57
|
agents?: OfficeAgent[];
|
|
32
58
|
tasks?: OfficeTask[];
|
|
33
59
|
}
|