dsh-mutipleagent 0.0.0-stage → 0.4.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/CHANGELOG.md +214 -0
- package/CONTRIBUTING.md +49 -0
- package/LICENSE +21 -0
- package/README.en.md +125 -0
- package/README.md +302 -2
- package/SECURITY.md +27 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/assets/agents/MAGPIE-LICENSE +21 -0
- package/assets/agents/README.md +47 -0
- package/assets/agents/antigravity.png +0 -0
- package/assets/agents/claude.png +0 -0
- package/assets/agents/codex.png +0 -0
- package/assets/agents/copilot.svg +1 -0
- package/assets/agents/dsh.svg +1 -0
- package/assets/agents/gemini.png +0 -0
- package/assets/agents/grok.svg +1 -0
- package/assets/agents/hermes.svg +1 -0
- package/assets/agents/kimi.svg +1 -0
- package/assets/agents/omp.svg +7 -0
- package/assets/agents/opencode.svg +1 -0
- package/assets/agents/pi.svg +1 -0
- package/assets/kenney/LICENSE-furniture-kit.txt +23 -0
- package/assets/kenney/LICENSE-mini-characters.txt +28 -0
- package/assets/kenney/characters/Textures/colormap.png +0 -0
- package/assets/kenney/characters/character-female-a.glb +0 -0
- package/assets/kenney/characters/character-female-b.glb +0 -0
- package/assets/kenney/characters/character-female-c.glb +0 -0
- package/assets/kenney/characters/character-female-d.glb +0 -0
- package/assets/kenney/characters/character-female-e.glb +0 -0
- package/assets/kenney/characters/character-female-f.glb +0 -0
- package/assets/kenney/characters/character-male-a.glb +0 -0
- package/assets/kenney/characters/character-male-b.glb +0 -0
- package/assets/kenney/characters/character-male-c.glb +0 -0
- package/assets/kenney/characters/character-male-d.glb +0 -0
- package/assets/kenney/characters/character-male-e.glb +0 -0
- package/assets/kenney/characters/character-male-f.glb +0 -0
- package/assets/kenney/furniture/benchCushion.glb +0 -0
- package/assets/kenney/furniture/bookcaseOpen.glb +0 -0
- package/assets/kenney/furniture/books.glb +0 -0
- package/assets/kenney/furniture/cabinetTelevision.glb +0 -0
- package/assets/kenney/furniture/cardboardBoxClosed.glb +0 -0
- package/assets/kenney/furniture/chairCushion.glb +0 -0
- package/assets/kenney/furniture/chairDesk.glb +0 -0
- package/assets/kenney/furniture/chairModernCushion.glb +0 -0
- package/assets/kenney/furniture/coatRackStanding.glb +0 -0
- package/assets/kenney/furniture/computerKeyboard.glb +0 -0
- package/assets/kenney/furniture/computerMouse.glb +0 -0
- package/assets/kenney/furniture/computerScreen.glb +0 -0
- package/assets/kenney/furniture/desk.glb +0 -0
- package/assets/kenney/furniture/doorway.glb +0 -0
- package/assets/kenney/furniture/kitchenBar.glb +0 -0
- package/assets/kenney/furniture/kitchenCabinet.glb +0 -0
- package/assets/kenney/furniture/kitchenCoffeeMachine.glb +0 -0
- package/assets/kenney/furniture/kitchenFridgeLarge.glb +0 -0
- package/assets/kenney/furniture/kitchenMicrowave.glb +0 -0
- package/assets/kenney/furniture/kitchenSink.glb +0 -0
- package/assets/kenney/furniture/lampRoundFloor.glb +0 -0
- package/assets/kenney/furniture/lampSquareTable.glb +0 -0
- package/assets/kenney/furniture/laptop.glb +0 -0
- package/assets/kenney/furniture/loungeChairRelax.glb +0 -0
- package/assets/kenney/furniture/loungeSofa.glb +0 -0
- package/assets/kenney/furniture/plantSmall1.glb +0 -0
- package/assets/kenney/furniture/plantSmall2.glb +0 -0
- package/assets/kenney/furniture/plantSmall3.glb +0 -0
- package/assets/kenney/furniture/pottedPlant.glb +0 -0
- package/assets/kenney/furniture/radio.glb +0 -0
- package/assets/kenney/furniture/rugRectangle.glb +0 -0
- package/assets/kenney/furniture/rugRound.glb +0 -0
- package/assets/kenney/furniture/sideTable.glb +0 -0
- package/assets/kenney/furniture/speaker.glb +0 -0
- package/assets/kenney/furniture/stoolBar.glb +0 -0
- package/assets/kenney/furniture/table.glb +0 -0
- package/assets/kenney/furniture/tableCoffee.glb +0 -0
- package/assets/kenney/furniture/tableRound.glb +0 -0
- package/assets/kenney/furniture/televisionModern.glb +0 -0
- package/assets/kenney/furniture/toaster.glb +0 -0
- package/assets/kenney/furniture/trashcan.glb +0 -0
- package/assets/kenney/furniture/wallWindow.glb +0 -0
- package/assets/logo.svg +12 -0
- package/assets/scene-icons/arcade.png +0 -0
- package/assets/scene-icons/cafe.png +0 -0
- package/assets/scene-icons/castle.png +0 -0
- package/assets/scene-icons/christmas.png +0 -0
- package/assets/scene-icons/graveyard.png +0 -0
- package/assets/scene-icons/map.png +0 -0
- package/assets/scene-icons/office.png +0 -0
- package/assets/scene-icons/pirate.png +0 -0
- package/assets/scene-icons/station.png +0 -0
- package/assets/scenes/arcade.jpg +0 -0
- package/assets/scenes/cafe.jpg +0 -0
- package/assets/scenes/castle.jpg +0 -0
- package/assets/scenes/christmas.jpg +0 -0
- package/assets/scenes/graveyard.jpg +0 -0
- package/assets/scenes/office.jpg +0 -0
- package/assets/scenes/pirate.jpg +0 -0
- package/assets/scenes/station.jpg +0 -0
- package/client.js +144 -0
- package/cordis.patch.yml +3 -0
- package/harness.cjs +49 -0
- package/package.json +89 -4
- package/public/app.js +1 -0
- package/public/chunks/chunk-MU3LEDFF.js +4305 -0
- package/public/demo-icons/arrow-counterclockwise.svg +4 -0
- package/public/demo-icons/arrow-right.svg +3 -0
- package/public/demo-icons/bell.svg +3 -0
- package/public/demo-icons/box-seam-fill.svg +3 -0
- package/public/demo-icons/chat-left-text-fill.svg +3 -0
- package/public/demo-icons/check-lg.svg +3 -0
- package/public/demo-icons/chevron-down.svg +3 -0
- package/public/demo-icons/cpu-fill.svg +4 -0
- package/public/demo-icons/crosshair.svg +3 -0
- package/public/demo-icons/diagram-3-fill.svg +3 -0
- package/public/demo-icons/file-earmark-code-fill.svg +3 -0
- package/public/demo-icons/fullscreen.svg +3 -0
- package/public/demo-icons/pause-fill.svg +3 -0
- package/public/demo-icons/people-fill.svg +3 -0
- package/public/demo-icons/pie-chart-fill.svg +3 -0
- package/public/demo-icons/play-fill.svg +3 -0
- package/public/demo-icons/search.svg +3 -0
- package/public/demo-icons/shield-check.svg +4 -0
- package/public/demo-icons/stack.svg +4 -0
- package/public/demo-icons/x-lg.svg +3 -0
- package/public/demo.css +225 -0
- package/public/demo.html +67 -0
- package/public/demo.js +7 -0
- package/public/index.html +59 -0
- package/public/style.css +15 -0
- package/public/vendor/office.js +1631 -0
- package/src/codex.js +674 -0
- package/src/demo.js +103 -0
- package/src/harness-watcher.js +584 -0
- package/src/server.js +239 -0
- package/vendor/licenses/BOOTSTRAP-ICONS-LICENSE.txt +21 -0
- package/vendor/licenses/ESBUILD-LICENSE.txt +21 -0
- package/vendor/licenses/MESHOPTIMIZER-LICENSE.txt +22 -0
- package/vendor/licenses/PLAYWRIGHT-LICENSE.txt +202 -0
- package/vendor/licenses/PLAYWRIGHT-NOTICE.txt +5 -0
- package/vendor/licenses/THREE-LICENSE.txt +21 -0
|
@@ -0,0 +1,1631 @@
|
|
|
1
|
+
// Isometric office: agents that are working sit at cubicle desks; a few that
|
|
2
|
+
// are off duty wander between the pantry, lounge, meeting room and ping-pong
|
|
3
|
+
// table. Plain canvas 2D with local provider logos, no dependencies.
|
|
4
|
+
(() => {
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const TW = 64; // tile width px
|
|
8
|
+
const TH = 32; // tile height px; one z unit is TH px too
|
|
9
|
+
const WALL_H = 2.4;
|
|
10
|
+
const WORKING = new Set(['thinking', 'tool', 'typing', 'waiting', 'error']);
|
|
11
|
+
const MAX_WANDERERS = 7;
|
|
12
|
+
const SPEED = 2.1; // tiles / second
|
|
13
|
+
|
|
14
|
+
const SKIN = ['#f3cfb0', '#e5b38f', '#c98d66', '#9a6646', '#6e4630', '#f7dcc7'];
|
|
15
|
+
const HAIR = ['#2a2019', '#4b3020', '#8a4b2a', '#c99a5b', '#1c1c1c', '#8c8c8c', '#a33a2c', '#3d3550'];
|
|
16
|
+
const SHIRT = {
|
|
17
|
+
claude: ['#c96442', '#d9825f', '#b4553a', '#e09a72'],
|
|
18
|
+
codex: ['#10a37f', '#2bb38f', '#0d8a6b', '#4cc3a0'],
|
|
19
|
+
gemini: ['#4f7cf0', '#6b8ff5', '#3d66d6', '#8aa6f8'],
|
|
20
|
+
copilot: ['#8f5cf5', '#a06af7', '#7a47d5', '#c5a4ff'],
|
|
21
|
+
omp: ['#a96926', '#c78942', '#8d531e', '#dca45c'],
|
|
22
|
+
pi: ['#4f8eb3', '#6ba5c7', '#3d7596', '#9cc7df'],
|
|
23
|
+
antigravity: ['#0ea5e9', '#38bdf8', '#0284c7', '#7dd3fc'],
|
|
24
|
+
opencode: ['#2f6fd0', '#4a86e0', '#2559b0', '#7aa5ee'],
|
|
25
|
+
hermes: ['#b4622d', '#d07c44', '#96501f', '#e2a374'],
|
|
26
|
+
};
|
|
27
|
+
// Official product icons, shared by both renderers and the page UI (OMP: a neutral monogram).
|
|
28
|
+
// Load once per page from the same origin so video capture stays readable.
|
|
29
|
+
// Initials remain a fallback if an image cannot be loaded.
|
|
30
|
+
const AGENT_BADGES = {
|
|
31
|
+
claude: { label: 'Claude', short: 'Cl', color: '#c96442', logo: './assets/agents/claude.png' },
|
|
32
|
+
codex: { label: 'Codex', short: 'Cx', color: '#10a37f', logo: './assets/agents/codex.png' },
|
|
33
|
+
gemini: { label: 'Gemini', short: 'G', color: '#4f7cf0', logo: './assets/agents/gemini.png' },
|
|
34
|
+
copilot: { label: 'Copilot', short: 'Co', color: '#8f5cf5', logo: './assets/agents/copilot.svg', mono: true },
|
|
35
|
+
kimi: { label: 'Kimi Code', short: 'Ki', logo: './assets/agents/kimi.svg', mono: true },
|
|
36
|
+
dsh: { label: 'DSH', short: 'Ds', logo: './assets/agents/dsh.svg' },
|
|
37
|
+
grok: { label: 'Grok Build', short: 'Gr', logo: './assets/agents/grok.svg', mono: true },
|
|
38
|
+
omp: { label: 'OMP', short: 'OMP', color: '#a96926', logo: './assets/agents/omp.svg' },
|
|
39
|
+
pi: { label: 'Pi', short: 'Pi', color: '#4f8eb3', logo: './assets/agents/pi.svg' },
|
|
40
|
+
antigravity: { label: 'Antigravity', short: 'Ag', color: '#0ea5e9', logo: './assets/agents/antigravity.png' },
|
|
41
|
+
opencode: { label: 'OpenCode', short: 'Oc', color: '#2f6fd0', logo: './assets/agents/opencode.svg', mono: true },
|
|
42
|
+
hermes: { label: 'Hermes', short: 'He', color: '#b4622d', logo: './assets/agents/hermes.svg', mono: true },
|
|
43
|
+
};
|
|
44
|
+
for (const badge of Object.values(AGENT_BADGES)) {
|
|
45
|
+
badge.image = new Image();
|
|
46
|
+
badge.image.src = badge.logo;
|
|
47
|
+
}
|
|
48
|
+
const PANTS = ['#34405a', '#2c2c34', '#4a3f35', '#3b4a3f'];
|
|
49
|
+
const PANEL = ['#7d93c4', '#d98b6a', '#7fb08a', '#c9a24c'];
|
|
50
|
+
|
|
51
|
+
const STATUS_COLOR = {
|
|
52
|
+
thinking: '#8b6be6', tool: '#3b8ee0', typing: '#22a874', waiting: '#e09412',
|
|
53
|
+
done: '#5f9a4c', error: '#de4b40', interrupted: '#9b9a94', idle: '#9b9a94',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const ACT_EMOJI = {
|
|
57
|
+
coffee: '☕', water: '💧', fridge: '🍎', sink: '🫧', stool: '💬', couch: '💬', beanbag: '🎧',
|
|
58
|
+
window: '☁️', read: '📖', whiteboard: '✏️', meeting: '💬', arcade: '🎮', pingpong: '🏓', plant: '🌱',
|
|
59
|
+
vending: '🥤', foosball: '⚽', phone: '📞',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function hash(str) {
|
|
63
|
+
let h = 2166136261;
|
|
64
|
+
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
65
|
+
return h >>> 0;
|
|
66
|
+
}
|
|
67
|
+
const pick = (arr, h, salt) => arr[(Math.imul(h ^ Math.imul(salt, 2654435761), 2246822519) >>> 0) % arr.length];
|
|
68
|
+
const rand = (a, b) => a + Math.random() * (b - a);
|
|
69
|
+
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
70
|
+
|
|
71
|
+
function palette() {
|
|
72
|
+
const dark = matchMedia('(prefers-color-scheme: dark)').matches;
|
|
73
|
+
return dark ? {
|
|
74
|
+
dark: true,
|
|
75
|
+
wood1: '#4a3f35', wood2: '#453a31', woodLine: 'rgba(0,0,0,.22)',
|
|
76
|
+
tileA: '#3d4044', tileB: '#35383c', carpet: '#33404d', carpetLine: 'rgba(255,255,255,.04)',
|
|
77
|
+
wallL: '#4a4640', wallR: '#3f3b36', wallTop: '#5a554e', trim: '#2b2926',
|
|
78
|
+
deskTop: '#8a6a4c', deskL: '#6e533b', deskR: '#5c4431',
|
|
79
|
+
monitor: '#18181a', screenOff: '#26282c', chair: '#2d2d32',
|
|
80
|
+
rug: '#47544f', rugEdge: '#3a4541', couch: '#5b6b8c', couchD: '#4a587a',
|
|
81
|
+
counter: '#d9d4c9', counterL: '#a9a397', counterR: '#958f84', metal: '#8d939a',
|
|
82
|
+
glass: 'rgba(160,200,230,.10)', glassEdge: 'rgba(190,220,240,.35)', mat: '#2f4a3a',
|
|
83
|
+
text: '#ecebe6', tagBg: 'rgba(20,20,19,.78)', bubble: '#f4f3ee', bubbleInk: '#1c1c1a',
|
|
84
|
+
plant: '#4f8a52', pot: '#9a5b3c', shadow: 'rgba(0,0,0,.28)', book: ['#c96442', '#3b8ee0', '#5f9a4c', '#c9a24c', '#8b6be6'],
|
|
85
|
+
} : {
|
|
86
|
+
dark: false,
|
|
87
|
+
wood1: '#e6d3b6', wood2: '#dfcaab', woodLine: 'rgba(120,90,50,.12)',
|
|
88
|
+
tileA: '#f4f4f1', tileB: '#e6e7e3', carpet: '#c9d3dc', carpetLine: 'rgba(0,0,0,.035)',
|
|
89
|
+
wallL: '#f4efe6', wallR: '#e8e1d4', wallTop: '#fbf8f2', trim: '#cfc5b3',
|
|
90
|
+
deskTop: '#c89f72', deskL: '#a8825a', deskR: '#946f4b',
|
|
91
|
+
monitor: '#2a2a2e', screenOff: '#3a3d44', chair: '#4a4a52',
|
|
92
|
+
rug: '#c4d4cb', rugEdge: '#a9bcb1', couch: '#7d93c4', couchD: '#6a80b0',
|
|
93
|
+
counter: '#fbfaf7', counterL: '#d9d4c9', counterR: '#c9c3b6', metal: '#b5bcc4',
|
|
94
|
+
glass: 'rgba(170,210,235,.16)', glassEdge: 'rgba(120,160,190,.55)', mat: '#bfd8c4',
|
|
95
|
+
text: '#1c1c1a', tagBg: 'rgba(255,255,255,.9)', bubble: '#ffffff', bubbleInk: '#1c1c1a',
|
|
96
|
+
plant: '#5a9b5d', pot: '#b86f4b', shadow: 'rgba(60,40,20,.18)', book: ['#c96442', '#3b8ee0', '#5f9a4c', '#c9a24c', '#8b6be6'],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function skyColor(d = new Date()) {
|
|
101
|
+
const h = d.getHours() + d.getMinutes() / 60;
|
|
102
|
+
if (h >= 7 && h < 17) return { sky: '#a9d4f2', night: false };
|
|
103
|
+
if (h >= 5 && h < 7) return { sky: '#f3c9a8', night: false };
|
|
104
|
+
if (h >= 17 && h < 19) return { sky: '#f0a679', night: false };
|
|
105
|
+
return { sky: '#1d2747', night: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------- A* grid
|
|
109
|
+
|
|
110
|
+
class Grid {
|
|
111
|
+
constructor(W, D, res = 4) {
|
|
112
|
+
this.res = res;
|
|
113
|
+
this.cw = Math.ceil(W * res);
|
|
114
|
+
this.ch = Math.ceil(D * res);
|
|
115
|
+
this.cost = new Uint8Array(this.cw * this.ch);
|
|
116
|
+
}
|
|
117
|
+
block(x, y, w, d, pad = 0.12) {
|
|
118
|
+
const R = this.res;
|
|
119
|
+
const i0 = clamp(Math.floor((x - pad) * R), 0, this.cw - 1);
|
|
120
|
+
const i1 = clamp(Math.ceil((x + w + pad) * R) - 1, 0, this.cw - 1);
|
|
121
|
+
const j0 = clamp(Math.floor((y - pad) * R), 0, this.ch - 1);
|
|
122
|
+
const j1 = clamp(Math.ceil((y + d + pad) * R) - 1, 0, this.ch - 1);
|
|
123
|
+
for (let j = j0; j <= j1; j++) for (let i = i0; i <= i1; i++) this.cost[j * this.cw + i] = 1;
|
|
124
|
+
}
|
|
125
|
+
blocked(x, y) {
|
|
126
|
+
const i = Math.floor(x * this.res), j = Math.floor(y * this.res);
|
|
127
|
+
if (i < 0 || j < 0 || i >= this.cw || j >= this.ch) return true;
|
|
128
|
+
return !!this.cost[j * this.cw + i];
|
|
129
|
+
}
|
|
130
|
+
path(ax, ay, bx, by) {
|
|
131
|
+
const R = this.res, cw = this.cw, ch = this.ch;
|
|
132
|
+
const si = clamp(Math.floor(ax * R), 0, cw - 1), sj = clamp(Math.floor(ay * R), 0, ch - 1);
|
|
133
|
+
const gi = clamp(Math.floor(bx * R), 0, cw - 1), gj = clamp(Math.floor(by * R), 0, ch - 1);
|
|
134
|
+
const N = cw * ch;
|
|
135
|
+
const g = new Float32Array(N).fill(Infinity);
|
|
136
|
+
const came = new Int32Array(N).fill(-1);
|
|
137
|
+
const closed = new Uint8Array(N);
|
|
138
|
+
const heap = [];
|
|
139
|
+
const push = (f, n) => {
|
|
140
|
+
heap.push([f, n]);
|
|
141
|
+
let k = heap.length - 1;
|
|
142
|
+
while (k > 0) { const p = (k - 1) >> 1; if (heap[p][0] <= heap[k][0]) break; [heap[p], heap[k]] = [heap[k], heap[p]]; k = p; }
|
|
143
|
+
};
|
|
144
|
+
const pop = () => {
|
|
145
|
+
const top = heap[0], last = heap.pop();
|
|
146
|
+
if (heap.length) {
|
|
147
|
+
heap[0] = last;
|
|
148
|
+
let k = 0;
|
|
149
|
+
for (;;) {
|
|
150
|
+
const l = 2 * k + 1, r = l + 1;
|
|
151
|
+
let m = k;
|
|
152
|
+
if (l < heap.length && heap[l][0] < heap[m][0]) m = l;
|
|
153
|
+
if (r < heap.length && heap[r][0] < heap[m][0]) m = r;
|
|
154
|
+
if (m === k) break;
|
|
155
|
+
[heap[m], heap[k]] = [heap[k], heap[m]]; k = m;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return top;
|
|
159
|
+
};
|
|
160
|
+
const hfn = (i, j) => { const dx = Math.abs(i - gi), dy = Math.abs(j - gj); return Math.max(dx, dy) + 0.41 * Math.min(dx, dy); };
|
|
161
|
+
// Blocked cells are expensive, not impossible, so a path always exists
|
|
162
|
+
// (someone standing right against furniture can still step away).
|
|
163
|
+
const stepCost = (n) => (this.cost[n] ? 25 : 1);
|
|
164
|
+
const start = sj * cw + si, goal = gj * cw + gi;
|
|
165
|
+
g[start] = 0;
|
|
166
|
+
push(hfn(si, sj), start);
|
|
167
|
+
const dirs = [[1, 0, 1], [-1, 0, 1], [0, 1, 1], [0, -1, 1], [1, 1, 1.41], [1, -1, 1.41], [-1, 1, 1.41], [-1, -1, 1.41]];
|
|
168
|
+
while (heap.length) {
|
|
169
|
+
const [, n] = pop();
|
|
170
|
+
if (n === goal) break;
|
|
171
|
+
if (closed[n]) continue;
|
|
172
|
+
closed[n] = 1;
|
|
173
|
+
const i = n % cw, j = (n / cw) | 0;
|
|
174
|
+
for (const [di, dj, c] of dirs) {
|
|
175
|
+
const ni = i + di, nj = j + dj;
|
|
176
|
+
if (ni < 0 || nj < 0 || ni >= cw || nj >= ch) continue;
|
|
177
|
+
if (di && dj && (this.cost[j * cw + ni] || this.cost[nj * cw + i])) continue;
|
|
178
|
+
const nn = nj * cw + ni;
|
|
179
|
+
const ng = g[n] + c * stepCost(nn);
|
|
180
|
+
if (ng < g[nn]) { g[nn] = ng; came[nn] = n; push(ng + hfn(ni, nj), nn); }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const cells = [];
|
|
184
|
+
for (let n = goal; n !== -1 && n !== start; n = came[n]) cells.push(n);
|
|
185
|
+
cells.reverse();
|
|
186
|
+
const pts = [];
|
|
187
|
+
let lastDir = null;
|
|
188
|
+
for (let k = 0; k < cells.length; k++) {
|
|
189
|
+
const n = cells[k];
|
|
190
|
+
const dir = n - (k ? cells[k - 1] : start);
|
|
191
|
+
const pt = { x: ((n % cw) + 0.5) / R, y: (((n / cw) | 0) + 0.5) / R };
|
|
192
|
+
if (dir === lastDir && pts.length) pts[pts.length - 1] = pt;
|
|
193
|
+
else pts.push(pt);
|
|
194
|
+
lastDir = dir;
|
|
195
|
+
}
|
|
196
|
+
if (pts.length) pts[pts.length - 1] = { x: bx, y: by };
|
|
197
|
+
else pts.push({ x: bx, y: by });
|
|
198
|
+
return pts;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------- office
|
|
203
|
+
|
|
204
|
+
class Office {
|
|
205
|
+
constructor(root, { onHover, label, onFrame, showAgent = true, compact = false } = {}) {
|
|
206
|
+
this.root = root;
|
|
207
|
+
this.onHover = onHover || (() => {});
|
|
208
|
+
this.label = label || ((s) => s.project || s.agent);
|
|
209
|
+
this.onFrame = onFrame;
|
|
210
|
+
this.showAgent = showAgent;
|
|
211
|
+
this.compact = compact;
|
|
212
|
+
this.offDutyLabel = 'went home';
|
|
213
|
+
this.canvas = document.createElement('canvas');
|
|
214
|
+
this.canvas.className = 'office-canvas';
|
|
215
|
+
root.appendChild(this.canvas);
|
|
216
|
+
this.ctx = this.canvas.getContext('2d');
|
|
217
|
+
this.people = new Map();
|
|
218
|
+
this.deskOf = new Map();
|
|
219
|
+
this.offDuty = 0;
|
|
220
|
+
this.first = true;
|
|
221
|
+
this.visible = true;
|
|
222
|
+
this.hit = [];
|
|
223
|
+
this.hoverKey = null;
|
|
224
|
+
this.t = performance.now();
|
|
225
|
+
this.pal = palette();
|
|
226
|
+
this.plan = this.buildPlan(0);
|
|
227
|
+
this.cat = { x: this.plan.W - 3, y: this.plan.D - 2, path: [], until: 0, face: 1 };
|
|
228
|
+
this.theme = matchMedia('(prefers-color-scheme: dark)');
|
|
229
|
+
this.onTheme = () => (this.pal = palette());
|
|
230
|
+
this.theme.addEventListener('change', this.onTheme);
|
|
231
|
+
|
|
232
|
+
this.ro = new ResizeObserver(() => this.resize());
|
|
233
|
+
this.ro.observe(root);
|
|
234
|
+
this.canvas.addEventListener('mousemove', (e) => this.pointer(e));
|
|
235
|
+
this.canvas.addEventListener('click', (e) => this.pointer(e, true));
|
|
236
|
+
this.canvas.addEventListener('mouseleave', () => { this.hoverKey = null; this.onHover(null); });
|
|
237
|
+
this.resize();
|
|
238
|
+
this.last = performance.now();
|
|
239
|
+
// Resolves true once a few frames have been drawn with data in place,
|
|
240
|
+
// false if the renderer gave up (3D load failure).
|
|
241
|
+
this.ready = new Promise((ok) => (this.markReady = ok));
|
|
242
|
+
this.framesShown = 0;
|
|
243
|
+
const loop = (t) => { if (this.dead) return; this.frame(t); this.raf = requestAnimationFrame(loop); };
|
|
244
|
+
this.raf = requestAnimationFrame(loop);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
destroy() {
|
|
248
|
+
this.dead = true;
|
|
249
|
+
cancelAnimationFrame(this.raf);
|
|
250
|
+
this.ro.disconnect();
|
|
251
|
+
this.theme.removeEventListener('change', this.onTheme);
|
|
252
|
+
this.canvas.remove();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ------------------------------------------------------------ floor plan
|
|
256
|
+
|
|
257
|
+
buildPlan(nWorking) {
|
|
258
|
+
const pal = this.pal;
|
|
259
|
+
const pods = Math.max(2, Math.ceil((nWorking + 2) / 4));
|
|
260
|
+
const podCols = pods <= 4 ? 2 : 3;
|
|
261
|
+
const podRows = Math.ceil(pods / podCols);
|
|
262
|
+
const OW = 1.3 + (podCols - 1) * 4.2 + 3.9; // open office width
|
|
263
|
+
const OD = podRows * 4.7 + 0.8; // open office depth (pods)
|
|
264
|
+
const D = Math.max(OD + 3.6, 14.5);
|
|
265
|
+
const RW = 7.2; // right column width
|
|
266
|
+
const W = OW + RW;
|
|
267
|
+
const MR = 5.2; // meeting room depth
|
|
268
|
+
const PY = 9.9; // pantry / lounge boundary
|
|
269
|
+
const grid = new Grid(W, D);
|
|
270
|
+
const items = [];
|
|
271
|
+
const pois = [];
|
|
272
|
+
const desks = [];
|
|
273
|
+
const add = (depth, draw) => items.push({ depth, draw });
|
|
274
|
+
// Semantic list of what is where, for renderers that don't use the 2D
|
|
275
|
+
// draw closures (the 3D view).
|
|
276
|
+
const props = [];
|
|
277
|
+
const prop = (t, o) => props.push({ t, ...o });
|
|
278
|
+
const solid = (x, y, w, d, pad) => grid.block(x, y, w, d, pad);
|
|
279
|
+
const poi = (x, y, act, face, opt = {}) => pois.push({ x, y, act, face, sit: 0, ...opt });
|
|
280
|
+
|
|
281
|
+
// Long boxes are split into ~1 tile pieces so depth sorting against
|
|
282
|
+
// people stays right along their length. `extra(x0, x1)` draws things on
|
|
283
|
+
// the front face of each piece (books on a shelf, etc).
|
|
284
|
+
const longBox = (x, y, z, w, d, h, top, left, right, opts = {}) => {
|
|
285
|
+
const n = Math.max(1, Math.ceil(Math.max(w, d) / 0.9));
|
|
286
|
+
for (let k = 0; k < n; k++) {
|
|
287
|
+
const sx = w >= d ? x + (w / n) * k : x, sy = w >= d ? y : y + (d / n) * k;
|
|
288
|
+
const sw = w >= d ? w / n : w, sd = w >= d ? d : d / n;
|
|
289
|
+
add(sx + sw / 2 + sy + sd / 2 + (opts.bias || 0), () => {
|
|
290
|
+
this.box(sx, sy, z, sw, sd, h, top, left, right);
|
|
291
|
+
if (opts.extra) opts.extra(sx, sx + sw);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// ---- cubicle pods: 4 desks back to back with a divider
|
|
297
|
+
for (let p = 0; p < pods; p++) {
|
|
298
|
+
const c = p % podCols, r = Math.floor(p / podCols);
|
|
299
|
+
const px = 1.3 + c * 4.2, py = 2.0 + r * 4.7;
|
|
300
|
+
const panel = PANEL[p % PANEL.length];
|
|
301
|
+
for (let k = 0; k < 2; k++) {
|
|
302
|
+
const x = px + k * 1.5;
|
|
303
|
+
desks.push({ x, y: py, side: 'n', seat: { x: x + 0.7, y: py - 0.45 } });
|
|
304
|
+
desks.push({ x, y: py + 0.92, side: 's', seat: { x: x + 0.7, y: py + 0.92 + 1.25 } });
|
|
305
|
+
solid(x, py, 1.4, 0.8, 0.08);
|
|
306
|
+
solid(x, py + 0.92, 1.4, 0.8, 0.08);
|
|
307
|
+
}
|
|
308
|
+
longBox(px - 0.1, py + 0.82, 0, 3.15, 0.08, 1.15, this.shade(panel, 25), panel, this.shade(panel, -15), { bias: 0.05 });
|
|
309
|
+
prop('panel', { x: px - 0.1, y: py + 0.82, w: 3.15, d: 0.08, h: 1.15, color: panel });
|
|
310
|
+
prop('panel', { x: px - 0.18, y: py - 0.05, w: 0.08, d: 1.82, h: 1.15, color: panel });
|
|
311
|
+
add(px - 0.1 + py + 0.86, () => this.box(px - 0.18, py - 0.05, 0, 0.08, 1.82, 1.15, this.shade(panel, 25), panel, this.shade(panel, -15)));
|
|
312
|
+
solid(px - 0.18, py + 0.8, 3.25, 0.12, 0.05);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---- open office extras
|
|
316
|
+
const rx = OW - 1.15;
|
|
317
|
+
add(rx + 0.35 + 0.5, () => this.serverRack(rx, 0.2));
|
|
318
|
+
prop('rack', { x: rx, y: 0.2 });
|
|
319
|
+
prop('printer', { x: OW - 2.3, y: 0.25 });
|
|
320
|
+
solid(rx, 0.2, 0.7, 0.6);
|
|
321
|
+
add(OW - 2.3 + 0.3 + 0.5, () => this.printer(OW - 2.3, 0.25));
|
|
322
|
+
solid(OW - 2.3, 0.25, 0.75, 0.55);
|
|
323
|
+
for (const [x, y] of [[0.45, 0.45], [0.45, OD - 0.2]]) { add(x + y, () => this.plant(x, y)); prop('plant', { x, y }); solid(x - 0.2, y - 0.2, 0.4, 0.4); }
|
|
324
|
+
const windows = [];
|
|
325
|
+
for (let x = 1.0; x < OW - 2.6; x += 2.4) {
|
|
326
|
+
windows.push(x);
|
|
327
|
+
poi(x + 0.75, 0.75, 'window', 'back');
|
|
328
|
+
}
|
|
329
|
+
poi(0.95, OD - 0.2, 'plant', 'back');
|
|
330
|
+
|
|
331
|
+
// ---- ping-pong corner
|
|
332
|
+
const tx = 2.4, ty = OD + 1.0;
|
|
333
|
+
add(tx + 1.3 + ty + 0.7, () => this.pingpong(tx, ty));
|
|
334
|
+
prop('pingpong', { x: tx, y: ty });
|
|
335
|
+
solid(tx, ty, 2.6, 1.4, 0.05);
|
|
336
|
+
poi(tx - 0.55, ty + 0.7, 'pingpong', 'side', { pair: 'pp' });
|
|
337
|
+
poi(tx + 3.15, ty + 0.7, 'pingpong', 'side', { pair: 'pp' });
|
|
338
|
+
const pp = { x0: tx - 0.2, x1: tx + 2.8, y: ty + 0.7 };
|
|
339
|
+
|
|
340
|
+
// ---- front corner: vending machines, foosball, phone booth
|
|
341
|
+
const fy = OD + 3.7;
|
|
342
|
+
if (D - fy >= 3.4) {
|
|
343
|
+
add(0.5 + fy + 0.4, () => this.vending(0.12, fy, '#c0392b'));
|
|
344
|
+
prop('vending', { x: 0.12, y: fy, color: '#c0392b' });
|
|
345
|
+
prop('vending', { x: 0.12, y: fy + 0.8, color: '#2f6fb0' });
|
|
346
|
+
add(0.5 + fy + 1.2, () => this.vending(0.12, fy + 0.8, '#2f6fb0'));
|
|
347
|
+
solid(0.1, fy, 0.75, 1.55);
|
|
348
|
+
poi(1.3, fy + 0.4, 'vending', 'back');
|
|
349
|
+
poi(1.3, fy + 1.2, 'vending', 'back');
|
|
350
|
+
}
|
|
351
|
+
if (D - fy >= 2.2) {
|
|
352
|
+
const fx = 3.0, fby = fy + 0.4;
|
|
353
|
+
add(fx + 0.8 + fby + 0.4, () => this.foosball(fx, fby));
|
|
354
|
+
prop('foosball', { x: fx, y: fby });
|
|
355
|
+
solid(fx, fby, 1.6, 0.8, 0.05);
|
|
356
|
+
poi(fx - 0.45, fby + 0.4, 'foosball', 'side', { pair: 'fb' });
|
|
357
|
+
poi(fx + 2.05, fby + 0.4, 'foosball', 'side', { pair: 'fb' });
|
|
358
|
+
const bx = OW - 2.0, by = fy;
|
|
359
|
+
this.booth(add, bx, by);
|
|
360
|
+
prop('booth', { x: bx, y: by });
|
|
361
|
+
solid(bx - 0.05, by - 0.05, 1.4, 0.1, 0.03);
|
|
362
|
+
solid(bx - 0.05, by - 0.05, 0.1, 1.4, 0.03);
|
|
363
|
+
solid(bx + 1.25, by - 0.05, 0.1, 1.4, 0.03);
|
|
364
|
+
solid(bx + 0.85, by + 1.25, 0.5, 0.1, 0.03);
|
|
365
|
+
poi(bx + 0.65, by + 0.6, 'phone', 'front', { sit: 0.45 });
|
|
366
|
+
add(OW - 0.45 + D - 0.6, () => this.plant(OW - 0.45, D - 0.6));
|
|
367
|
+
prop('plant', { x: OW - 0.45, y: D - 0.6 });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ---- meeting room behind glass, door at y 3.6–4.6
|
|
371
|
+
const mx = OW;
|
|
372
|
+
for (const [y, d] of [[0, 1], [1, 1], [2, 1], [3, 0.6], [4.6, MR - 4.6]]) this.glassX(add, mx, y, d);
|
|
373
|
+
for (let x = mx; x < W - 0.01; x += 1) this.glassY(add, x, MR, Math.min(1, W - x));
|
|
374
|
+
prop('glass', { x: mx, y: 0, w: 0, d: 3.6 });
|
|
375
|
+
prop('glass', { x: mx, y: 4.6, w: 0, d: MR - 4.6 });
|
|
376
|
+
prop('glass', { x: mx, y: MR, w: RW, d: 0 });
|
|
377
|
+
solid(mx - 0.05, 0, 0.1, 3.6, 0.05);
|
|
378
|
+
solid(mx - 0.05, 4.6, 0.1, MR - 4.6, 0.05);
|
|
379
|
+
solid(mx, MR - 0.05, RW, 0.1, 0.05);
|
|
380
|
+
const mtx = mx + 1.6, mty = 1.7;
|
|
381
|
+
add(mtx + 1.5 + mty + 0.7, () => this.meetingTable(mtx, mty));
|
|
382
|
+
prop('meetingTable', { x: mtx, y: mty, w: 3.0, d: 1.4 });
|
|
383
|
+
solid(mtx, mty, 3.0, 1.4);
|
|
384
|
+
for (let k = 0; k < 3; k++) {
|
|
385
|
+
const cx = mtx + 0.5 + k * 1.0;
|
|
386
|
+
add(cx + mty - 0.45, () => this.stool(cx, mty - 0.4, 0.4, '#555c66'));
|
|
387
|
+
add(cx + mty + 1.78, () => this.stool(cx, mty + 1.8, 0.4, '#555c66'));
|
|
388
|
+
poi(cx, mty - 0.4, 'meeting', 'front', { sit: 0.42 });
|
|
389
|
+
prop('chair', { x: cx, y: mty - 0.4, face: 'front' });
|
|
390
|
+
prop('chair', { x: cx, y: mty + 1.8, face: 'back' });
|
|
391
|
+
poi(cx, mty + 1.8, 'meeting', 'back', { sit: 0.42 });
|
|
392
|
+
}
|
|
393
|
+
poi(mx + 3.6, 0.65, 'whiteboard', 'back');
|
|
394
|
+
add(W - 0.45 + 0.45, () => this.plant(W - 0.45, 0.45));
|
|
395
|
+
prop('plant', { x: W - 0.45, y: 0.45 });
|
|
396
|
+
solid(W - 0.65, 0.25, 0.4, 0.4);
|
|
397
|
+
|
|
398
|
+
// ---- pantry
|
|
399
|
+
const cy = MR + 0.3;
|
|
400
|
+
longBox(mx + 1.0, cy, 0, RW - 2.3, 0.7, 0.9, pal.counter, pal.counterL, pal.counterR, { bias: -0.5 });
|
|
401
|
+
solid(mx + 1.0, cy, RW - 2.3, 0.7);
|
|
402
|
+
prop('counter', { x: mx + 1.0, y: cy, w: RW - 2.3, d: 0.7 });
|
|
403
|
+
prop('coffee', { x: mx + 2.4, y: cy + 0.12 });
|
|
404
|
+
prop('sink', { x: mx + 3.9, y: cy + 0.12 });
|
|
405
|
+
prop('fridge', { x: W - 1.15, y: cy });
|
|
406
|
+
prop('cooler', { x: mx + 0.35, y: cy + 0.05 });
|
|
407
|
+
add(mx + 2.65 + cy + 0.35, () => this.coffeeMachine(mx + 2.4, cy + 0.12));
|
|
408
|
+
add(mx + 4.15 + cy + 0.35, () => this.sink(mx + 3.9, cy + 0.12));
|
|
409
|
+
add(W - 0.75 + cy + 0.4, () => this.fridge(W - 1.15, cy));
|
|
410
|
+
solid(W - 1.15, cy, 0.85, 0.75);
|
|
411
|
+
add(mx + 0.55 + cy + 0.25, () => this.waterCooler(mx + 0.35, cy + 0.05));
|
|
412
|
+
solid(mx + 0.3, cy, 0.5, 0.5);
|
|
413
|
+
poi(mx + 2.65, cy + 1.2, 'coffee', 'back');
|
|
414
|
+
poi(mx + 4.15, cy + 1.2, 'sink', 'back');
|
|
415
|
+
poi(W - 0.75, cy + 1.3, 'fridge', 'back');
|
|
416
|
+
poi(mx + 0.6, cy + 0.95, 'water', 'back');
|
|
417
|
+
const rtx = mx + 3.6, rty = MR + 2.9;
|
|
418
|
+
add(rtx + rty, () => this.roundTable(rtx, rty));
|
|
419
|
+
prop('roundTable', { x: rtx, y: rty });
|
|
420
|
+
solid(rtx - 0.45, rty - 0.45, 0.9, 0.9);
|
|
421
|
+
for (const [sx, sy, face] of [[rtx - 1.0, rty, 'front'], [rtx + 1.0, rty, 'front'], [rtx, rty + 1.0, 'back'], [rtx, rty - 0.95, 'front']]) {
|
|
422
|
+
add(sx + sy - 0.02, () => this.stool(sx, sy, 0.45, '#c9a24c'));
|
|
423
|
+
poi(sx, sy, 'stool', face, { sit: 0.47 });
|
|
424
|
+
prop('barStool', { x: sx, y: sy });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ---- lounge
|
|
428
|
+
const lx = mx + 0.6, ly = PY + 0.4;
|
|
429
|
+
const shelfX = mx + 3.4, shelfW = RW - 3.6;
|
|
430
|
+
longBox(shelfX, PY - 0.25, 0, shelfW, 0.45, 1.45, this.shade(pal.deskTop, -5), pal.deskL, pal.deskR, {
|
|
431
|
+
extra: (x0, x1) => this.books(x0, x1, PY + 0.2),
|
|
432
|
+
});
|
|
433
|
+
solid(shelfX, PY - 0.25, shelfW, 0.45);
|
|
434
|
+
prop('shelf', { x: shelfX, y: PY - 0.25, w: shelfW, d: 0.45 });
|
|
435
|
+
poi(mx + 4.6, PY + 0.75, 'read', 'back');
|
|
436
|
+
poi(mx + 5.8, PY + 0.75, 'read', 'back');
|
|
437
|
+
add(lx + ly + 0.6, () => this.couch(lx, ly + 0.6));
|
|
438
|
+
prop('couch', { x: lx, y: ly + 0.6, w: 2.8, d: 0.8 });
|
|
439
|
+
prop('coffeeTable', { x: lx + 0.6, y: ly + 2.0, w: 1.6, d: 0.7 });
|
|
440
|
+
prop('beanbag', { x: lx + 0.4, y: ly + 3.6, color: '#c96442' });
|
|
441
|
+
prop('beanbag', { x: lx + 2.6, y: ly + 3.6, color: '#7d93c4' });
|
|
442
|
+
solid(lx, ly + 0.6, 2.8, 0.8);
|
|
443
|
+
for (let k = 0; k < 3; k++) poi(lx + 0.5 + k * 0.9, ly + 1.2, 'couch', 'front', { sit: 0.36 });
|
|
444
|
+
add(lx + 1.4 + ly + 2.35, () => this.coffeeTable(lx + 0.6, ly + 2.0));
|
|
445
|
+
solid(lx + 0.6, ly + 2.0, 1.6, 0.7);
|
|
446
|
+
add(lx + 0.4 + ly + 3.6, () => this.beanbag(lx + 0.4, ly + 3.6, '#c96442'));
|
|
447
|
+
add(lx + 2.6 + ly + 3.6, () => this.beanbag(lx + 2.6, ly + 3.6, '#7d93c4'));
|
|
448
|
+
poi(lx + 0.4, ly + 3.6, 'beanbag', 'front', { sit: 0.18 });
|
|
449
|
+
poi(lx + 2.6, ly + 3.6, 'beanbag', 'front', { sit: 0.18 });
|
|
450
|
+
const ax = W - 1.2, ay = ly + 1.0;
|
|
451
|
+
add(ax + 0.35 + ay + 0.35, () => this.arcade(ax, ay));
|
|
452
|
+
prop('arcade', { x: ax, y: ay });
|
|
453
|
+
prop('plant', { x: W - 0.5, y: D - 0.5 });
|
|
454
|
+
prop('lamp', { x: lx - 0.2, y: D - 0.6 });
|
|
455
|
+
prop('rug', { x: OW + 0.3, y: PY + 0.6, w: 4.0, d: D - PY - 1.0 });
|
|
456
|
+
solid(ax, ay, 0.7, 0.7);
|
|
457
|
+
poi(ax + 0.35, ay + 1.15, 'arcade', 'back');
|
|
458
|
+
add(W - 0.5 + D - 0.5, () => this.plant(W - 0.5, D - 0.5));
|
|
459
|
+
add(lx - 0.2 + D - 0.6, () => this.floorLamp(lx - 0.2, D - 0.6));
|
|
460
|
+
|
|
461
|
+
const posters = [
|
|
462
|
+
{ y: 2.2, color: '#c96442', text: 'SHIP IT' },
|
|
463
|
+
{ y: 4.6, color: '#10a37f', text: 'TESTS ✓' },
|
|
464
|
+
];
|
|
465
|
+
return { W, D, OW, OD, MR, PY, RW, grid, items, props, pois, desks, windows, posters, pp, door: { x: 0.4, y: D - 1.2 } };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
glassX(add, x, y, d) {
|
|
469
|
+
add(x + y + d / 2 + 0.02, () => {
|
|
470
|
+
const P = (a, b, c) => this.iso(a, b, c);
|
|
471
|
+
this.poly([P(x, y, 0), P(x, y + d, 0), P(x, y + d, 0.22), P(x, y, 0.22)], this.pal.trim);
|
|
472
|
+
this.poly([P(x, y, 0.22), P(x, y + d, 0.22), P(x, y + d, 2.1), P(x, y, 2.1)], this.pal.glass, this.pal.glassEdge);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
glassY(add, x, y, w) {
|
|
477
|
+
add(x + w / 2 + y + 0.02, () => {
|
|
478
|
+
const P = (a, b, c) => this.iso(a, b, c);
|
|
479
|
+
this.poly([P(x, y, 0), P(x + w, y, 0), P(x + w, y, 0.22), P(x, y, 0.22)], this.pal.trim);
|
|
480
|
+
this.poly([P(x, y, 0.22), P(x + w, y, 0.22), P(x + w, y, 2.1), P(x, y, 2.1)], this.pal.glass, this.pal.glassEdge);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ------------------------------------------------------------ sessions → people
|
|
485
|
+
|
|
486
|
+
update(sessions) {
|
|
487
|
+
const working = sessions.filter((s) => WORKING.has(s.status));
|
|
488
|
+
const off = sessions
|
|
489
|
+
.filter((s) => !WORKING.has(s.status))
|
|
490
|
+
.sort((a, b) => (b.lastEventAt || 0) - (a.lastEventAt || 0));
|
|
491
|
+
const wanderers = off.slice(0, MAX_WANDERERS);
|
|
492
|
+
this.offDuty = off.length - wanderers.length;
|
|
493
|
+
|
|
494
|
+
const cap = this.plan.desks.length;
|
|
495
|
+
// Scene packs have a fixed floor plan; the built-in office grows with the team.
|
|
496
|
+
if (!this.plan.fixed && (working.length > cap - 1 || (cap > 8 && working.length < cap * 0.35))) {
|
|
497
|
+
this.plan = this.buildPlan(working.length);
|
|
498
|
+
this.deskOf.clear();
|
|
499
|
+
for (const p of this.people.values()) { p.poi = null; p.napDesk = null; p.desk = null; p.until = 0; }
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const workKeys = new Set(working.map((s) => s.key));
|
|
503
|
+
for (const k of [...this.deskOf.keys()]) if (!workKeys.has(k)) this.deskOf.delete(k);
|
|
504
|
+
const used = new Set(this.deskOf.values());
|
|
505
|
+
// Fill the desks that face us first so more faces and screens show.
|
|
506
|
+
const desks = this.plan.desks;
|
|
507
|
+
const order = desks.map((d, i) => i).sort((a, b) => (desks[a].side === 's') - (desks[b].side === 's') || a - b);
|
|
508
|
+
for (const s of [...working].sort((a, b) => (a.startedAt || 0) - (b.startedAt || 0))) {
|
|
509
|
+
if (this.deskOf.has(s.key)) continue;
|
|
510
|
+
const i = order.find((n) => !used.has(n));
|
|
511
|
+
if (i == null) continue;
|
|
512
|
+
this.deskOf.set(s.key, i);
|
|
513
|
+
used.add(i);
|
|
514
|
+
}
|
|
515
|
+
const takenDesks = new Set([...this.deskOf.values()].map((i) => desks[i]));
|
|
516
|
+
|
|
517
|
+
const shown = new Set();
|
|
518
|
+
for (const s of [...working, ...wanderers]) {
|
|
519
|
+
const isWorker = this.deskOf.has(s.key);
|
|
520
|
+
if (!isWorker && WORKING.has(s.status)) continue; // no desk left
|
|
521
|
+
shown.add(s.key);
|
|
522
|
+
let p = this.people.get(s.key);
|
|
523
|
+
if (!p) { p = this.newPerson(s); this.people.set(s.key, p); }
|
|
524
|
+
p.s = s;
|
|
525
|
+
p.leaving = false;
|
|
526
|
+
if (isWorker) {
|
|
527
|
+
this.release(p);
|
|
528
|
+
const desk = desks[this.deskOf.get(s.key)];
|
|
529
|
+
if (p.desk !== desk || p.mode !== 'work') { p.desk = desk; this.goTo(p, desk.seat); }
|
|
530
|
+
p.mode = 'work';
|
|
531
|
+
} else {
|
|
532
|
+
if (p.mode !== 'wander') { p.mode = 'wander'; p.desk = null; p.poi = null; p.until = 0; }
|
|
533
|
+
// Someone started working at the desk this one was napping on.
|
|
534
|
+
if (p.napDesk && takenDesks.has(p.napDesk)) { this.release(p); p.path = []; }
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
for (const [k, p] of this.people) {
|
|
538
|
+
if (!shown.has(k) && !p.leaving) {
|
|
539
|
+
p.leaving = true;
|
|
540
|
+
p.mode = 'leave';
|
|
541
|
+
this.release(p);
|
|
542
|
+
p.desk = null;
|
|
543
|
+
this.goTo(p, this.plan.door);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
this.first = false;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
release(p) {
|
|
550
|
+
if (p.poi) { p.poi.taken = null; p.poi = null; }
|
|
551
|
+
if (p.napDesk) { p.napDesk.napper = null; p.napDesk = null; }
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
newPerson(s) {
|
|
555
|
+
const h = hash(s.key);
|
|
556
|
+
const door = this.plan.door;
|
|
557
|
+
return {
|
|
558
|
+
key: s.key, h, s,
|
|
559
|
+
skin: pick(SKIN, h, 3), hair: pick(HAIR, h, 7), pants: pick(PANTS, h, 13),
|
|
560
|
+
shirt: pick(SHIRT[s.agent] || SHIRT.claude, h, 11),
|
|
561
|
+
hairStyle: h % 4,
|
|
562
|
+
phase: (h % 1000) / 1000 * Math.PI * 2,
|
|
563
|
+
x: door.x, y: door.y,
|
|
564
|
+
path: [], walkFace: 'front', until: 0, mode: null,
|
|
565
|
+
// On the first snapshot everyone is placed directly, no stampede.
|
|
566
|
+
placeOnArrive: this.first,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
goTo(p, target) {
|
|
571
|
+
if (p.placeOnArrive) {
|
|
572
|
+
p.x = target.x; p.y = target.y; p.path = []; p.placeOnArrive = false;
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
p.path = this.plan.grid.path(p.x, p.y, target.x, target.y);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
wander(p, now) {
|
|
579
|
+
if (p.path.length) return;
|
|
580
|
+
if (p.poi && now < p.until) return;
|
|
581
|
+
this.release(p);
|
|
582
|
+
const plan = this.plan;
|
|
583
|
+
const workingDesks = new Set([...this.deskOf.values()].map((i) => plan.desks[i]));
|
|
584
|
+
const sleepy = p.s.status === 'idle';
|
|
585
|
+
const weighted = [];
|
|
586
|
+
for (const q of plan.pois) {
|
|
587
|
+
if (q.taken) continue;
|
|
588
|
+
let w = 1;
|
|
589
|
+
if (q.pair) w = plan.pois.some((o) => o.pair === q.pair && o !== q && o.taken) ? 7 : 1.2;
|
|
590
|
+
if (q.act === 'couch' || q.act === 'beanbag' || q.sleepy) w = sleepy ? 3 : 1.4;
|
|
591
|
+
weighted.push([q, w]);
|
|
592
|
+
}
|
|
593
|
+
for (const d of plan.noNap ? [] : plan.desks) {
|
|
594
|
+
if (workingDesks.has(d) || d.napper) continue;
|
|
595
|
+
weighted.push([{ x: d.seat.x, y: d.seat.y, act: 'nap', face: d.side === 'n' ? 'front' : 'back', sit: 0.34, desk: d }, sleepy ? 3 : 0.25]);
|
|
596
|
+
}
|
|
597
|
+
if (!weighted.length) return;
|
|
598
|
+
let r = Math.random() * weighted.reduce((a, [, w]) => a + w, 0);
|
|
599
|
+
let choice = weighted[0][0];
|
|
600
|
+
for (const [q, w] of weighted) { r -= w; if (r <= 0) { choice = q; break; } }
|
|
601
|
+
if (choice.desk) { choice.desk.napper = p.key; p.napDesk = choice.desk; }
|
|
602
|
+
choice.taken = p.key;
|
|
603
|
+
p.poi = choice;
|
|
604
|
+
p.until = Infinity; // set on arrival
|
|
605
|
+
p.stay = (choice.act === 'nap' ? rand(25, 60) : choice.act === 'pingpong' ? rand(15, 30) : rand(8, 20)) * 1000;
|
|
606
|
+
this.goTo(p, choice);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
setVisible(v) { this.visible = v; if (v) this.resize(); }
|
|
610
|
+
|
|
611
|
+
// Room on the right covered by an overlay (the agent panel). The canvas
|
|
612
|
+
// keeps its size; the view eases over so the room stays centred in what
|
|
613
|
+
// is still visible, instead of the canvas resizing under the slide.
|
|
614
|
+
setInset(px, animate = true) {
|
|
615
|
+
const now = performance.now();
|
|
616
|
+
this.insetFrom = animate ? this.inset(now) : px;
|
|
617
|
+
this.insetTo = px;
|
|
618
|
+
this.insetT0 = animate ? now : -Infinity;
|
|
619
|
+
}
|
|
620
|
+
inset(now = performance.now()) {
|
|
621
|
+
if (this.insetTo == null) return 0;
|
|
622
|
+
const k = Math.min(1, (now - this.insetT0) / 340);
|
|
623
|
+
const e = 1 - Math.pow(1 - k, 3); // ease out, like the panel's slide
|
|
624
|
+
return this.insetFrom + (this.insetTo - this.insetFrom) * e;
|
|
625
|
+
}
|
|
626
|
+
insetMoving(now = performance.now()) { return this.insetTo != null && now - this.insetT0 < 360; }
|
|
627
|
+
|
|
628
|
+
// ------------------------------------------------------------ camera / input
|
|
629
|
+
|
|
630
|
+
resize() {
|
|
631
|
+
const r = this.root.getBoundingClientRect();
|
|
632
|
+
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
|
633
|
+
this.cw = Math.max(this.compact ? 1 : 200, r.width);
|
|
634
|
+
this.ch = Math.max(this.compact ? 1 : 240, r.height);
|
|
635
|
+
this.canvas.width = Math.round(this.cw * dpr);
|
|
636
|
+
this.canvas.height = Math.round(this.ch * dpr);
|
|
637
|
+
this.canvas.style.width = this.cw + 'px';
|
|
638
|
+
this.canvas.style.height = this.ch + 'px';
|
|
639
|
+
this.dpr = dpr;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
fitCamera() {
|
|
643
|
+
const { W, D } = this.plan;
|
|
644
|
+
const left = this.iso(0, D, 0).x, right = this.iso(W, 0, 0).x;
|
|
645
|
+
const top = this.iso(0, 0, WALL_H).y - 20, bottom = this.iso(W, D, 0).y + 10;
|
|
646
|
+
const inset = this.inset();
|
|
647
|
+
const vw = Math.max(120, this.cw - inset);
|
|
648
|
+
const zoom = Math.min(vw / (right - left + 30), this.ch / (bottom - top + 30), 2);
|
|
649
|
+
const want = { zoom, ox: vw / 2 - ((left + right) / 2) * zoom, oy: this.ch / 2 - ((top + bottom) / 2) * zoom };
|
|
650
|
+
// While the inset eases, follow it exactly; otherwise settle gently.
|
|
651
|
+
if (!this.cam || this.insetMoving()) this.cam = want;
|
|
652
|
+
else for (const k of ['zoom', 'ox', 'oy']) this.cam[k] += (want[k] - this.cam[k]) * 0.1;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
pointer(e, click) {
|
|
656
|
+
const r = this.canvas.getBoundingClientRect();
|
|
657
|
+
const mx = e.clientX - r.left, my = e.clientY - r.top;
|
|
658
|
+
let found = null;
|
|
659
|
+
for (let i = this.hit.length - 1; i >= 0; i--) {
|
|
660
|
+
const h = this.hit[i];
|
|
661
|
+
if (mx >= h.x0 && mx <= h.x1 && my >= h.y0 && my <= h.y1) { found = h; break; }
|
|
662
|
+
}
|
|
663
|
+
const key = found ? found.key : null;
|
|
664
|
+
this.canvas.style.cursor = key ? 'pointer' : 'default';
|
|
665
|
+
if (key !== this.hoverKey || click) {
|
|
666
|
+
this.hoverKey = key;
|
|
667
|
+
const p = key && this.people.get(key);
|
|
668
|
+
this.onHover(p ? p.s : null, { x: e.clientX, y: e.clientY }, !!click);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
iso(x, y, z = 0) {
|
|
673
|
+
return { x: (x - y) * TW / 2, y: (x + y) * TH / 2 - z * TH };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ------------------------------------------------------------ primitives
|
|
677
|
+
|
|
678
|
+
poly(pts, fill, stroke) {
|
|
679
|
+
const c = this.ctx;
|
|
680
|
+
c.beginPath();
|
|
681
|
+
pts.forEach((p, i) => (i ? c.lineTo(p.x, p.y) : c.moveTo(p.x, p.y)));
|
|
682
|
+
c.closePath();
|
|
683
|
+
if (fill) { c.fillStyle = fill; c.fill(); }
|
|
684
|
+
if (stroke) { c.strokeStyle = stroke; c.lineWidth = 1; c.stroke(); }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
box(x, y, z, w, d, h, top, left, right) {
|
|
688
|
+
const P = (a, b, cc) => this.iso(a, b, cc);
|
|
689
|
+
this.poly([P(x, y + d, z), P(x + w, y + d, z), P(x + w, y + d, z + h), P(x, y + d, z + h)], left);
|
|
690
|
+
this.poly([P(x + w, y, z), P(x + w, y + d, z), P(x + w, y + d, z + h), P(x + w, y, z + h)], right);
|
|
691
|
+
this.poly([P(x, y, z + h), P(x + w, y, z + h), P(x + w, y + d, z + h), P(x, y + d, z + h)], top);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
shade(color, amt) {
|
|
695
|
+
if (color[0] !== '#') return color;
|
|
696
|
+
const n = parseInt(color.slice(1), 16);
|
|
697
|
+
const f = (v) => clamp(Math.round(v + amt), 0, 255);
|
|
698
|
+
return `rgb(${f(n >> 16)},${f((n >> 8) & 255)},${f(n & 255)})`;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
roundRect(x, y, w, h, r, fill, stroke) {
|
|
702
|
+
const c = this.ctx;
|
|
703
|
+
c.beginPath();
|
|
704
|
+
c.moveTo(x + r, y);
|
|
705
|
+
c.arcTo(x + w, y, x + w, y + h, r);
|
|
706
|
+
c.arcTo(x + w, y + h, x, y + h, r);
|
|
707
|
+
c.arcTo(x, y + h, x, y, r);
|
|
708
|
+
c.arcTo(x, y, x + w, y, r);
|
|
709
|
+
c.closePath();
|
|
710
|
+
if (fill) { c.fillStyle = fill; c.fill(); }
|
|
711
|
+
if (stroke) { c.strokeStyle = stroke; c.lineWidth = 1.5; c.stroke(); }
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Draw flat things on a wall: 'y0' = back wall (runs along x),
|
|
715
|
+
// 'x0' = left wall (runs along y).
|
|
716
|
+
onWall(dir, a, z, fn) {
|
|
717
|
+
const c = this.ctx;
|
|
718
|
+
const o = dir === 'y0' ? this.iso(a, 0, z) : this.iso(0, a, z);
|
|
719
|
+
c.save();
|
|
720
|
+
c.translate(o.x, o.y);
|
|
721
|
+
c.transform(1, dir === 'y0' ? 0.5 : -0.5, 0, 1, 0, 0);
|
|
722
|
+
fn(c);
|
|
723
|
+
c.restore();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ------------------------------------------------------------ room shell
|
|
727
|
+
|
|
728
|
+
drawRoom() {
|
|
729
|
+
const { W, D, OW, MR, PY, pp } = this.plan;
|
|
730
|
+
const pal = this.pal;
|
|
731
|
+
const P = (x, y, z) => this.iso(x, y, z);
|
|
732
|
+
const t = this.t / 1000;
|
|
733
|
+
|
|
734
|
+
// floors: wood office, carpet meeting room, tiled pantry, wood lounge
|
|
735
|
+
for (let x = 0; x < Math.ceil(OW); x++) {
|
|
736
|
+
for (let y = 0; y < Math.ceil(D); y++) {
|
|
737
|
+
const x1 = Math.min(OW, x + 1), y1 = Math.min(D, y + 1);
|
|
738
|
+
this.poly([P(x, y), P(x1, y), P(x1, y1), P(x, y1)], (x + y) % 2 ? pal.wood1 : pal.wood2);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
for (let y = 0.5; y < D; y += 0.5) this.poly([P(0, y), P(OW, y), P(OW, y + 0.012), P(0, y + 0.012)], pal.woodLine);
|
|
742
|
+
this.poly([P(OW, 0), P(W, 0), P(W, MR), P(OW, MR)], pal.carpet);
|
|
743
|
+
for (let x = OW + 0.5; x < W; x += 0.5) this.poly([P(x, 0), P(x + 0.015, 0), P(x + 0.015, MR), P(x, MR)], pal.carpetLine);
|
|
744
|
+
for (let i = 0; i < Math.ceil((W - OW) * 2); i++) {
|
|
745
|
+
for (let j = 0; j < Math.ceil((PY - MR) * 2); j++) {
|
|
746
|
+
const x = OW + i / 2, y = MR + j / 2;
|
|
747
|
+
const x1 = Math.min(W, x + 0.5), y1 = Math.min(PY, y + 0.5);
|
|
748
|
+
this.poly([P(x, y), P(x1, y), P(x1, y1), P(x, y1)], (i + j) % 2 ? pal.tileA : pal.tileB);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
for (let x = Math.floor(OW); x < W; x++) {
|
|
752
|
+
for (let y = PY; y < D; y++) {
|
|
753
|
+
const x0 = Math.max(OW, x), x1 = Math.min(W, x + 1), y1 = Math.min(D, y + 1);
|
|
754
|
+
this.poly([P(x0, y), P(x1, y), P(x1, y1), P(x0, y1)], (x + Math.floor(y)) % 2 ? pal.wood1 : pal.wood2);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
this.poly([P(OW + 0.3, PY + 0.6), P(OW + 4.3, PY + 0.6), P(OW + 4.3, D - 0.4), P(OW + 0.3, D - 0.4)], pal.rug);
|
|
758
|
+
this.poly([P(OW + 0.5, PY + 0.8), P(OW + 4.1, PY + 0.8), P(OW + 4.1, D - 0.6), P(OW + 0.5, D - 0.6)], null, pal.rugEdge);
|
|
759
|
+
this.poly([P(pp.x0 - 0.6, pp.y - 1.4), P(pp.x1 + 0.6, pp.y - 1.4), P(pp.x1 + 0.6, pp.y + 1.3), P(pp.x0 - 0.6, pp.y + 1.3)], pal.mat);
|
|
760
|
+
|
|
761
|
+
// walls
|
|
762
|
+
this.poly([P(0, 0, 0), P(W, 0, 0), P(W, 0, WALL_H), P(0, 0, WALL_H)], pal.wallL);
|
|
763
|
+
this.poly([P(0, 0, 0), P(0, D, 0), P(0, D, WALL_H), P(0, 0, WALL_H)], pal.wallR);
|
|
764
|
+
this.poly([P(0, 0, WALL_H), P(W, 0, WALL_H), P(W, -0.15, WALL_H), P(-0.15, -0.15, WALL_H), P(-0.15, D, WALL_H), P(0, D, WALL_H)], pal.wallTop);
|
|
765
|
+
this.poly([P(0, 0, 0), P(W, 0, 0), P(W, 0, 0.12), P(0, 0, 0.12)], pal.trim);
|
|
766
|
+
this.poly([P(0, 0, 0), P(0, D, 0), P(0, D, 0.12), P(0, 0, 0.12)], pal.trim);
|
|
767
|
+
|
|
768
|
+
// windows with the real sky outside
|
|
769
|
+
const sky = skyColor();
|
|
770
|
+
for (const x of this.plan.windows) {
|
|
771
|
+
this.poly([P(x, 0, 0.85), P(x + 1.5, 0, 0.85), P(x + 1.5, 0, 2.0), P(x, 0, 2.0)], sky.sky);
|
|
772
|
+
if (sky.night) {
|
|
773
|
+
this.onWall('y0', x + 0.3, 1.75, (c) => { c.fillStyle = '#f4f1d0'; c.beginPath(); c.arc(0, 0, 3, 0, Math.PI * 2); c.fill(); });
|
|
774
|
+
this.onWall('y0', x + 1.1, 1.55, (c) => { c.fillStyle = 'rgba(255,255,255,.7)'; c.fillRect(0, 0, 1.5, 1.5); });
|
|
775
|
+
} else {
|
|
776
|
+
const cx = x + 0.25 + ((t * 0.02 + x * 0.37) % 1) * 1.0;
|
|
777
|
+
this.onWall('y0', cx, 1.65, (c) => {
|
|
778
|
+
c.fillStyle = 'rgba(255,255,255,.85)';
|
|
779
|
+
c.beginPath(); c.ellipse(0, 0, 7, 3, 0, 0, Math.PI * 2); c.ellipse(5, -2, 5, 3, 0, 0, Math.PI * 2); c.fill();
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
this.poly([P(x + 0.74, 0, 0.85), P(x + 0.76, 0, 0.85), P(x + 0.76, 0, 2.0), P(x + 0.74, 0, 2.0)], pal.trim);
|
|
783
|
+
this.poly([P(x, 0, 0.85), P(x + 1.5, 0, 0.85), P(x + 1.5, 0, 0.9), P(x, 0, 0.9)], pal.trim);
|
|
784
|
+
this.poly([P(x, 0.05), P(x + 1.5, 0.05), P(x + 1.95, 1.2), P(x + 0.45, 1.2)], sky.night ? 'rgba(120,140,200,.06)' : 'rgba(255,250,230,.16)');
|
|
785
|
+
}
|
|
786
|
+
// meeting room whiteboard + sign
|
|
787
|
+
const wx = OW + 2.4;
|
|
788
|
+
this.poly([P(wx, 0, 0.8), P(wx + 2.6, 0, 0.8), P(wx + 2.6, 0, 1.95), P(wx, 0, 1.95)], '#fdfdfb', pal.trim);
|
|
789
|
+
this.onWall('y0', wx + 0.25, 1.75, (c) => {
|
|
790
|
+
c.lineWidth = 1.4;
|
|
791
|
+
c.strokeStyle = '#3b8ee0';
|
|
792
|
+
c.beginPath(); c.moveTo(0, 0); c.lineTo(22, 0); c.moveTo(0, 7); c.lineTo(36, 7); c.stroke();
|
|
793
|
+
c.strokeStyle = '#c96442';
|
|
794
|
+
c.strokeRect(46, -2, 20, 12); c.strokeRect(80, 6, 20, 12);
|
|
795
|
+
c.beginPath(); c.moveTo(66, 4); c.lineTo(80, 12); c.stroke();
|
|
796
|
+
c.strokeStyle = '#5f9a4c';
|
|
797
|
+
c.beginPath(); c.moveTo(0, 22); for (let i = 0; i < 8; i++) c.lineTo(i * 8, 22 - ((i * 37) % 13)); c.stroke();
|
|
798
|
+
});
|
|
799
|
+
this.onWall('y0', OW + 0.5, 2.2, (c) => {
|
|
800
|
+
c.font = '700 9px ui-sans-serif, system-ui, sans-serif'; c.fillStyle = this.shade(pal.trim, -60); c.fillText('MEETING', 0, 0);
|
|
801
|
+
});
|
|
802
|
+
// left wall: posters, clock, door
|
|
803
|
+
for (const ps of this.plan.posters) {
|
|
804
|
+
this.poly([P(0, ps.y, 0.95), P(0, ps.y + 0.9, 0.95), P(0, ps.y + 0.9, 1.95), P(0, ps.y, 1.95)], ps.color);
|
|
805
|
+
this.onWall('x0', ps.y + 0.45, 1.4, (c) => {
|
|
806
|
+
c.font = '800 8px ui-sans-serif, system-ui, sans-serif'; c.fillStyle = '#fff'; c.textAlign = 'center'; c.fillText(ps.text, 0, 0);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
this.onWall('x0', 1.0, 1.85, (c) => {
|
|
810
|
+
c.beginPath(); c.arc(0, 0, 11, 0, Math.PI * 2); c.fillStyle = pal.bubble; c.fill();
|
|
811
|
+
c.lineWidth = 2; c.strokeStyle = pal.trim; c.stroke();
|
|
812
|
+
const d = new Date();
|
|
813
|
+
const hand = (ang, len, w) => { c.beginPath(); c.moveTo(0, 0); c.lineTo(Math.sin(ang) * len, -Math.cos(ang) * len); c.lineWidth = w; c.strokeStyle = pal.bubbleInk; c.stroke(); };
|
|
814
|
+
hand(((d.getHours() % 12) + d.getMinutes() / 60) / 12 * Math.PI * 2, 5.5, 2);
|
|
815
|
+
hand((d.getMinutes() / 60) * Math.PI * 2, 8, 1.4);
|
|
816
|
+
});
|
|
817
|
+
const dy = this.plan.door.y;
|
|
818
|
+
this.poly([P(0, dy - 0.6, 0), P(0, dy + 0.6, 0), P(0, dy + 0.6, 1.8), P(0, dy - 0.6, 1.8)], pal.trim);
|
|
819
|
+
this.poly([P(0, dy - 0.52, 0), P(0, dy + 0.52, 0), P(0, dy + 0.52, 1.72), P(0, dy - 0.52, 1.72)], this.shade(pal.deskL, -10));
|
|
820
|
+
this.onWall('x0', dy - 0.25, 0.85, (c) => { c.fillStyle = '#d8c48a'; c.beginPath(); c.arc(0, 0, 1.8, 0, Math.PI * 2); c.fill(); });
|
|
821
|
+
this.poly([P(0.05, dy - 0.6), P(0.9, dy - 0.6), P(0.9, dy + 0.6), P(0.05, dy + 0.6)], pal.dark ? '#5a3b2e' : '#b9654a');
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ------------------------------------------------------------ furniture
|
|
825
|
+
|
|
826
|
+
plant(x, y) {
|
|
827
|
+
const pal = this.pal;
|
|
828
|
+
this.box(x - 0.18, y - 0.18, 0, 0.36, 0.36, 0.3, this.shade(pal.pot, 10), pal.pot, this.shade(pal.pot, -15));
|
|
829
|
+
const c = this.ctx;
|
|
830
|
+
const b = this.iso(x, y, 0.3);
|
|
831
|
+
const sway = Math.sin(this.t / 1400 + x) * 0.05;
|
|
832
|
+
c.fillStyle = pal.plant;
|
|
833
|
+
for (let i = 0; i < 6; i++) {
|
|
834
|
+
const a = (i / 6) * Math.PI * 2;
|
|
835
|
+
c.beginPath();
|
|
836
|
+
c.ellipse(b.x + Math.cos(a) * 6, b.y - 13 + Math.sin(a) * 3, 5.5, 13, a / 3 + sway, 0, Math.PI * 2);
|
|
837
|
+
c.fill();
|
|
838
|
+
}
|
|
839
|
+
c.fillStyle = this.shade(pal.plant, 22);
|
|
840
|
+
c.beginPath(); c.ellipse(b.x, b.y - 20, 5, 10, sway, 0, Math.PI * 2); c.fill();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
serverRack(x, y) {
|
|
844
|
+
this.box(x, y, 0, 0.7, 0.6, 1.9, '#3a3d44', '#24262b', '#1b1d21');
|
|
845
|
+
const c = this.ctx;
|
|
846
|
+
const busy = this.deskOf.size;
|
|
847
|
+
for (let r = 0; r < 7; r++) {
|
|
848
|
+
for (let k = 0; k < 4; k++) {
|
|
849
|
+
const on = Math.sin(this.t / (180 + r * 37 + k * 53) + r * k) > 0.2 && (r * 4 + k) < Math.max(4, busy * 3);
|
|
850
|
+
const p = this.iso(x + 0.12 + k * 0.13, y + 0.6, 1.7 - r * 0.22);
|
|
851
|
+
c.fillStyle = on ? (k % 3 === 0 ? '#ffb347' : '#5cf08a') : '#2f3a33';
|
|
852
|
+
c.fillRect(p.x - 1, p.y - 1, 2.5, 2);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
printer(x, y) {
|
|
858
|
+
const pal = this.pal;
|
|
859
|
+
this.box(x, y, 0, 0.75, 0.55, 0.55, pal.deskTop, pal.deskL, pal.deskR);
|
|
860
|
+
this.box(x + 0.05, y + 0.05, 0.55, 0.65, 0.45, 0.28, '#e8e8e8', '#c8c8c8', '#b3b3b3');
|
|
861
|
+
this.box(x + 0.15, y + 0.35, 0.83, 0.45, 0.15, 0.02, '#ffffff', '#eee', '#ddd');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
pingpong(x, y) {
|
|
865
|
+
for (const [lx, ly] of [[0.1, 0.1], [2.4, 0.1], [0.1, 1.2], [2.4, 1.2]]) this.box(x + lx, y + ly, 0, 0.08, 0.08, 0.7, '#333', '#333', '#222');
|
|
866
|
+
this.box(x, y, 0.7, 2.6, 1.4, 0.06, '#2f7a52', '#245f40', '#1d4f35');
|
|
867
|
+
const P = (a, b, c) => this.iso(a, b, c);
|
|
868
|
+
this.poly([P(x + 0.04, y + 0.69, 0.765), P(x + 2.56, y + 0.69, 0.765), P(x + 2.56, y + 0.71, 0.765), P(x + 0.04, y + 0.71, 0.765)], '#ffffff');
|
|
869
|
+
this.poly([P(x + 1.3, y - 0.05, 0.76), P(x + 1.3, y + 1.45, 0.76), P(x + 1.3, y + 1.45, 0.95), P(x + 1.3, y - 0.05, 0.95)], 'rgba(255,255,255,.55)', '#ddd');
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
meetingTable(x, y) {
|
|
873
|
+
const pal = this.pal;
|
|
874
|
+
this.box(x + 0.3, y + 0.5, 0, 0.12, 0.4, 0.7, pal.deskL, pal.deskL, pal.deskR);
|
|
875
|
+
this.box(x + 2.6, y + 0.5, 0, 0.12, 0.4, 0.7, pal.deskL, pal.deskL, pal.deskR);
|
|
876
|
+
this.box(x, y, 0.7, 3.0, 1.4, 0.08, this.shade(pal.deskTop, 12), pal.deskL, pal.deskR);
|
|
877
|
+
this.box(x + 1.2, y + 0.45, 0.78, 0.5, 0.35, 0.03, '#2a2a2e', '#222', '#1a1a1a');
|
|
878
|
+
this.box(x + 0.4, y + 0.3, 0.78, 0.2, 0.28, 0.02, '#fff', '#eee', '#ddd');
|
|
879
|
+
this.box(x + 2.2, y + 0.8, 0.78, 0.12, 0.12, 0.12, '#f2f2f2', '#d0d0d0', '#bbb');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
stool(x, y, h, color) {
|
|
883
|
+
this.box(x - 0.04, y - 0.04, 0, 0.08, 0.08, h, '#555', '#444', '#333');
|
|
884
|
+
this.box(x - 0.2, y - 0.2, h, 0.4, 0.4, 0.07, this.shade(color, 18), color, this.shade(color, -18));
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
roundTable(x, y) {
|
|
888
|
+
const c = this.ctx;
|
|
889
|
+
this.box(x - 0.05, y - 0.05, 0, 0.1, 0.1, 0.72, '#555', '#444', '#333');
|
|
890
|
+
const p = this.iso(x, y, 0.75);
|
|
891
|
+
c.fillStyle = this.shade(this.pal.deskTop, -15);
|
|
892
|
+
c.beginPath(); c.ellipse(p.x, p.y + 3, 30, 15, 0, 0, Math.PI * 2); c.fill();
|
|
893
|
+
c.fillStyle = this.shade(this.pal.deskTop, 15);
|
|
894
|
+
c.beginPath(); c.ellipse(p.x, p.y, 30, 15, 0, 0, Math.PI * 2); c.fill();
|
|
895
|
+
c.fillStyle = '#fff';
|
|
896
|
+
c.beginPath(); c.ellipse(p.x + 8, p.y - 2, 5, 2.5, 0, 0, Math.PI * 2); c.fill();
|
|
897
|
+
c.fillStyle = '#e6b35c';
|
|
898
|
+
c.beginPath(); c.arc(p.x + 8, p.y - 3, 2, 0, Math.PI * 2); c.fill();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
coffeeMachine(x, y) {
|
|
902
|
+
this.box(x, y, 0.9, 0.5, 0.45, 0.6, '#555b63', '#3a3f45', '#2e3237');
|
|
903
|
+
this.box(x + 0.15, y + 0.42, 1.0, 0.2, 0.05, 0.15, '#222', '#111', '#111');
|
|
904
|
+
const p = this.iso(x + 0.4, y + 0.45, 1.35);
|
|
905
|
+
this.ctx.fillStyle = Math.sin(this.t / 400) > 0 ? '#ff6b4a' : '#7a3a2a';
|
|
906
|
+
this.ctx.fillRect(p.x, p.y, 2.5, 2.5);
|
|
907
|
+
this.box(x + 0.6, y + 0.1, 0.9, 0.14, 0.14, 0.14, '#fff', '#ddd', '#ccc');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
sink(x, y) {
|
|
911
|
+
this.box(x, y + 0.05, 0.9, 0.55, 0.4, 0.02, '#9aa3ad', '#7d858e', '#6c737b');
|
|
912
|
+
this.box(x + 0.22, y + 0.02, 0.92, 0.06, 0.06, 0.3, this.pal.metal, this.pal.metal, '#888');
|
|
913
|
+
this.box(x + 0.22, y + 0.02, 1.2, 0.06, 0.2, 0.04, this.pal.metal, this.pal.metal, '#888');
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
fridge(x, y) {
|
|
917
|
+
this.box(x, y, 0, 0.85, 0.75, 1.95, '#eef1f4', '#cfd5db', '#bcc3ca');
|
|
918
|
+
const P = (a, b, c) => this.iso(a, b, c);
|
|
919
|
+
this.poly([P(x, y + 0.75, 1.25), P(x + 0.85, y + 0.75, 1.25), P(x + 0.85, y + 0.75, 1.27), P(x, y + 0.75, 1.27)], '#9aa3ad');
|
|
920
|
+
this.poly([P(x + 0.7, y + 0.75, 1.4), P(x + 0.73, y + 0.75, 1.4), P(x + 0.73, y + 0.75, 1.75), P(x + 0.7, y + 0.75, 1.75)], '#888');
|
|
921
|
+
for (const [mx, mz, col] of [[0.2, 1.6, '#c96442'], [0.4, 1.45, '#3b8ee0'], [0.3, 0.9, '#5f9a4c']]) {
|
|
922
|
+
const p = P(x + mx, y + 0.75, mz);
|
|
923
|
+
this.ctx.fillStyle = col; this.ctx.fillRect(p.x, p.y, 4, 4);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
waterCooler(x, y) {
|
|
928
|
+
this.box(x, y, 0, 0.42, 0.42, 0.95, '#e8ecef', '#cfd5db', '#bcc3ca');
|
|
929
|
+
const c = this.ctx;
|
|
930
|
+
const p = this.iso(x + 0.21, y + 0.21, 0.95);
|
|
931
|
+
c.fillStyle = 'rgba(120,180,240,.6)';
|
|
932
|
+
c.beginPath(); c.ellipse(p.x, p.y - 9, 8, 11, 0, 0, Math.PI * 2); c.fill();
|
|
933
|
+
c.fillStyle = 'rgba(255,255,255,.5)';
|
|
934
|
+
c.fillRect(p.x - 4, p.y - 16, 2, 10);
|
|
935
|
+
const k = (this.t / 1800) % 1;
|
|
936
|
+
c.fillStyle = 'rgba(255,255,255,.85)';
|
|
937
|
+
c.beginPath(); c.arc(p.x + 2, p.y - 2 - k * 16, 1.2, 0, Math.PI * 2); c.fill();
|
|
938
|
+
const tap = this.iso(x + 0.21, y + 0.42, 0.65);
|
|
939
|
+
c.fillStyle = '#3b8ee0'; c.fillRect(tap.x - 1.5, tap.y, 3, 3);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
couch(x, y) {
|
|
943
|
+
const pal = this.pal;
|
|
944
|
+
this.box(x, y - 0.05, 0.32, 2.8, 0.25, 0.42, this.shade(pal.couch, 10), pal.couchD, this.shade(pal.couchD, -12));
|
|
945
|
+
this.box(x, y, 0, 2.8, 0.8, 0.32, pal.couch, pal.couchD, this.shade(pal.couchD, -12));
|
|
946
|
+
this.box(x - 0.15, y - 0.05, 0, 0.18, 0.85, 0.52, this.shade(pal.couch, 6), pal.couchD, this.shade(pal.couchD, -12));
|
|
947
|
+
this.box(x + 2.8, y - 0.05, 0, 0.18, 0.85, 0.52, this.shade(pal.couch, 6), pal.couchD, this.shade(pal.couchD, -12));
|
|
948
|
+
this.box(x + 2.15, y + 0.12, 0.32, 0.35, 0.16, 0.28, '#e9c46a', '#d4ae52', '#c19c43');
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
coffeeTable(x, y) {
|
|
952
|
+
const pal = this.pal;
|
|
953
|
+
this.box(x, y, 0, 1.6, 0.7, 0.3, this.shade(pal.deskTop, -10), pal.deskL, pal.deskR);
|
|
954
|
+
this.box(x + 0.2, y + 0.15, 0.3, 0.35, 0.25, 0.06, '#c96442', '#a85336', '#8f462e');
|
|
955
|
+
this.box(x + 1.1, y + 0.3, 0.3, 0.12, 0.12, 0.12, '#fff', '#ddd', '#ccc');
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
beanbag(x, y, color) {
|
|
959
|
+
const c = this.ctx;
|
|
960
|
+
const p = this.iso(x, y, 0);
|
|
961
|
+
c.fillStyle = this.shade(color, -25);
|
|
962
|
+
c.beginPath(); c.ellipse(p.x, p.y - 4, 20, 11, 0, 0, Math.PI * 2); c.fill();
|
|
963
|
+
c.fillStyle = color;
|
|
964
|
+
c.beginPath(); c.ellipse(p.x, p.y - 8, 18, 10, 0, 0, Math.PI * 2); c.fill();
|
|
965
|
+
c.fillStyle = this.shade(color, 20);
|
|
966
|
+
c.beginPath(); c.ellipse(p.x - 3, p.y - 14, 10, 6, -0.2, 0, Math.PI * 2); c.fill();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
books(x0, x1, y) {
|
|
970
|
+
const P = (a, b, c) => this.iso(a, b, c);
|
|
971
|
+
const cols = this.pal.book;
|
|
972
|
+
for (let shelf = 0; shelf < 3; shelf++) {
|
|
973
|
+
const z = 0.12 + shelf * 0.45;
|
|
974
|
+
let bx = x0 + 0.05;
|
|
975
|
+
let i = Math.round(x0 * 7) + shelf * 5;
|
|
976
|
+
while (bx < x1 - 0.1) {
|
|
977
|
+
const bw = 0.07 + ((i * 13) % 5) * 0.015;
|
|
978
|
+
const bh = 0.25 + ((i * 7) % 4) * 0.03;
|
|
979
|
+
if (i % 7 !== 3) this.poly([P(bx, y, z), P(bx + bw, y, z), P(bx + bw, y, z + bh), P(bx, y, z + bh)], cols[i % cols.length]);
|
|
980
|
+
bx += bw + 0.015;
|
|
981
|
+
i++;
|
|
982
|
+
}
|
|
983
|
+
this.poly([P(x0, y, z - 0.02), P(x1, y, z - 0.02), P(x1, y, z), P(x0, y, z)], this.pal.deskR);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
arcade(x, y) {
|
|
988
|
+
this.box(x, y, 0, 0.7, 0.7, 1.75, '#3a2a6a', '#2b1f52', '#221843');
|
|
989
|
+
const P = (a, b, c) => this.iso(a, b, c);
|
|
990
|
+
const hue = (this.t / 20) % 360;
|
|
991
|
+
this.poly([P(x + 0.1, y + 0.7, 1.0), P(x + 0.6, y + 0.7, 1.0), P(x + 0.6, y + 0.7, 1.45), P(x + 0.1, y + 0.7, 1.45)], `hsl(${hue},70%,55%)`);
|
|
992
|
+
const blip = this.iso(x + 0.25 + 0.2 * Math.sin(this.t / 300), y + 0.7, 1.2);
|
|
993
|
+
this.ctx.fillStyle = '#fff'; this.ctx.fillRect(blip.x, blip.y, 3, 3);
|
|
994
|
+
this.poly([P(x, y + 0.7, 1.55), P(x + 0.7, y + 0.7, 1.55), P(x + 0.7, y + 0.7, 1.72), P(x, y + 0.7, 1.72)], '#ffcf4a');
|
|
995
|
+
this.box(x, y + 0.7, 0.8, 0.7, 0.2, 0.08, '#4a3a8a', '#3a2a6a', '#2b1f52');
|
|
996
|
+
const j = this.iso(x + 0.25, y + 0.8, 0.88);
|
|
997
|
+
this.ctx.fillStyle = '#e04a4a'; this.ctx.beginPath(); this.ctx.arc(j.x, j.y - 3, 2.5, 0, Math.PI * 2); this.ctx.fill();
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
floorLamp(x, y) {
|
|
1001
|
+
const c = this.ctx;
|
|
1002
|
+
const b = this.iso(x, y, 0), t = this.iso(x, y, 1.7);
|
|
1003
|
+
if (skyColor().night) {
|
|
1004
|
+
c.fillStyle = 'rgba(255,220,140,.14)';
|
|
1005
|
+
c.beginPath(); c.ellipse(b.x, b.y, 50, 25, 0, 0, Math.PI * 2); c.fill();
|
|
1006
|
+
}
|
|
1007
|
+
c.strokeStyle = '#333'; c.lineWidth = 2;
|
|
1008
|
+
c.beginPath(); c.moveTo(b.x, b.y); c.lineTo(t.x, t.y); c.stroke();
|
|
1009
|
+
c.fillStyle = '#333'; c.beginPath(); c.ellipse(b.x, b.y, 7, 3.5, 0, 0, Math.PI * 2); c.fill();
|
|
1010
|
+
c.fillStyle = '#f3e3b5';
|
|
1011
|
+
c.beginPath(); c.moveTo(t.x - 9, t.y + 4); c.lineTo(t.x + 9, t.y + 4); c.lineTo(t.x + 5, t.y - 8); c.lineTo(t.x - 5, t.y - 8); c.closePath(); c.fill();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
vending(x, y, color) {
|
|
1015
|
+
this.box(x, y, 0, 0.7, 0.75, 1.85, this.shade(color, 15), this.shade(color, -10), color);
|
|
1016
|
+
const P = (a, b, c) => this.iso(a, b, c);
|
|
1017
|
+
const fx = x + 0.7;
|
|
1018
|
+
this.poly([P(fx, y + 0.08, 0.7), P(fx, y + 0.55, 0.7), P(fx, y + 0.55, 1.7), P(fx, y + 0.08, 1.7)], 'rgba(20,30,40,.75)');
|
|
1019
|
+
const cols = ['#f2d16b', '#5cf08a', '#ff8a65', '#81d4fa', '#ce93d8'];
|
|
1020
|
+
for (let r = 0; r < 4; r++) {
|
|
1021
|
+
for (let k = 0; k < 3; k++) {
|
|
1022
|
+
const q = P(fx, y + 0.15 + k * 0.14, 1.55 - r * 0.23);
|
|
1023
|
+
this.ctx.fillStyle = cols[(r * 3 + k + Math.round(y)) % cols.length];
|
|
1024
|
+
this.ctx.fillRect(q.x - 1.5, q.y - 4, 3, 5);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
this.poly([P(fx, y + 0.1, 0.25), P(fx, y + 0.5, 0.25), P(fx, y + 0.5, 0.45), P(fx, y + 0.1, 0.45)], '#222');
|
|
1028
|
+
const glow = P(fx, y + 0.65, 1.2);
|
|
1029
|
+
this.ctx.fillStyle = Math.sin(this.t / 700 + y) > 0 ? '#9ef' : '#4ad';
|
|
1030
|
+
this.ctx.fillRect(glow.x - 1, glow.y - 3, 2, 6);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
foosball(x, y) {
|
|
1034
|
+
for (const [lx, ly] of [[0.1, 0.1], [1.42, 0.1], [0.1, 0.62], [1.42, 0.62]]) this.box(x + lx, y + ly, 0, 0.08, 0.08, 0.6, '#5a4030', '#5a4030', '#4a3428');
|
|
1035
|
+
this.box(x, y, 0.6, 1.6, 0.8, 0.25, '#2f8a4a', '#6e533b', '#5c4431');
|
|
1036
|
+
const P = (a, b, c) => this.iso(a, b, c);
|
|
1037
|
+
const players = this.plan && this.plan.pois.filter((q) => q.pair === 'fb' && q.taken).length === 2;
|
|
1038
|
+
for (let k = 0; k < 6; k++) {
|
|
1039
|
+
const rx = x + 0.2 + k * 0.24;
|
|
1040
|
+
const shift = players ? Math.sin(this.t / 120 + k) * 0.06 : 0;
|
|
1041
|
+
const a = P(rx, y - 0.15, 0.82), b = P(rx, y + 0.95, 0.82);
|
|
1042
|
+
this.ctx.strokeStyle = '#c8ccd0'; this.ctx.lineWidth = 1.2;
|
|
1043
|
+
this.ctx.beginPath(); this.ctx.moveTo(a.x, a.y); this.ctx.lineTo(b.x, b.y); this.ctx.stroke();
|
|
1044
|
+
for (let m = 0; m < 2; m++) {
|
|
1045
|
+
const q = P(rx, y + 0.25 + m * 0.3 + shift, 0.82);
|
|
1046
|
+
this.ctx.fillStyle = k % 2 ? '#c0392b' : '#2f6fb0';
|
|
1047
|
+
this.ctx.fillRect(q.x - 1.5, q.y - 3, 3, 6);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
booth(add, x, y) {
|
|
1053
|
+
const P = (a, b, c) => this.iso(a, b, c);
|
|
1054
|
+
const pal = this.pal;
|
|
1055
|
+
const H = 2.0;
|
|
1056
|
+
// back walls (felt panels), then the stool, then glass front
|
|
1057
|
+
add(x + 0.65 + y + 0.05, () => {
|
|
1058
|
+
this.poly([P(x, y, 0), P(x + 1.3, y, 0), P(x + 1.3, y, H), P(x, y, H)], pal.dark ? '#3c4a5a' : '#9fb3c8');
|
|
1059
|
+
this.poly([P(x, y, 0), P(x, y + 1.3, 0), P(x, y + 1.3, H), P(x, y, H)], pal.dark ? '#34404e' : '#8ea3b8');
|
|
1060
|
+
this.poly([P(x + 0.02, y + 0.02), P(x + 1.28, y + 0.02), P(x + 1.28, y + 1.28), P(x + 0.02, y + 1.28)], pal.dark ? '#2a323b' : '#d8dee5');
|
|
1061
|
+
const lamp = P(x + 0.65, y + 0.65, H - 0.05);
|
|
1062
|
+
this.ctx.fillStyle = 'rgba(255,230,160,.9)';
|
|
1063
|
+
this.ctx.beginPath(); this.ctx.ellipse(lamp.x, lamp.y, 8, 4, 0, 0, Math.PI * 2); this.ctx.fill();
|
|
1064
|
+
this.stool(x + 0.65, y + 0.6, 0.43, '#c96442');
|
|
1065
|
+
});
|
|
1066
|
+
add(x + 1.3 + y + 0.65 + 0.3, () => {
|
|
1067
|
+
this.poly([P(x + 1.3, y, 0), P(x + 1.3, y + 1.3, 0), P(x + 1.3, y + 1.3, H), P(x + 1.3, y, H)], pal.glass, pal.glassEdge);
|
|
1068
|
+
this.poly([P(x, y + 1.3, 0), P(x + 1.3, y + 1.3, 0), P(x + 1.3, y + 1.3, H), P(x, y + 1.3, H)], pal.glass, pal.glassEdge);
|
|
1069
|
+
this.poly([P(x + 0.85, y + 1.3, 0), P(x + 0.87, y + 1.3, 0), P(x + 0.87, y + 1.3, H), P(x + 0.85, y + 1.3, H)], pal.glassEdge);
|
|
1070
|
+
this.box(x - 0.03, y - 0.03, H, 1.36, 1.36, 0.08, pal.dark ? '#4a5868' : '#b5c4d3', pal.trim, pal.trim);
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
desk(dk, p) {
|
|
1075
|
+
const pal = this.pal;
|
|
1076
|
+
const { x, y } = dk;
|
|
1077
|
+
for (const [lx, ly] of [[0.05, 0.05], [1.27, 0.05], [0.05, 0.67], [1.27, 0.67]]) {
|
|
1078
|
+
this.box(x + lx, y + ly, 0, 0.08, 0.08, 0.72, pal.deskL, pal.deskL, pal.deskR);
|
|
1079
|
+
}
|
|
1080
|
+
this.box(x, y, 0.72, 1.4, 0.8, 0.07, pal.deskTop, pal.deskL, pal.deskR);
|
|
1081
|
+
const worker = p && p.mode === 'work' ? p : null;
|
|
1082
|
+
if (dk.side === 's') {
|
|
1083
|
+
const mx = x + 0.42, my = y + 0.1, mz = 0.79;
|
|
1084
|
+
this.box(mx + 0.22, my + 0.04, mz, 0.1, 0.1, 0.18, pal.monitor, pal.monitor, pal.monitor);
|
|
1085
|
+
this.box(mx, my, mz + 0.18, 0.56, 0.08, 0.42, pal.monitor, pal.monitor, this.shade(pal.monitor, 10));
|
|
1086
|
+
this.screen(mx + 0.04, my + 0.08, mz + 0.22, 0.48, 0.34, worker);
|
|
1087
|
+
this.box(x + 0.48, y + 0.48, 0.79, 0.44, 0.16, 0.025, '#d8d8d8', '#aaa', '#999');
|
|
1088
|
+
} else {
|
|
1089
|
+
// We see this monitor from behind; a status LED on its back.
|
|
1090
|
+
const mx = x + 0.42, my = y + 0.6, mz = 0.79;
|
|
1091
|
+
this.box(x + 0.48, y + 0.16, 0.79, 0.44, 0.16, 0.025, '#d8d8d8', '#aaa', '#999');
|
|
1092
|
+
this.box(mx + 0.22, my - 0.02, mz, 0.1, 0.1, 0.18, pal.monitor, pal.monitor, pal.monitor);
|
|
1093
|
+
this.box(mx, my, mz + 0.18, 0.56, 0.08, 0.42, this.shade(pal.monitor, 15), this.shade(pal.monitor, 8), this.shade(pal.monitor, 12));
|
|
1094
|
+
if (worker) {
|
|
1095
|
+
const g = this.iso(mx + 0.28, my + 0.08, mz + 0.5);
|
|
1096
|
+
this.ctx.fillStyle = STATUS_COLOR[worker.s.status] || '#888';
|
|
1097
|
+
this.ctx.globalAlpha = 0.6 + 0.4 * Math.sin(this.t / 400 + worker.phase);
|
|
1098
|
+
this.ctx.beginPath(); this.ctx.arc(g.x, g.y, 1.8, 0, Math.PI * 2); this.ctx.fill();
|
|
1099
|
+
this.ctx.globalAlpha = 1;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
// personal touches, stable per desk
|
|
1103
|
+
const h = hash(`${x.toFixed(1)},${y.toFixed(1)}`);
|
|
1104
|
+
if (h % 3 === 0) this.box(x + 1.12, y + 0.3, 0.79, 0.12, 0.12, 0.13, '#f2f2f2', '#d0d0d0', '#bbb');
|
|
1105
|
+
if (h % 5 === 1) {
|
|
1106
|
+
this.box(x + 0.14, y + 0.24, 0.79, 0.12, 0.12, 0.08, pal.pot, pal.pot, this.shade(pal.pot, -15));
|
|
1107
|
+
const q = this.iso(x + 0.2, y + 0.3, 0.87);
|
|
1108
|
+
this.ctx.fillStyle = pal.plant;
|
|
1109
|
+
this.ctx.beginPath(); this.ctx.ellipse(q.x, q.y - 4, 4, 6, 0, 0, Math.PI * 2); this.ctx.fill();
|
|
1110
|
+
}
|
|
1111
|
+
if (h % 4 === 2) this.box(x + 0.12, y + 0.45, 0.79, 0.22, 0.15, 0.05, '#f2d16b', '#d9b84f', '#c4a33f');
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
screen(x, y, z, w, h, p) {
|
|
1115
|
+
const P = (a, b, cc) => this.iso(a, b, cc);
|
|
1116
|
+
const st = p ? p.s.status : 'off';
|
|
1117
|
+
const base = { thinking: '#3a3160', tool: '#14324f', typing: '#123d2e', waiting: '#4d3510', error: '#4a1612' }[st] || this.pal.screenOff;
|
|
1118
|
+
const corners = [P(x, y, z), P(x + w, y, z), P(x + w, y, z + h), P(x, y, z + h)];
|
|
1119
|
+
this.poly(corners, base);
|
|
1120
|
+
if (!p) return;
|
|
1121
|
+
const c = this.ctx;
|
|
1122
|
+
c.save();
|
|
1123
|
+
c.beginPath();
|
|
1124
|
+
corners.forEach((q, i) => (i ? c.lineTo(q.x, q.y) : c.moveTo(q.x, q.y)));
|
|
1125
|
+
c.closePath();
|
|
1126
|
+
c.clip();
|
|
1127
|
+
const t = this.t / 1000;
|
|
1128
|
+
const col = STATUS_COLOR[st];
|
|
1129
|
+
if (st === 'tool' || st === 'typing') {
|
|
1130
|
+
const scroll = (t * (st === 'tool' ? 2.2 : 1.2) + p.phase) % 1;
|
|
1131
|
+
for (let i = -1; i < 6; i++) {
|
|
1132
|
+
const zz = z + h - 0.05 - ((i + scroll) * h) / 5;
|
|
1133
|
+
const len = 0.12 + ((p.h >> ((i + 8) & 15)) % 7) / 20;
|
|
1134
|
+
const a = P(x + 0.05, y, zz), b = P(x + 0.05 + Math.min(len, w - 0.1), y, zz);
|
|
1135
|
+
c.strokeStyle = i % 3 === 0 ? col : 'rgba(255,255,255,.55)';
|
|
1136
|
+
c.lineWidth = 1.5;
|
|
1137
|
+
c.beginPath(); c.moveTo(a.x, a.y); c.lineTo(b.x, b.y); c.stroke();
|
|
1138
|
+
}
|
|
1139
|
+
} else {
|
|
1140
|
+
const m = P(x + w / 2, y, z + h / 2);
|
|
1141
|
+
c.fillStyle = col;
|
|
1142
|
+
c.globalAlpha = st === 'thinking' ? 0.5 + 0.4 * Math.sin(t * 3 + p.phase) : 0.9;
|
|
1143
|
+
c.font = '700 10px ui-sans-serif, system-ui, sans-serif';
|
|
1144
|
+
c.textAlign = 'center';
|
|
1145
|
+
c.textBaseline = 'middle';
|
|
1146
|
+
c.fillText({ thinking: '…', waiting: '?', error: '!' }[st] || '', m.x, m.y);
|
|
1147
|
+
}
|
|
1148
|
+
c.restore();
|
|
1149
|
+
c.save();
|
|
1150
|
+
c.globalAlpha = 0.18;
|
|
1151
|
+
const g = P(x + w / 2, y + 0.35, 0.8);
|
|
1152
|
+
c.fillStyle = col;
|
|
1153
|
+
c.beginPath(); c.ellipse(g.x, g.y, 18, 7, 0, 0, Math.PI * 2); c.fill();
|
|
1154
|
+
c.restore();
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
chair(dk, p) {
|
|
1158
|
+
const pal = this.pal;
|
|
1159
|
+
const sx = dk.seat.x - 0.25, sy = dk.seat.y - 0.22;
|
|
1160
|
+
const seated = p && !p.path.length && p.pose === 'sit' && Math.hypot(p.x - dk.seat.x, p.y - dk.seat.y) < 0.05;
|
|
1161
|
+
const ox = seated ? 0 : 0.12, oy = seated ? 0 : dk.side === 's' ? 0.18 : -0.18;
|
|
1162
|
+
this.box(sx + 0.22 + ox, sy + 0.19 + oy, 0, 0.06, 0.06, 0.32, '#222', '#222', '#111');
|
|
1163
|
+
if (dk.side === 'n') this.box(sx + ox, sy + oy - 0.05, 0.4, 0.5, 0.07, 0.55, this.shade(pal.chair, 15), pal.chair, this.shade(pal.chair, -10));
|
|
1164
|
+
this.box(sx + ox, sy + oy, 0.32, 0.5, 0.45, 0.08, this.shade(pal.chair, 15), pal.chair, this.shade(pal.chair, -10));
|
|
1165
|
+
if (dk.side === 's' && !seated) this.box(sx + ox, sy + oy + 0.4, 0.4, 0.5, 0.07, 0.5, this.shade(pal.chair, 15), pal.chair, this.shade(pal.chair, -10));
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// ------------------------------------------------------------ people
|
|
1169
|
+
|
|
1170
|
+
stateOf(p) {
|
|
1171
|
+
const s = p.s || {};
|
|
1172
|
+
if (p.path.length) return { pose: 'walk', face: p.walkFace, act: null, sitZ: 0 };
|
|
1173
|
+
if (p.mode === 'work' && p.desk) {
|
|
1174
|
+
const face = p.desk.side === 'n' ? 'front' : 'back';
|
|
1175
|
+
if (s.status === 'waiting' || s.status === 'error') return { pose: 'stand', face, act: 'wave', sitZ: 0 };
|
|
1176
|
+
if (p.desk.pose === 'stand') return { pose: 'stand', face, act: 'station', sitZ: 0 };
|
|
1177
|
+
return { pose: 'sit', face, act: s.status === 'tool' || s.status === 'typing' ? 'type' : 'think', sitZ: 0.34 };
|
|
1178
|
+
}
|
|
1179
|
+
if (p.mode === 'wander' && p.poi) {
|
|
1180
|
+
const q = p.poi;
|
|
1181
|
+
return { pose: q.sit ? 'sit' : 'stand', face: q.face === 'side' ? 'front' : q.face, act: q.act, sitZ: q.sit || 0 };
|
|
1182
|
+
}
|
|
1183
|
+
return { pose: 'stand', face: 'front', act: null, sitZ: 0 };
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
person(p) {
|
|
1187
|
+
const c = this.ctx;
|
|
1188
|
+
const t = this.t / 1000;
|
|
1189
|
+
const s = p.s || {};
|
|
1190
|
+
const st = this.stateOf(p);
|
|
1191
|
+
p.pose = st.pose;
|
|
1192
|
+
const moving = st.pose === 'walk';
|
|
1193
|
+
const seated = st.pose === 'sit';
|
|
1194
|
+
const front = st.face !== 'back';
|
|
1195
|
+
const b = this.iso(p.x, p.y, st.sitZ);
|
|
1196
|
+
const step = moving ? Math.sin(t * 12 + p.phase) : 0;
|
|
1197
|
+
const bob = moving ? Math.abs(step) * 1.5 : 0;
|
|
1198
|
+
|
|
1199
|
+
const fb = this.iso(p.x, p.y, 0);
|
|
1200
|
+
c.fillStyle = this.pal.shadow;
|
|
1201
|
+
c.beginPath(); c.ellipse(fb.x, fb.y, 10, 5, 0, 0, Math.PI * 2); c.fill();
|
|
1202
|
+
if (this.hoverKey === p.key) {
|
|
1203
|
+
c.strokeStyle = STATUS_COLOR[s.status] || '#888';
|
|
1204
|
+
c.lineWidth = 2;
|
|
1205
|
+
c.beginPath(); c.ellipse(fb.x, fb.y, 14, 7, 0, 0, Math.PI * 2); c.stroke();
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
let x = b.x;
|
|
1209
|
+
const y = b.y - bob;
|
|
1210
|
+
if (st.act === 'pingpong') x += Math.sin(t * 7 + p.phase) * 4;
|
|
1211
|
+
if (st.act === 'foosball') x += Math.sin(t * 5 + p.phase) * 1.5;
|
|
1212
|
+
const sc = s.subagent ? 0.8 : 1;
|
|
1213
|
+
c.save();
|
|
1214
|
+
c.translate(x, y);
|
|
1215
|
+
c.scale(sc, sc);
|
|
1216
|
+
|
|
1217
|
+
c.fillStyle = p.pants;
|
|
1218
|
+
if (seated) {
|
|
1219
|
+
c.fillRect(-5, -4, 4, 5);
|
|
1220
|
+
c.fillRect(1, -4, 4, 5);
|
|
1221
|
+
} else {
|
|
1222
|
+
c.fillRect(-5, -10, 4, 10 + step * 2);
|
|
1223
|
+
c.fillRect(1, -10, 4, 10 - step * 2);
|
|
1224
|
+
c.fillStyle = '#2a2a2a';
|
|
1225
|
+
c.fillRect(-5, -1 + step, 4, 2);
|
|
1226
|
+
c.fillRect(1, -1 - step, 4, 2);
|
|
1227
|
+
}
|
|
1228
|
+
const hip = seated ? -4 : -10;
|
|
1229
|
+
this.roundRect(-7, hip - 13, 14, 14, 3, p.shirt);
|
|
1230
|
+
const arm = this.shade(p.shirt, -18);
|
|
1231
|
+
const hand = (hx, hy) => { c.fillStyle = p.skin; c.beginPath(); c.arc(hx, hy, 2.2, 0, Math.PI * 2); c.fill(); };
|
|
1232
|
+
c.fillStyle = arm;
|
|
1233
|
+
const sip = Math.sin(t * 0.9 + p.phase) > 0.6;
|
|
1234
|
+
switch (st.act) {
|
|
1235
|
+
case 'wave': {
|
|
1236
|
+
const wave = Math.sin(t * 8 + p.phase) * 2;
|
|
1237
|
+
c.fillRect(6, hip - 26, 3, 14);
|
|
1238
|
+
hand(7.5 + wave * 0.5, hip - 27);
|
|
1239
|
+
c.fillStyle = arm; c.fillRect(-9, hip - 11, 3, 10);
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
case 'type': {
|
|
1243
|
+
const k1 = Math.sin(t * 18 + p.phase) > 0 ? 1 : 0;
|
|
1244
|
+
c.fillRect(-9, hip - 11 - k1, 3, 8);
|
|
1245
|
+
c.fillRect(6, hip - 11 - (1 - k1), 3, 8);
|
|
1246
|
+
break;
|
|
1247
|
+
}
|
|
1248
|
+
case 'coffee':
|
|
1249
|
+
case 'water': {
|
|
1250
|
+
c.fillRect(-9, hip - 11, 3, 10);
|
|
1251
|
+
c.fillRect(6, sip ? hip - 15 : hip - 11, 3, sip ? 7 : 10);
|
|
1252
|
+
const cupY = sip && front ? hip - 19 : hip - 4;
|
|
1253
|
+
c.fillStyle = st.act === 'water' ? 'rgba(170,215,255,.95)' : '#fafafa';
|
|
1254
|
+
c.fillRect(6, cupY, 5, 6);
|
|
1255
|
+
if (st.act === 'coffee') { c.fillStyle = '#6b3f22'; c.fillRect(7, cupY, 3, 1.5); }
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1258
|
+
case 'read': {
|
|
1259
|
+
c.fillRect(-8, hip - 11, 3, 7);
|
|
1260
|
+
c.fillRect(5, hip - 11, 3, 7);
|
|
1261
|
+
if (front) { c.fillStyle = pick(this.pal.book, p.h, 5); c.fillRect(-7, hip - 12, 14, 9); c.fillStyle = '#fff'; c.fillRect(-0.5, hip - 12, 1, 9); }
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
case 'whiteboard':
|
|
1265
|
+
case 'arcade': {
|
|
1266
|
+
const k = Math.sin(t * (st.act === 'arcade' ? 14 : 3) + p.phase);
|
|
1267
|
+
c.fillRect(-9, hip - 11, 3, 10);
|
|
1268
|
+
c.fillRect(6, hip - 20 + k * 2, 3, 12);
|
|
1269
|
+
hand(7.5, hip - 21 + k * 2);
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
case 'pingpong': {
|
|
1273
|
+
c.fillRect(-9, hip - 11, 3, 10);
|
|
1274
|
+
c.fillRect(6, hip - 14, 3, 9);
|
|
1275
|
+
c.fillStyle = '#c0392b';
|
|
1276
|
+
c.beginPath(); c.arc(10, hip - 16, 4, 0, Math.PI * 2); c.fill();
|
|
1277
|
+
break;
|
|
1278
|
+
}
|
|
1279
|
+
case 'foosball': {
|
|
1280
|
+
const k1 = Math.sin(t * 16 + p.phase) * 1.5;
|
|
1281
|
+
c.fillRect(-9, hip - 12 + k1, 3, 7);
|
|
1282
|
+
c.fillRect(6, hip - 12 - k1, 3, 7);
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
case 'phone': {
|
|
1286
|
+
c.fillRect(-9, hip - 11, 3, 10);
|
|
1287
|
+
c.fillRect(5, hip - 22, 3, 12);
|
|
1288
|
+
c.fillStyle = '#222'; c.fillRect(5, hip - 25, 3, 6);
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
case 'nap': {
|
|
1292
|
+
c.fillRect(-9, hip - 13, 3, 6);
|
|
1293
|
+
c.fillRect(6, hip - 13, 3, 6);
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
default: {
|
|
1297
|
+
const sw = moving ? step * 2 : 0;
|
|
1298
|
+
c.fillRect(-9, hip - 11 + sw, 3, 10);
|
|
1299
|
+
c.fillRect(6, hip - 11 - sw, 3, 10);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const napping = st.act === 'nap';
|
|
1304
|
+
const hy = hip - 20 + (napping ? 7 : 0) + (st.act === 'think' ? Math.sin(t * 1.5 + p.phase) * 0.6 : 0);
|
|
1305
|
+
c.fillStyle = p.skin;
|
|
1306
|
+
c.beginPath(); c.arc(0, hy, 7, 0, Math.PI * 2); c.fill();
|
|
1307
|
+
c.fillStyle = p.hair;
|
|
1308
|
+
if (front && !napping) {
|
|
1309
|
+
c.beginPath(); c.arc(0, hy - 1.5, 7.3, Math.PI * 1.05, Math.PI * 1.95); c.fill();
|
|
1310
|
+
if (p.hairStyle === 1) { c.fillRect(-7.3, hy - 2, 2.5, 8); c.fillRect(4.8, hy - 2, 2.5, 8); }
|
|
1311
|
+
if (p.hairStyle === 2) { c.beginPath(); c.arc(0, hy - 8, 3.2, 0, Math.PI * 2); c.fill(); }
|
|
1312
|
+
c.fillStyle = '#2a2018';
|
|
1313
|
+
const sleepy = s.status === 'idle';
|
|
1314
|
+
const blink = Math.sin(t * 0.7 + p.phase * 3) > 0.985;
|
|
1315
|
+
c.fillRect(-3, hy - 0.5, 1.6, blink || sleepy ? 0.7 : 2);
|
|
1316
|
+
c.fillRect(1.6, hy - 0.5, 1.6, blink || sleepy ? 0.7 : 2);
|
|
1317
|
+
c.strokeStyle = '#2a2018'; c.lineWidth = 1;
|
|
1318
|
+
if (p.mode === 'wander' && !sleepy) {
|
|
1319
|
+
c.beginPath(); c.arc(0, hy + 2.2, 2, 0.15 * Math.PI, 0.85 * Math.PI); c.stroke();
|
|
1320
|
+
} else if (s.status === 'error' && p.mode === 'work') {
|
|
1321
|
+
c.beginPath(); c.arc(0, hy + 4.5, 2, 1.15 * Math.PI, 1.85 * Math.PI); c.stroke();
|
|
1322
|
+
}
|
|
1323
|
+
} else {
|
|
1324
|
+
c.beginPath(); c.arc(0, hy - 0.5, 7.2, 0, Math.PI * 2); c.fill();
|
|
1325
|
+
if (p.hairStyle === 2) { c.beginPath(); c.arc(0, hy - 7, 3.2, 0, Math.PI * 2); c.fill(); }
|
|
1326
|
+
if (p.hairStyle === 1) c.fillRect(-6, hy, 12, 8);
|
|
1327
|
+
}
|
|
1328
|
+
if (st.act === 'arcade' || st.act === 'beanbag') {
|
|
1329
|
+
c.strokeStyle = '#222'; c.lineWidth = 2;
|
|
1330
|
+
c.beginPath(); c.arc(0, hy, 8, Math.PI * 1.1, Math.PI * 1.9); c.stroke();
|
|
1331
|
+
}
|
|
1332
|
+
c.restore();
|
|
1333
|
+
|
|
1334
|
+
const headTop = y + (hy - 8) * sc;
|
|
1335
|
+
this.hit.push({ key: p.key, x0: x - 13, x1: x + 13, y0: headTop - 22, y1: fb.y + 4 });
|
|
1336
|
+
return { x, y: headTop, act: st.act };
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
bubble(p, at) {
|
|
1340
|
+
const c = this.ctx;
|
|
1341
|
+
const t = this.t / 1000;
|
|
1342
|
+
const s = p.s || {};
|
|
1343
|
+
const st = s.status;
|
|
1344
|
+
const pal = this.pal;
|
|
1345
|
+
const x = at.x, y = at.y - 6;
|
|
1346
|
+
c.save();
|
|
1347
|
+
c.textAlign = 'center';
|
|
1348
|
+
c.textBaseline = 'middle';
|
|
1349
|
+
const font = (w, px) => `${w} ${px}px ui-sans-serif, system-ui, -apple-system, "PingFang TC", "Apple Color Emoji", sans-serif`;
|
|
1350
|
+
c.font = font(600, 10);
|
|
1351
|
+
|
|
1352
|
+
const badge = this.showAgent && AGENT_BADGES[s.agent];
|
|
1353
|
+
const pill = (text, fg, bg, yy, alpha = 1, identify = false) => {
|
|
1354
|
+
c.globalAlpha = alpha;
|
|
1355
|
+
let tx = text;
|
|
1356
|
+
while (c.measureText(tx).width > 118 && tx.length > 2) tx = tx.slice(0, -2) + '…';
|
|
1357
|
+
const extra = badge && identify ? 26 : 0;
|
|
1358
|
+
const w = c.measureText(tx).width + 12 + extra;
|
|
1359
|
+
const height = extra ? 22 : 16;
|
|
1360
|
+
this.roundRect(x - w / 2, yy - height / 2, w, height, 8, bg);
|
|
1361
|
+
c.fillStyle = fg;
|
|
1362
|
+
c.fillText(tx, x + extra / 2, yy + 0.5);
|
|
1363
|
+
if (extra) this.agentBadge(badge, x - w / 2 + 13, yy);
|
|
1364
|
+
c.globalAlpha = 1;
|
|
1365
|
+
return yy - 8;
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
let top = y;
|
|
1369
|
+
if (p.mode === 'work' && !p.path.length) {
|
|
1370
|
+
const col = STATUS_COLOR[st] || '#888';
|
|
1371
|
+
if (st === 'thinking') {
|
|
1372
|
+
c.fillStyle = pal.bubble;
|
|
1373
|
+
c.beginPath(); c.arc(x + 6, y - 2, 2, 0, Math.PI * 2); c.fill();
|
|
1374
|
+
c.beginPath(); c.arc(x + 10, y - 7, 3, 0, Math.PI * 2); c.fill();
|
|
1375
|
+
this.roundRect(x - 2, y - 26, 32, 16, 8, pal.bubble);
|
|
1376
|
+
for (let i = 0; i < 3; i++) {
|
|
1377
|
+
c.fillStyle = col;
|
|
1378
|
+
c.globalAlpha = 0.3 + 0.7 * Math.max(0, Math.sin(t * 5 - i * 0.7));
|
|
1379
|
+
c.beginPath(); c.arc(x + 6 + i * 8, y - 18, 2.4, 0, Math.PI * 2); c.fill();
|
|
1380
|
+
}
|
|
1381
|
+
c.globalAlpha = 1;
|
|
1382
|
+
top = y - 26;
|
|
1383
|
+
} else if (st === 'typing') {
|
|
1384
|
+
this.roundRect(x - 2, y - 24, 34, 16, 5, col);
|
|
1385
|
+
c.beginPath(); c.moveTo(x + 2, y - 9); c.lineTo(x - 2, y - 3); c.lineTo(x + 8, y - 9); c.fillStyle = col; c.fill();
|
|
1386
|
+
const n = Math.floor(t * 4) % 4;
|
|
1387
|
+
c.fillStyle = '#fff';
|
|
1388
|
+
for (let i = 0; i < n; i++) { c.beginPath(); c.arc(x + 7 + i * 8, y - 16, 2, 0, Math.PI * 2); c.fill(); }
|
|
1389
|
+
top = y - 24;
|
|
1390
|
+
} else if (st === 'tool') {
|
|
1391
|
+
const tool = (s.activity || 'tool').split(/[:\s]/)[0];
|
|
1392
|
+
top = pill('⚙ ' + tool, '#fff', col, y - 12);
|
|
1393
|
+
} else if (st === 'waiting' || st === 'error') {
|
|
1394
|
+
const jump = Math.abs(Math.sin(t * 4)) * 4;
|
|
1395
|
+
c.fillStyle = col;
|
|
1396
|
+
c.beginPath(); c.arc(x, y - 12 - jump, 9, 0, Math.PI * 2); c.fill();
|
|
1397
|
+
c.fillStyle = '#fff';
|
|
1398
|
+
c.font = font(800, 12);
|
|
1399
|
+
c.fillText('!', x, y - 11.5 - jump);
|
|
1400
|
+
c.font = font(600, 10);
|
|
1401
|
+
top = y - 22 - jump;
|
|
1402
|
+
}
|
|
1403
|
+
} else if (p.mode === 'wander') {
|
|
1404
|
+
const justDone = st === 'done' && Date.now() - (s.lastEventAt || 0) < 3 * 60_000;
|
|
1405
|
+
const dozing = at.act === 'nap' || (st === 'idle' && (at.act === 'couch' || at.act === 'beanbag' || (p.poi && p.poi.sleepy)));
|
|
1406
|
+
if (justDone) {
|
|
1407
|
+
c.fillStyle = STATUS_COLOR.done;
|
|
1408
|
+
c.beginPath(); c.arc(x, y - 10, 7, 0, Math.PI * 2); c.fill();
|
|
1409
|
+
c.fillStyle = '#fff'; c.font = font(800, 9); c.fillText('✓', x, y - 9.5); c.font = font(600, 10);
|
|
1410
|
+
top = y - 18;
|
|
1411
|
+
} else if (dozing && !p.path.length) {
|
|
1412
|
+
c.fillStyle = STATUS_COLOR.idle;
|
|
1413
|
+
c.font = font(700, 10);
|
|
1414
|
+
for (let i = 0; i < 3; i++) {
|
|
1415
|
+
const k = (t * 0.6 + i / 3) % 1;
|
|
1416
|
+
c.globalAlpha = 1 - k;
|
|
1417
|
+
c.fillText('z', x + 6 + k * 10, y - 4 - k * 18);
|
|
1418
|
+
}
|
|
1419
|
+
c.globalAlpha = 1;
|
|
1420
|
+
c.font = font(600, 10);
|
|
1421
|
+
top = y - 20;
|
|
1422
|
+
} else if (at.act && (ACT_EMOJI[at.act] || (p.poi && p.poi.emoji)) && !p.path.length) {
|
|
1423
|
+
const cycle = (t + p.phase * 3) % 9;
|
|
1424
|
+
if (cycle < 2.2) {
|
|
1425
|
+
c.globalAlpha = Math.min(1, cycle * 3, (2.2 - cycle) * 3);
|
|
1426
|
+
c.font = font(400, 13);
|
|
1427
|
+
c.fillText((p.poi && p.poi.emoji) || ACT_EMOJI[at.act], x, y - 10);
|
|
1428
|
+
c.globalAlpha = 1;
|
|
1429
|
+
c.font = font(600, 10);
|
|
1430
|
+
top = y - 18;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const hover = this.hoverKey === p.key;
|
|
1436
|
+
if (!s.subagent || hover) {
|
|
1437
|
+
pill(this.label(s), pal.text, pal.tagBg, top - 11, p.mode === 'work' || hover ? 1 : 0.7, true);
|
|
1438
|
+
} else if (badge) {
|
|
1439
|
+
this.agentBadge(badge, x, top - 11);
|
|
1440
|
+
}
|
|
1441
|
+
c.restore();
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
agentBadge(badge, x, y) {
|
|
1445
|
+
const c = this.ctx;
|
|
1446
|
+
c.save();
|
|
1447
|
+
c.globalAlpha = 1;
|
|
1448
|
+
if (badge.image.complete && badge.image.naturalWidth > 0) {
|
|
1449
|
+
if (badge.mono && this.pal.dark) c.filter = 'invert(1)';
|
|
1450
|
+
c.imageSmoothingEnabled = true;
|
|
1451
|
+
c.imageSmoothingQuality = 'high';
|
|
1452
|
+
c.drawImage(badge.image, x - 9, y - 9, 18, 18);
|
|
1453
|
+
} else {
|
|
1454
|
+
c.fillStyle = this.pal.text;
|
|
1455
|
+
c.font = '800 10px ui-sans-serif, system-ui, sans-serif';
|
|
1456
|
+
c.textAlign = 'center';
|
|
1457
|
+
c.textBaseline = 'middle';
|
|
1458
|
+
c.fillText(badge.short, x, y + 0.5);
|
|
1459
|
+
}
|
|
1460
|
+
c.restore();
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
moveCat(dt, now) {
|
|
1464
|
+
const k = this.cat;
|
|
1465
|
+
if (!k.path.length && now > k.until) {
|
|
1466
|
+
const { W, D, grid } = this.plan;
|
|
1467
|
+
let tx, ty, tries = 0;
|
|
1468
|
+
do { tx = rand(0.6, W - 0.6); ty = rand(1, D - 0.6); tries++; } while (grid.blocked(tx, ty) && tries < 30);
|
|
1469
|
+
k.path = grid.path(k.x, k.y, tx, ty);
|
|
1470
|
+
k.until = Infinity;
|
|
1471
|
+
}
|
|
1472
|
+
if (k.path.length) {
|
|
1473
|
+
const n = k.path[0];
|
|
1474
|
+
const dx = n.x - k.x, dy = n.y - k.y, d = Math.hypot(dx, dy), v = 1.2 * dt;
|
|
1475
|
+
if (d > 0.001) k.face = dx - dy >= 0 ? 1 : -1;
|
|
1476
|
+
if (d <= v) { k.x = n.x; k.y = n.y; k.path.shift(); if (!k.path.length) k.until = now + rand(4000, 12000); }
|
|
1477
|
+
else { k.x += (dx / d) * v; k.y += (dy / d) * v; }
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
drawCat() {
|
|
1482
|
+
const k = this.cat;
|
|
1483
|
+
const c = this.ctx;
|
|
1484
|
+
const p = this.iso(k.x, k.y, 0);
|
|
1485
|
+
const t = this.t / 1000;
|
|
1486
|
+
const walking = k.path.length > 0;
|
|
1487
|
+
c.save();
|
|
1488
|
+
c.translate(p.x, p.y);
|
|
1489
|
+
c.scale(k.face, 1);
|
|
1490
|
+
c.fillStyle = this.pal.shadow;
|
|
1491
|
+
c.beginPath(); c.ellipse(0, 0, 8, 3, 0, 0, Math.PI * 2); c.fill();
|
|
1492
|
+
const col = '#e09a4a';
|
|
1493
|
+
c.fillStyle = col;
|
|
1494
|
+
if (walking) {
|
|
1495
|
+
const s = Math.sin(t * 14) * 1.5;
|
|
1496
|
+
c.fillRect(-5, -4, 2, 4 + s); c.fillRect(3, -4, 2, 4 - s);
|
|
1497
|
+
}
|
|
1498
|
+
c.beginPath(); c.ellipse(0, walking ? -6 : -4, 7, walking ? 4 : 5, 0, 0, Math.PI * 2); c.fill();
|
|
1499
|
+
c.beginPath(); c.arc(6, walking ? -10 : -9, 3.6, 0, Math.PI * 2); c.fill();
|
|
1500
|
+
c.beginPath(); c.moveTo(4, -12); c.lineTo(5, -16); c.lineTo(7, -12.5); c.fill();
|
|
1501
|
+
c.beginPath(); c.moveTo(7, -12.5); c.lineTo(9, -15.5); c.lineTo(9.5, -11); c.fill();
|
|
1502
|
+
c.strokeStyle = col; c.lineWidth = 2; c.lineCap = 'round';
|
|
1503
|
+
c.beginPath(); c.moveTo(-6, -6); c.quadraticCurveTo(-12, -8 + Math.sin(t * 3) * 3, -10, -14); c.stroke();
|
|
1504
|
+
c.fillStyle = '#2a2018';
|
|
1505
|
+
c.fillRect(7, -10.5, 1.2, walking ? 1.2 : 0.6);
|
|
1506
|
+
c.restore();
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
pingPongBall() {
|
|
1510
|
+
const players = this.plan.pois
|
|
1511
|
+
.filter((q) => q.pair === 'pp' && q.taken)
|
|
1512
|
+
.map((q) => this.people.get(q.taken))
|
|
1513
|
+
.filter((p) => p && p.poi && p.poi.pair === 'pp' && !p.path.length);
|
|
1514
|
+
if (players.length < 2) return;
|
|
1515
|
+
const pp = this.plan.pp;
|
|
1516
|
+
const k = (this.t / 900) % 2;
|
|
1517
|
+
const f = k < 1 ? k : 2 - k;
|
|
1518
|
+
const x = pp.x0 + 0.3 + f * (pp.x1 - pp.x0 - 0.6);
|
|
1519
|
+
const z = 0.8 + Math.abs(Math.sin(f * Math.PI * 2)) * 0.45;
|
|
1520
|
+
const c = this.ctx;
|
|
1521
|
+
const sh = this.iso(x, pp.y, 0.77);
|
|
1522
|
+
c.fillStyle = 'rgba(0,0,0,.25)';
|
|
1523
|
+
c.beginPath(); c.ellipse(sh.x, sh.y, 2.5, 1.2, 0, 0, Math.PI * 2); c.fill();
|
|
1524
|
+
const p = this.iso(x, pp.y, z);
|
|
1525
|
+
c.fillStyle = '#fff';
|
|
1526
|
+
c.beginPath(); c.arc(p.x, p.y, 2.2, 0, Math.PI * 2); c.fill();
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// ------------------------------------------------------------ frame
|
|
1530
|
+
|
|
1531
|
+
frame(now) {
|
|
1532
|
+
const dt = Math.min(0.05, (now - this.last) / 1000);
|
|
1533
|
+
this.last = now;
|
|
1534
|
+
this.t = now;
|
|
1535
|
+
if (!this.visible) return;
|
|
1536
|
+
this.step(dt);
|
|
1537
|
+
if (this.draw() !== false) {
|
|
1538
|
+
if (!this.first && ++this.framesShown === 3) this.markReady(true);
|
|
1539
|
+
// Capture immediately: a WebGL drawing buffer may be cleared after
|
|
1540
|
+
// this task. The overlay and the 3D canvas must be copied together.
|
|
1541
|
+
if (this.onFrame) this.onFrame(this);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// Advance people and the cat; shared by the 2D and 3D renderers.
|
|
1546
|
+
step(dt) {
|
|
1547
|
+
const wall = Date.now();
|
|
1548
|
+
for (const [k, p] of this.people) {
|
|
1549
|
+
if (p.mode === 'wander') {
|
|
1550
|
+
if (p.poi && !p.path.length && p.until === Infinity) p.until = wall + p.stay;
|
|
1551
|
+
this.wander(p, wall);
|
|
1552
|
+
}
|
|
1553
|
+
if (p.path.length) {
|
|
1554
|
+
const n = p.path[0];
|
|
1555
|
+
const dx = n.x - p.x, dy = n.y - p.y, d = Math.hypot(dx, dy), v = SPEED * dt;
|
|
1556
|
+
if (d > 0.001) p.walkFace = dx + dy >= 0 ? 'front' : 'back';
|
|
1557
|
+
if (d <= v) { p.x = n.x; p.y = n.y; p.path.shift(); }
|
|
1558
|
+
else { p.x += (dx / d) * v; p.y += (dy / d) * v; }
|
|
1559
|
+
} else if (p.leaving) {
|
|
1560
|
+
this.people.delete(k);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
this.moveCat(dt, wall);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
draw() {
|
|
1567
|
+
const c = this.ctx;
|
|
1568
|
+
c.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
|
|
1569
|
+
c.clearRect(0, 0, this.cw, this.ch);
|
|
1570
|
+
this.fitCamera();
|
|
1571
|
+
c.translate(this.cam.ox, this.cam.oy);
|
|
1572
|
+
c.scale(this.cam.zoom, this.cam.zoom);
|
|
1573
|
+
|
|
1574
|
+
this.drawRoom();
|
|
1575
|
+
const occupant = new Map();
|
|
1576
|
+
for (const p of this.people.values()) {
|
|
1577
|
+
if (p.mode === 'work' && p.desk) occupant.set(p.desk, p);
|
|
1578
|
+
else if (p.napDesk) occupant.set(p.napDesk, p);
|
|
1579
|
+
}
|
|
1580
|
+
const items = [...this.plan.items];
|
|
1581
|
+
for (const dk of this.plan.desks) {
|
|
1582
|
+
const p = occupant.get(dk);
|
|
1583
|
+
items.push({ depth: dk.x + 0.7 + dk.y + 0.4, draw: () => this.desk(dk, p) });
|
|
1584
|
+
items.push({ depth: dk.seat.x + dk.seat.y - 0.06, draw: () => this.chair(dk, p) });
|
|
1585
|
+
}
|
|
1586
|
+
for (const p of this.people.values()) items.push({ depth: p.x + p.y + 0.01, person: p });
|
|
1587
|
+
items.push({ depth: this.cat.x + this.cat.y, draw: () => this.drawCat() });
|
|
1588
|
+
items.sort((a, b) => a.depth - b.depth);
|
|
1589
|
+
|
|
1590
|
+
this.hit = [];
|
|
1591
|
+
const heads = [];
|
|
1592
|
+
for (const it of items) {
|
|
1593
|
+
if (it.person) heads.push([it.person, this.person(it.person)]);
|
|
1594
|
+
else it.draw();
|
|
1595
|
+
}
|
|
1596
|
+
this.pingPongBall();
|
|
1597
|
+
if (this.compact) {
|
|
1598
|
+
// Keep labels readable when the room is scaled down for PiP.
|
|
1599
|
+
c.save();
|
|
1600
|
+
c.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
|
|
1601
|
+
for (const [p, at] of heads) this.bubble(p, {
|
|
1602
|
+
...at, x: at.x * this.cam.zoom + this.cam.ox, y: at.y * this.cam.zoom + this.cam.oy,
|
|
1603
|
+
});
|
|
1604
|
+
c.restore();
|
|
1605
|
+
} else {
|
|
1606
|
+
for (const [p, at] of heads) this.bubble(p, at);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (this.offDuty > 0) {
|
|
1610
|
+
const d = this.iso(0.2, this.plan.door.y + 0.9, 0);
|
|
1611
|
+
c.font = '600 10px ui-sans-serif, system-ui, -apple-system, "PingFang TC", sans-serif';
|
|
1612
|
+
c.textAlign = 'center';
|
|
1613
|
+
c.fillStyle = this.pal.text;
|
|
1614
|
+
c.globalAlpha = 0.65;
|
|
1615
|
+
c.fillText(`+${this.offDuty} ${this.offDutyLabel}`, d.x - 10, d.y + 14);
|
|
1616
|
+
c.globalAlpha = 1;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const z = this.cam.zoom;
|
|
1620
|
+
this.hit = this.hit.map((h) => ({
|
|
1621
|
+
key: h.key,
|
|
1622
|
+
x0: h.x0 * z + this.cam.ox, x1: h.x1 * z + this.cam.ox,
|
|
1623
|
+
y0: h.y0 * z + this.cam.oy, y1: h.y1 * z + this.cam.oy,
|
|
1624
|
+
}));
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
window.Office = Office;
|
|
1629
|
+
window.OFFICE_AGENT_BADGES = AGENT_BADGES;
|
|
1630
|
+
window.OfficeGrid = Grid;
|
|
1631
|
+
})();
|