@clawlabz/clawskin 1.0.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 +98 -0
- package/CONTRIBUTING.md +40 -0
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/docs/ARCHITECTURE.md +132 -0
- package/docs/PROGRESS.md +15 -0
- package/docs/ROADMAP.md +130 -0
- package/package.json +27 -0
- package/public/_headers +13 -0
- package/public/_redirects +1 -0
- package/public/app.html +595 -0
- package/public/css/app.css +379 -0
- package/public/css/style.css +415 -0
- package/public/index.html +324 -0
- package/public/js/app/AgentSlot.js +596 -0
- package/public/js/app/AgentStateMapper.js +213 -0
- package/public/js/app/ClawSkinApp.js +551 -0
- package/public/js/app/ConnectionPanel.js +142 -0
- package/public/js/app/DeviceIdentity.js +118 -0
- package/public/js/app/GatewayClient.js +284 -0
- package/public/js/app/SettingsManager.js +47 -0
- package/public/js/character/AnimationManager.js +69 -0
- package/public/js/character/BubbleManager.js +157 -0
- package/public/js/character/CharacterSprite.js +98 -0
- package/public/js/game.js +116 -0
- package/public/js/pets/Pet.js +303 -0
- package/public/js/pets/PetManager.js +102 -0
- package/public/js/scenes/CafeScene.js +594 -0
- package/public/js/scenes/HackerScene.js +527 -0
- package/public/js/scenes/OfficeScene.js +404 -0
- package/public/js/sprites/SpriteGenerator.js +927 -0
- package/public/js/state/AgentStateSync.js +89 -0
- package/public/js/state/DemoMode.js +65 -0
- package/public/js/ui/CharacterEditor.js +145 -0
- package/public/js/ui/ScenePicker.js +29 -0
- package/serve.cjs +132 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OfficeScene.js — Pixel office with proper depth layering per workstation
|
|
3
|
+
*
|
|
4
|
+
* Render order (back to front):
|
|
5
|
+
* 1. Background wall + decorations
|
|
6
|
+
* 2. Per agent: chair → character → desk (hides legs) → monitor + cup on desk
|
|
7
|
+
* 3. Name tags + bubbles on top
|
|
8
|
+
*
|
|
9
|
+
* Characters SIT behind desks — desk surface crosses at waist level.
|
|
10
|
+
*/
|
|
11
|
+
class OfficeScene {
|
|
12
|
+
constructor(canvas, ctx, spriteGen) {
|
|
13
|
+
this.canvas = canvas;
|
|
14
|
+
this.ctx = ctx;
|
|
15
|
+
this.gen = spriteGen;
|
|
16
|
+
this.bgCanvas = null;
|
|
17
|
+
this.cloudX = 0;
|
|
18
|
+
this.sunbeamTimer = 0;
|
|
19
|
+
this.screenFlicker = 0;
|
|
20
|
+
this.name = 'office';
|
|
21
|
+
this.label = '🏢 Office';
|
|
22
|
+
this.workstations = [];
|
|
23
|
+
|
|
24
|
+
// Wandering targets (shared locations agents can visit)
|
|
25
|
+
this.poiList = []; // populated in getWorkstations
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
init() {
|
|
29
|
+
this.bgCanvas = this.gen.generateSceneBg('office', this.canvas.width, this.canvas.height);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getCharacterPosition() {
|
|
33
|
+
return { x: this.canvas.width / 2 - 48, y: this.canvas.height * 0.38 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Calculate workstation layout for N agents.
|
|
38
|
+
* All coordinates are absolute canvas pixels.
|
|
39
|
+
*/
|
|
40
|
+
getWorkstations(count) {
|
|
41
|
+
const w = this.canvas.width;
|
|
42
|
+
const h = this.canvas.height;
|
|
43
|
+
const floorY = Math.round(h * 0.40); // floor line
|
|
44
|
+
const stations = [];
|
|
45
|
+
|
|
46
|
+
// Character scale
|
|
47
|
+
const cs = 2.5; // slightly smaller for better proportions
|
|
48
|
+
const charH = Math.round(32 * cs); // 80px
|
|
49
|
+
const charW = Math.round(32 * cs); // 80px
|
|
50
|
+
|
|
51
|
+
// Furniture sizes (display pixels) — 3/4 top-down view
|
|
52
|
+
const deskW = 120, deskH = 42; // desk (top surface + front face)
|
|
53
|
+
const monW = 44, monH = 44; // iMac back view — square-ish, Apple logo visible
|
|
54
|
+
const chairW = 36, chairH = 40; // 3/4 chair
|
|
55
|
+
const cupW = 16, cupH = 20;
|
|
56
|
+
|
|
57
|
+
// Desk surface Y — pushed down into the floor area, away from wall
|
|
58
|
+
const deskSurfaceY = floorY + 50;
|
|
59
|
+
|
|
60
|
+
if (count <= 0) return stations;
|
|
61
|
+
|
|
62
|
+
// Horizontal spacing
|
|
63
|
+
const positions = [];
|
|
64
|
+
if (count === 1) {
|
|
65
|
+
positions.push(w / 2);
|
|
66
|
+
} else if (count <= 4) {
|
|
67
|
+
for (let i = 0; i < count; i++) {
|
|
68
|
+
positions.push((w / (count + 1)) * (i + 1));
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Two rows for 5+
|
|
72
|
+
const topN = Math.ceil(count / 2);
|
|
73
|
+
const botN = count - topN;
|
|
74
|
+
for (let i = 0; i < topN; i++) positions.push((w / (topN + 1)) * (i + 1));
|
|
75
|
+
for (let i = 0; i < botN; i++) positions.push((w / (botN + 1)) * (i + 1));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < count; i++) {
|
|
79
|
+
const cx = Math.round(positions[i]);
|
|
80
|
+
const isBackRow = count > 4 && i >= Math.ceil(count / 2);
|
|
81
|
+
const rowDeskY = isBackRow ? deskSurfaceY + 70 : deskSurfaceY;
|
|
82
|
+
|
|
83
|
+
// 3/4 RPG view: character sits close to desk
|
|
84
|
+
// Monitor is flat/tilted so it's very short — doesn't block upper body
|
|
85
|
+
const charY = rowDeskY - charH + 10; // character close to desk, legs slightly behind desk
|
|
86
|
+
const deskY = rowDeskY;
|
|
87
|
+
|
|
88
|
+
stations.push({
|
|
89
|
+
// Character: sitting at desk, upper body fully visible above the flat monitor
|
|
90
|
+
charX: cx - charW / 2,
|
|
91
|
+
charY: charY,
|
|
92
|
+
charScale: cs,
|
|
93
|
+
|
|
94
|
+
// Chair: behind character
|
|
95
|
+
chairX: cx - chairW / 2,
|
|
96
|
+
chairY: charY + charH * 0.3,
|
|
97
|
+
chairW, chairH,
|
|
98
|
+
|
|
99
|
+
// Desk: in front of character
|
|
100
|
+
deskX: cx - deskW / 2,
|
|
101
|
+
deskY: deskY,
|
|
102
|
+
deskW, deskH,
|
|
103
|
+
|
|
104
|
+
// iMac 45° angled: on desk LEFT side, away from character center
|
|
105
|
+
monX: cx - deskW / 2 + 6, // flush left on desk
|
|
106
|
+
monY: deskY - monH + 10, // base sits on desk surface
|
|
107
|
+
monW, monH,
|
|
108
|
+
|
|
109
|
+
// Coffee cup: on desk right side
|
|
110
|
+
cupX: cx + deskW / 2 - cupW - 12,
|
|
111
|
+
cupY: deskY - cupH + 8,
|
|
112
|
+
cupW, cupH,
|
|
113
|
+
|
|
114
|
+
// Center point (for returning from walks)
|
|
115
|
+
cx, cy: rowDeskY,
|
|
116
|
+
index: i,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Points of interest for wandering — far enough from walls for 80px-wide characters
|
|
121
|
+
const floorH = h - floorY;
|
|
122
|
+
this.poiList = [
|
|
123
|
+
{ x: w / 2, y: 30, label: 'window' },
|
|
124
|
+
{ x: 80, y: floorY + 30, label: 'bookshelf' },
|
|
125
|
+
{ x: w - 90, y: floorY + 20, label: 'fridge' },
|
|
126
|
+
{ x: 90, y: floorY + floorH * 0.55, label: 'sofa' },
|
|
127
|
+
{ x: w - 90, y: floorY + floorH * 0.45, label: 'bar' },
|
|
128
|
+
{ x: w - 90, y: floorY + floorH * 0.28, label: 'coffee_machine' },
|
|
129
|
+
{ x: 80, y: floorY + floorH * 0.30, label: 'plant' },
|
|
130
|
+
{ x: w - 90, y: floorY + floorH * 0.68, label: 'plant' },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
// Collision obstacles — solid rects that agents/pets must avoid
|
|
134
|
+
this.obstacles = [
|
|
135
|
+
...stations.map(s => ({x: s.deskX - 5, y: s.deskY - 5, w: s.deskW + 10, h: s.deskH + 10, deskIndex: s.index})),
|
|
136
|
+
{x: 0, y: floorY + 5, w: 30, h: 92}, // left bookshelf
|
|
137
|
+
{x: 0, y: floorY + floorH * 0.46, w: 50, h: 62}, // left sofa
|
|
138
|
+
{x: w - 28, y: floorY + 3, w: 28, h: 58}, // right fridge
|
|
139
|
+
{x: w - 30, y: floorY + floorH * 0.23, w: 30, h: 85}, // right bar + coffee
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
this.workstations = stations;
|
|
143
|
+
return stations;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
update(dt) {
|
|
147
|
+
this.cloudX += dt * 0.003;
|
|
148
|
+
this.sunbeamTimer += dt;
|
|
149
|
+
this.screenFlicker += dt;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Render background wall + static decorations (called once per frame) */
|
|
153
|
+
render(ctx) {
|
|
154
|
+
const w = this.canvas.width;
|
|
155
|
+
const h = this.canvas.height;
|
|
156
|
+
|
|
157
|
+
if (this.bgCanvas) ctx.drawImage(this.bgCanvas, 0, 0);
|
|
158
|
+
|
|
159
|
+
// Animated clouds — inside window, pushed down to match new window position
|
|
160
|
+
const wx = (w - 80) / 2;
|
|
161
|
+
const wy = Math.round(h * 0.08);
|
|
162
|
+
ctx.save();
|
|
163
|
+
ctx.beginPath(); ctx.rect(wx, wy + 2, 80, 70); ctx.clip();
|
|
164
|
+
ctx.fillStyle = '#FFF';
|
|
165
|
+
const cx1 = wx + ((this.cloudX * 20) % 120) - 20;
|
|
166
|
+
ctx.fillRect(cx1, wy + 20, 18, 6); ctx.fillRect(cx1 + 3, wy + 17, 11, 4);
|
|
167
|
+
const cx2 = wx + ((this.cloudX * 12 + 60) % 130) - 10;
|
|
168
|
+
ctx.fillRect(cx2, wy + 35, 14, 5); ctx.fillRect(cx2 + 2, wy + 32, 9, 4);
|
|
169
|
+
ctx.restore();
|
|
170
|
+
|
|
171
|
+
// Sunbeam
|
|
172
|
+
const floorLine = h * 0.40;
|
|
173
|
+
if (Math.sin(this.sunbeamTimer * 0.001) > 0) {
|
|
174
|
+
ctx.fillStyle = 'rgba(255,255,200,0.04)';
|
|
175
|
+
const bx = wx + 60;
|
|
176
|
+
ctx.beginPath(); ctx.moveTo(bx, wy + 80); ctx.lineTo(bx+40, wy); ctx.lineTo(bx+55, wy); ctx.lineTo(bx+80, h*0.55); ctx.fill();
|
|
177
|
+
ctx.fillStyle = 'rgba(255,255,200,0.3)';
|
|
178
|
+
for (let i = 0; i < 5; i++) {
|
|
179
|
+
const px = bx + 20 + Math.sin(this.sunbeamTimer*0.0005+i*2)*25;
|
|
180
|
+
const py = wy + 30 + ((this.sunbeamTimer*0.02+i*40)%(h*0.25));
|
|
181
|
+
ctx.beginPath(); ctx.arc(px,py,1.5,0,Math.PI*2); ctx.fill();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Wall decorations — below top UI safe zone (~50px from top)
|
|
186
|
+
const wallSafe = Math.round(h * 0.12); // safe Y start below UI
|
|
187
|
+
ctx.drawImage(this.gen.generateFurniture('bookshelf'), 15, wallSafe, 48, 64);
|
|
188
|
+
ctx.drawImage(this.gen.generateFurniture('plant'), 75, floorLine - 36, 26, 36);
|
|
189
|
+
ctx.drawImage(this.gen.generateFurniture('lamp'), 100, wallSafe + 5, 18, 32);
|
|
190
|
+
ctx.drawImage(this.gen.generateFurniture('lamp'), w-110, wallSafe + 5, 18, 32);
|
|
191
|
+
this._drawWhiteboard(ctx, w*0.15, wallSafe + 2, 70, 44);
|
|
192
|
+
this._drawClock(ctx, w-45, wallSafe + 8);
|
|
193
|
+
this._drawWaterCooler(ctx, w-80, floorLine - 30);
|
|
194
|
+
|
|
195
|
+
// ── Floor decorations — side-view, pressed against left/right walls ──
|
|
196
|
+
const floorH = h - floorLine;
|
|
197
|
+
|
|
198
|
+
// LEFT WALL — furniture faces RIGHT (side-view: thin width = depth, height = visible front)
|
|
199
|
+
this._drawSideBookshelf(ctx, 0, floorLine + 8, 22, 90, 'left');
|
|
200
|
+
this._drawSideSofa(ctx, 0, floorLine + floorH * 0.46, 45, 55, 'left');
|
|
201
|
+
ctx.drawImage(this.gen.generateFurniture('plant'), 3, floorLine + floorH * 0.28, 22, 30);
|
|
202
|
+
|
|
203
|
+
// RIGHT WALL — furniture faces LEFT (mirrored side-view)
|
|
204
|
+
this._drawSideFridge(ctx, w - 22, floorLine + 5, 22, 55, 'right');
|
|
205
|
+
this._drawSideBar(ctx, w - 25, floorLine + floorH * 0.25, 25, 80, 'right');
|
|
206
|
+
this._drawSideCoffeeMachine(ctx, w - 18, floorLine + floorH * 0.25 - 32, 18, 30, 'right');
|
|
207
|
+
ctx.drawImage(this.gen.generateFurniture('plant'), w - 22, floorLine + floorH * 0.68, 22, 30);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Render chair (behind character) */
|
|
211
|
+
renderChair(ctx, s) {
|
|
212
|
+
ctx.drawImage(this.gen.generateFurniture('chair'), s.chairX, s.chairY, s.chairW, s.chairH);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Render desk + laptop + cup (in front of character in 3/4 view) */
|
|
216
|
+
renderDesk(ctx, s, agentState) {
|
|
217
|
+
const isWorking = ['typing','executing','browsing'].includes(agentState);
|
|
218
|
+
|
|
219
|
+
// Desk surface first (3/4 view — top surface + front face)
|
|
220
|
+
ctx.drawImage(this.gen.generateFurniture('desk'), s.deskX, s.deskY, s.deskW, s.deskH);
|
|
221
|
+
|
|
222
|
+
// Laptop on desk surface
|
|
223
|
+
const monType = isWorking && Math.floor(this.screenFlicker/400)%2===0 ? 'laptop_active' : 'laptop';
|
|
224
|
+
ctx.drawImage(this.gen.generateFurniture(monType), s.monX, s.monY, s.monW, s.monH);
|
|
225
|
+
|
|
226
|
+
// Laptop screen glow when working
|
|
227
|
+
if (isWorking) {
|
|
228
|
+
ctx.fillStyle = 'rgba(100,200,255,0.04)';
|
|
229
|
+
ctx.fillRect(s.monX-6, s.monY-3, s.monW+12, s.monH+6);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Coffee cup on desk
|
|
233
|
+
ctx.drawImage(this.gen.generateFurniture('coffee_cup'), s.cupX, s.cupY, s.cupW, s.cupH);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_drawWaterCooler(ctx, x, y) {
|
|
237
|
+
ctx.fillStyle='#B0C4DE'; ctx.fillRect(x,y,14,26);
|
|
238
|
+
ctx.fillStyle='#87CEEB'; ctx.fillRect(x+2,y+2,10,10);
|
|
239
|
+
ctx.fillStyle='#708090'; ctx.fillRect(x-2,y+24,18,3);
|
|
240
|
+
ctx.fillStyle='#FFF'; ctx.fillRect(x+3,y+14,3,2);
|
|
241
|
+
ctx.fillStyle='#E74C3C'; ctx.fillRect(x+8,y+14,3,2);
|
|
242
|
+
}
|
|
243
|
+
_drawWhiteboard(ctx, x, y, w, h) {
|
|
244
|
+
ctx.fillStyle='#A0A0A0'; ctx.fillRect(x-2,y-2,w+4,h+4);
|
|
245
|
+
ctx.fillStyle='#F8F8F8'; ctx.fillRect(x,y,w,h);
|
|
246
|
+
ctx.fillStyle='#4A90D9'; ctx.fillRect(x+4,y+7,22,2); ctx.fillRect(x+4,y+12,30,2);
|
|
247
|
+
ctx.fillStyle='#E74C3C'; ctx.fillRect(x+40,y+7,12,12);
|
|
248
|
+
ctx.fillStyle='#2ECC71'; ctx.fillRect(x+4,y+24,25,2); ctx.fillRect(x+4,y+30,35,2);
|
|
249
|
+
}
|
|
250
|
+
_drawClock(ctx, x, y) {
|
|
251
|
+
ctx.fillStyle='#333'; ctx.beginPath(); ctx.arc(x,y+10,11,0,Math.PI*2); ctx.fill();
|
|
252
|
+
ctx.fillStyle='#F8F8F8'; ctx.beginPath(); ctx.arc(x,y+10,9,0,Math.PI*2); ctx.fill();
|
|
253
|
+
const now=new Date(), hr=now.getHours()%12, mn=now.getMinutes();
|
|
254
|
+
ctx.strokeStyle='#333'; ctx.lineWidth=1.5;
|
|
255
|
+
const ha=(hr+mn/60)*Math.PI/6-Math.PI/2;
|
|
256
|
+
ctx.beginPath(); ctx.moveTo(x,y+10); ctx.lineTo(x+Math.cos(ha)*5,y+10+Math.sin(ha)*5); ctx.stroke();
|
|
257
|
+
ctx.lineWidth=1; const ma=mn*Math.PI/30-Math.PI/2;
|
|
258
|
+
ctx.beginPath(); ctx.moveTo(x,y+10); ctx.lineTo(x+Math.cos(ma)*7,y+10+Math.sin(ma)*7); ctx.stroke();
|
|
259
|
+
ctx.fillStyle='#E74C3C'; ctx.beginPath(); ctx.arc(x,y+10,1.5,0,Math.PI*2); ctx.fill();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Side-view floor decoration draw methods ──
|
|
263
|
+
// Furniture is pressed flat against the left/right wall.
|
|
264
|
+
// "depth" is the thin dimension going into the room; "h" is the tall front face.
|
|
265
|
+
|
|
266
|
+
_drawSideBookshelf(ctx, x, y, depth, h, side) {
|
|
267
|
+
// Side panel (visible depth going into room)
|
|
268
|
+
const frameColor = '#6B4226', backColor = '#8B5E3C', shelfColor = '#5D3A1A';
|
|
269
|
+
const bookColors = ['#E74C3C','#3498DB','#2ECC71','#F39C12','#9B59B6','#1ABC9C','#E67E22'];
|
|
270
|
+
// Outer frame
|
|
271
|
+
ctx.fillStyle = frameColor; ctx.fillRect(x, y, depth, h);
|
|
272
|
+
// Inner back
|
|
273
|
+
ctx.fillStyle = backColor; ctx.fillRect(x + 2, y + 2, depth - 4, h - 4);
|
|
274
|
+
// Front-facing edge (the side we see is the narrow depth)
|
|
275
|
+
const edgeX = side === 'left' ? x + depth - 3 : x;
|
|
276
|
+
ctx.fillStyle = shelfColor; ctx.fillRect(edgeX, y, 3, h);
|
|
277
|
+
// Shelves — horizontal dividers
|
|
278
|
+
const shelfCount = 5;
|
|
279
|
+
const shelfGap = Math.floor((h - 6) / shelfCount);
|
|
280
|
+
for (let i = 0; i < shelfCount; i++) {
|
|
281
|
+
const sy = y + 4 + i * shelfGap;
|
|
282
|
+
ctx.fillStyle = shelfColor; ctx.fillRect(x + 2, sy, depth - 4, 2);
|
|
283
|
+
// Books — vertical spines visible from the side
|
|
284
|
+
for (let b = 0; b < 3; b++) {
|
|
285
|
+
const bh = shelfGap - 6;
|
|
286
|
+
const bw = 2 + (b % 2);
|
|
287
|
+
const bx = x + 4 + b * (bw + 1);
|
|
288
|
+
ctx.fillStyle = bookColors[(i * 3 + b) % bookColors.length];
|
|
289
|
+
ctx.fillRect(bx, sy - bh, bw, bh);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Top/bottom trim
|
|
293
|
+
ctx.fillStyle = shelfColor;
|
|
294
|
+
ctx.fillRect(x - 1, y - 2, depth + 2, 3);
|
|
295
|
+
ctx.fillRect(x - 1, y + h - 1, depth + 2, 3);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
_drawSideSofa(ctx, x, y, depth, h, side) {
|
|
299
|
+
// Side-view sofa: we see the arm rest + cushion profile
|
|
300
|
+
const sofaColor = '#5B7BA5', sofaDark = '#4A6A8E', sofaLight = '#6B8BB5';
|
|
301
|
+
// Arm rest (the full side face)
|
|
302
|
+
ctx.fillStyle = sofaDark; ctx.fillRect(x, y, depth, h);
|
|
303
|
+
// Inner cushion area
|
|
304
|
+
ctx.fillStyle = sofaColor; ctx.fillRect(x + 3, y + 6, depth - 6, h - 10);
|
|
305
|
+
// Back rest (top portion)
|
|
306
|
+
ctx.fillStyle = sofaDark; ctx.fillRect(x + 2, y, depth - 4, 10);
|
|
307
|
+
ctx.fillStyle = sofaLight; ctx.fillRect(x + 4, y + 2, depth - 8, 6);
|
|
308
|
+
// Seat cushion profile
|
|
309
|
+
ctx.fillStyle = sofaLight; ctx.fillRect(x + 5, y + 14, depth - 10, h - 22);
|
|
310
|
+
// Cushion seam — horizontal line
|
|
311
|
+
ctx.fillStyle = sofaDark;
|
|
312
|
+
ctx.fillRect(x + 5, y + Math.floor(h * 0.45), depth - 10, 1);
|
|
313
|
+
ctx.fillRect(x + 5, y + Math.floor(h * 0.65), depth - 10, 1);
|
|
314
|
+
// Legs
|
|
315
|
+
ctx.fillStyle = '#4A3728';
|
|
316
|
+
ctx.fillRect(x + 4, y + h - 2, 4, 4);
|
|
317
|
+
ctx.fillRect(x + depth - 8, y + h - 2, 4, 4);
|
|
318
|
+
// Pillow leaning against back
|
|
319
|
+
ctx.fillStyle = '#E8D4B8';
|
|
320
|
+
const px = side === 'left' ? x + depth - 12 : x + 4;
|
|
321
|
+
ctx.fillRect(px, y + 4, 8, 12);
|
|
322
|
+
ctx.fillStyle = '#D4C0A4'; ctx.fillRect(px + 1, y + 5, 6, 10);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
_drawSideFridge(ctx, x, y, depth, h, side) {
|
|
326
|
+
// Side-view fridge: we see the narrow side panel
|
|
327
|
+
ctx.fillStyle = '#C8CDD3'; ctx.fillRect(x, y, depth, h); // side panel
|
|
328
|
+
ctx.fillStyle = '#D0D5DB'; ctx.fillRect(x + 2, y + 2, depth - 4, h - 4); // inset
|
|
329
|
+
// Door edge line (front-facing edge)
|
|
330
|
+
const edgeX = side === 'right' ? x : x + depth - 3;
|
|
331
|
+
ctx.fillStyle = '#A0A8B0'; ctx.fillRect(edgeX, y, 3, h);
|
|
332
|
+
// Door split line
|
|
333
|
+
ctx.fillStyle = '#A0A8B0'; ctx.fillRect(x, y + Math.floor(h * 0.6), depth, 2);
|
|
334
|
+
// Handle on front edge
|
|
335
|
+
ctx.fillStyle = '#888';
|
|
336
|
+
ctx.fillRect(edgeX + 1, y + Math.floor(h * 0.25), 1, 8);
|
|
337
|
+
ctx.fillRect(edgeX + 1, y + Math.floor(h * 0.72), 1, 6);
|
|
338
|
+
// Top surface
|
|
339
|
+
ctx.fillStyle = '#E0E5EB'; ctx.fillRect(x, y - 2, depth, 3);
|
|
340
|
+
// Brand sticker on side
|
|
341
|
+
ctx.fillStyle = '#4A90D9'; ctx.fillRect(x + 6, y + 5, 8, 3);
|
|
342
|
+
// Magnet
|
|
343
|
+
ctx.fillStyle = '#E74C3C'; ctx.fillRect(x + 5, y + 14, 3, 3);
|
|
344
|
+
ctx.fillStyle = '#F1C40F'; ctx.fillRect(x + 12, y + 12, 3, 3);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
_drawSideBar(ctx, x, y, depth, h, side) {
|
|
348
|
+
// Side-view bar counter: we see the narrow profile
|
|
349
|
+
const woodColor = '#7B5B3A', woodDark = '#5D3A1A', woodLight = '#9B7B5A';
|
|
350
|
+
// Counter top (narrow strip)
|
|
351
|
+
ctx.fillStyle = woodLight; ctx.fillRect(x, y, depth, 4);
|
|
352
|
+
ctx.fillStyle = woodColor; ctx.fillRect(x, y + 4, depth, h - 4);
|
|
353
|
+
// Wood grain — horizontal lines on side panel
|
|
354
|
+
ctx.fillStyle = woodDark;
|
|
355
|
+
for (let ly = y + 12; ly < y + h; ly += 12) {
|
|
356
|
+
ctx.fillRect(x + 2, ly, depth - 4, 1);
|
|
357
|
+
}
|
|
358
|
+
// Front-facing edge
|
|
359
|
+
const edgeX = side === 'right' ? x : x + depth - 3;
|
|
360
|
+
ctx.fillStyle = woodDark; ctx.fillRect(edgeX, y, 3, h);
|
|
361
|
+
// Top highlight
|
|
362
|
+
ctx.fillStyle = woodLight; ctx.fillRect(x, y, depth, 2);
|
|
363
|
+
// Items on top of counter — bottles (seen from side)
|
|
364
|
+
ctx.fillStyle = '#2ECC71'; ctx.fillRect(x + 4, y - 12, 4, 12);
|
|
365
|
+
ctx.fillStyle = '#27AE60'; ctx.fillRect(x + 5, y - 14, 2, 3);
|
|
366
|
+
ctx.fillStyle = '#E74C3C'; ctx.fillRect(x + 12, y - 10, 4, 10);
|
|
367
|
+
ctx.fillStyle = '#C0392B'; ctx.fillRect(x + 13, y - 12, 2, 3);
|
|
368
|
+
// Cup
|
|
369
|
+
ctx.fillStyle = '#FFF'; ctx.fillRect(x + 8, y - 6, 3, 6);
|
|
370
|
+
ctx.fillStyle = '#87CEEB'; ctx.fillRect(x + 9, y - 4, 1, 3);
|
|
371
|
+
// Bar stool in front (facing into room)
|
|
372
|
+
const stoolX = side === 'right' ? x - 14 : x + depth + 4;
|
|
373
|
+
this._drawSideBarStool(ctx, stoolX, y + h - 8);
|
|
374
|
+
this._drawSideBarStool(ctx, stoolX, y + h - 38);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
_drawSideBarStool(ctx, x, y) {
|
|
378
|
+
ctx.fillStyle = '#4A4A4A'; ctx.fillRect(x + 3, y, 2, 12); // stem
|
|
379
|
+
ctx.fillStyle = '#333'; ctx.fillRect(x, y - 3, 8, 4); // seat top
|
|
380
|
+
ctx.fillStyle = '#555'; ctx.fillRect(x + 1, y + 11, 6, 2); // base
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
_drawSideCoffeeMachine(ctx, x, y, depth, h, side) {
|
|
384
|
+
// Side-view espresso machine sitting on the bar counter
|
|
385
|
+
ctx.fillStyle = '#708090'; ctx.fillRect(x, y, depth, h);
|
|
386
|
+
ctx.fillStyle = '#5A6A7A'; ctx.fillRect(x + 2, y + 2, depth - 4, 8);
|
|
387
|
+
// Power LED
|
|
388
|
+
ctx.fillStyle = '#2ECC71'; ctx.fillRect(x + 4, y + 5, 2, 2);
|
|
389
|
+
// Drip area
|
|
390
|
+
ctx.fillStyle = '#555'; ctx.fillRect(x + 3, y + 12, depth - 6, 10);
|
|
391
|
+
ctx.fillStyle = '#444'; ctx.fillRect(x + 5, y + 14, depth - 10, 6);
|
|
392
|
+
// Cup
|
|
393
|
+
ctx.fillStyle = '#FFF'; ctx.fillRect(x + 5, y + 18, 5, 4);
|
|
394
|
+
ctx.fillStyle = '#8B4513'; ctx.fillRect(x + 6, y + 19, 3, 2);
|
|
395
|
+
// Steam wisps
|
|
396
|
+
ctx.fillStyle = 'rgba(200,200,200,0.4)';
|
|
397
|
+
ctx.fillRect(x + 6, y + 15, 1, 3);
|
|
398
|
+
ctx.fillRect(x + 9, y + 14, 1, 3);
|
|
399
|
+
// Base
|
|
400
|
+
ctx.fillStyle = '#5A6A7A'; ctx.fillRect(x - 1, y + h - 2, depth + 2, 3);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (typeof window !== 'undefined') window.OfficeScene = OfficeScene;
|