@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,594 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CafeScene.js — Warm coffee shop with rain outside, cozy lighting
|
|
3
|
+
* Architecture matches OfficeScene: getWorkstations, renderChair, renderDesk
|
|
4
|
+
*
|
|
5
|
+
* Render order (back to front):
|
|
6
|
+
* 1. Background wall + rain + warm glow + decorations
|
|
7
|
+
* 2. Per agent: café chair → character → round table (hides legs) → laptop + latte
|
|
8
|
+
* 3. Name tags + bubbles on top
|
|
9
|
+
*/
|
|
10
|
+
class CafeScene {
|
|
11
|
+
constructor(canvas, ctx, spriteGen) {
|
|
12
|
+
this.canvas = canvas;
|
|
13
|
+
this.ctx = ctx;
|
|
14
|
+
this.gen = spriteGen;
|
|
15
|
+
this.bgCanvas = null;
|
|
16
|
+
this.rainDrops = [];
|
|
17
|
+
this.steamParticles = [];
|
|
18
|
+
this.warmGlow = 0;
|
|
19
|
+
this.screenFlicker = 0;
|
|
20
|
+
this.name = 'cafe';
|
|
21
|
+
this.label = '☕ Cozy Café';
|
|
22
|
+
this.workstations = [];
|
|
23
|
+
this.poiList = [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
init() {
|
|
27
|
+
this.bgCanvas = this.gen.generateSceneBg('cafe', this.canvas.width, this.canvas.height);
|
|
28
|
+
this.rainDrops = [];
|
|
29
|
+
for (let i = 0; i < 50; i++) {
|
|
30
|
+
this.rainDrops.push({
|
|
31
|
+
x: Math.random() * this.canvas.width,
|
|
32
|
+
y: Math.random() * this.canvas.height * 0.5,
|
|
33
|
+
speed: 2 + Math.random() * 3,
|
|
34
|
+
length: 4 + Math.random() * 8,
|
|
35
|
+
opacity: 0.2 + Math.random() * 0.4,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
this.steamParticles = [];
|
|
39
|
+
for (let i = 0; i < 8; i++) {
|
|
40
|
+
this.steamParticles.push({
|
|
41
|
+
x: 0, y: 0, life: Math.random() * 100, maxLife: 100,
|
|
42
|
+
vx: (Math.random() - 0.5) * 0.3, vy: -0.3 - Math.random() * 0.3,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getCharacterPosition() {
|
|
48
|
+
return { x: this.canvas.width / 2 - 48, y: this.canvas.height * 0.38 };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Calculate workstation layout for N agents.
|
|
53
|
+
* Café tables: round wooden tables with comfy chairs.
|
|
54
|
+
*/
|
|
55
|
+
getWorkstations(count) {
|
|
56
|
+
const w = this.canvas.width;
|
|
57
|
+
const h = this.canvas.height;
|
|
58
|
+
const floorY = Math.round(h * 0.40);
|
|
59
|
+
const stations = [];
|
|
60
|
+
|
|
61
|
+
const cs = 2.5;
|
|
62
|
+
const charH = Math.round(32 * cs);
|
|
63
|
+
const charW = Math.round(32 * cs);
|
|
64
|
+
|
|
65
|
+
// Café tables are smaller and rounder-looking
|
|
66
|
+
const deskW = 110, deskH = 38;
|
|
67
|
+
const monW = 44, monH = 44;
|
|
68
|
+
const chairW = 36, chairH = 40;
|
|
69
|
+
const cupW = 16, cupH = 20;
|
|
70
|
+
|
|
71
|
+
const deskSurfaceY = floorY + 50;
|
|
72
|
+
|
|
73
|
+
if (count <= 0) return stations;
|
|
74
|
+
|
|
75
|
+
const positions = [];
|
|
76
|
+
if (count === 1) {
|
|
77
|
+
positions.push(w / 2);
|
|
78
|
+
} else if (count <= 4) {
|
|
79
|
+
for (let i = 0; i < count; i++) {
|
|
80
|
+
positions.push((w / (count + 1)) * (i + 1));
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
const topN = Math.ceil(count / 2);
|
|
84
|
+
const botN = count - topN;
|
|
85
|
+
for (let i = 0; i < topN; i++) positions.push((w / (topN + 1)) * (i + 1));
|
|
86
|
+
for (let i = 0; i < botN; i++) positions.push((w / (botN + 1)) * (i + 1));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < count; i++) {
|
|
90
|
+
const cx = Math.round(positions[i]);
|
|
91
|
+
const isBackRow = count > 4 && i >= Math.ceil(count / 2);
|
|
92
|
+
const rowDeskY = isBackRow ? deskSurfaceY + 70 : deskSurfaceY;
|
|
93
|
+
|
|
94
|
+
const charY = rowDeskY - charH + 10;
|
|
95
|
+
const deskY = rowDeskY;
|
|
96
|
+
|
|
97
|
+
stations.push({
|
|
98
|
+
charX: cx - charW / 2,
|
|
99
|
+
charY,
|
|
100
|
+
charScale: cs,
|
|
101
|
+
|
|
102
|
+
chairX: cx - chairW / 2,
|
|
103
|
+
chairY: charY + charH * 0.3,
|
|
104
|
+
chairW, chairH,
|
|
105
|
+
|
|
106
|
+
deskX: cx - deskW / 2,
|
|
107
|
+
deskY,
|
|
108
|
+
deskW, deskH,
|
|
109
|
+
|
|
110
|
+
monX: cx - deskW / 2 + 6,
|
|
111
|
+
monY: deskY - monH + 10,
|
|
112
|
+
monW, monH,
|
|
113
|
+
|
|
114
|
+
cupX: cx + deskW / 2 - cupW - 12,
|
|
115
|
+
cupY: deskY - cupH + 8,
|
|
116
|
+
cupW, cupH,
|
|
117
|
+
|
|
118
|
+
cx, cy: rowDeskY,
|
|
119
|
+
index: i,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Points of interest for agent wandering
|
|
124
|
+
const floorH = h - floorY;
|
|
125
|
+
this.poiList = [
|
|
126
|
+
{ x: 80, y: floorY + 20, label: 'bookshelf' },
|
|
127
|
+
{ x: w - 90, y: floorY + 20, label: 'counter' },
|
|
128
|
+
{ x: w / 2, y: 30, label: 'window' },
|
|
129
|
+
{ x: 80, y: floorY + floorH * 0.55, label: 'sofa' },
|
|
130
|
+
{ x: w - 90, y: floorY + floorH * 0.45, label: 'pastry_case' },
|
|
131
|
+
{ x: w - 90, y: floorY + floorH * 0.28, label: 'coffee_bar' },
|
|
132
|
+
{ x: 80, y: floorY + floorH * 0.30, label: 'plant' },
|
|
133
|
+
{ x: w - 90, y: floorY + floorH * 0.68, label: 'magazine_rack' },
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// Collision obstacles
|
|
137
|
+
this.obstacles = [
|
|
138
|
+
...stations.map(s => ({ x: s.deskX - 5, y: s.deskY - 5, w: s.deskW + 10, h: s.deskH + 10, deskIndex: s.index })),
|
|
139
|
+
{ x: 0, y: floorY + 5, w: 30, h: 80 }, // left bookshelf
|
|
140
|
+
{ x: 0, y: floorY + floorH * 0.46, w: 45, h: 55 }, // left sofa
|
|
141
|
+
{ x: w-28, y: floorY + 3, w: 28, h: 55 }, // right counter
|
|
142
|
+
{ x: w-30, y: floorY + floorH * 0.23, w: 30, h: 80 }, // right bar + pastry
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
this.workstations = stations;
|
|
146
|
+
return stations;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
update(dt) {
|
|
150
|
+
this.warmGlow += dt * 0.002;
|
|
151
|
+
this.screenFlicker += dt;
|
|
152
|
+
this.rainDrops.forEach(d => {
|
|
153
|
+
d.y += d.speed;
|
|
154
|
+
d.x += 0.5;
|
|
155
|
+
if (d.y > this.canvas.height * 0.55) {
|
|
156
|
+
d.y = -d.length;
|
|
157
|
+
d.x = Math.random() * this.canvas.width;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
this.steamParticles.forEach(p => {
|
|
161
|
+
p.life += dt * 0.05;
|
|
162
|
+
if (p.life > p.maxLife) {
|
|
163
|
+
p.life = 0;
|
|
164
|
+
p.x = 0; p.y = 0;
|
|
165
|
+
}
|
|
166
|
+
p.x += p.vx;
|
|
167
|
+
p.y += p.vy;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Render background + static decorations */
|
|
172
|
+
render(ctx) {
|
|
173
|
+
const w = this.canvas.width;
|
|
174
|
+
const h = this.canvas.height;
|
|
175
|
+
|
|
176
|
+
if (this.bgCanvas) ctx.drawImage(this.bgCanvas, 0, 0);
|
|
177
|
+
|
|
178
|
+
// ── Rain on window ──
|
|
179
|
+
const cwx = w - 150, cwy = 18, cww = 120, cwh = 70;
|
|
180
|
+
ctx.save();
|
|
181
|
+
ctx.beginPath();
|
|
182
|
+
ctx.rect(cwx, cwy, cww, cwh);
|
|
183
|
+
ctx.clip();
|
|
184
|
+
ctx.strokeStyle = 'rgba(180, 200, 220, 0.5)';
|
|
185
|
+
ctx.lineWidth = 1;
|
|
186
|
+
this.rainDrops.forEach(d => {
|
|
187
|
+
if (d.x > cwx && d.x < cwx + cww) {
|
|
188
|
+
ctx.globalAlpha = d.opacity;
|
|
189
|
+
ctx.beginPath();
|
|
190
|
+
ctx.moveTo(d.x, d.y);
|
|
191
|
+
ctx.lineTo(d.x + 1, d.y + d.length);
|
|
192
|
+
ctx.stroke();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
ctx.restore();
|
|
196
|
+
|
|
197
|
+
// Rain streaks on window glass
|
|
198
|
+
ctx.save();
|
|
199
|
+
ctx.beginPath();
|
|
200
|
+
ctx.rect(cwx, cwy, cww, cwh);
|
|
201
|
+
ctx.clip();
|
|
202
|
+
ctx.strokeStyle = 'rgba(200, 220, 240, 0.15)';
|
|
203
|
+
ctx.lineWidth = 2;
|
|
204
|
+
for (let i = 0; i < 5; i++) {
|
|
205
|
+
const sx = cwx + 10 + i * 25;
|
|
206
|
+
ctx.beginPath();
|
|
207
|
+
ctx.moveTo(sx, cwy);
|
|
208
|
+
for (let j = 0; j < 8; j++) {
|
|
209
|
+
ctx.lineTo(sx + Math.sin(j + i) * 3, cwy + j * 10);
|
|
210
|
+
}
|
|
211
|
+
ctx.stroke();
|
|
212
|
+
}
|
|
213
|
+
ctx.restore();
|
|
214
|
+
|
|
215
|
+
// ── Warm ambient light ──
|
|
216
|
+
const glowIntensity = 0.08 + Math.sin(this.warmGlow) * 0.02;
|
|
217
|
+
const warmGrad = ctx.createRadialGradient(w * 0.35, h * 0.35, 10, w * 0.35, h * 0.35, 200);
|
|
218
|
+
warmGrad.addColorStop(0, `rgba(255, 200, 100, ${glowIntensity})`);
|
|
219
|
+
warmGrad.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
|
220
|
+
ctx.fillStyle = warmGrad;
|
|
221
|
+
ctx.fillRect(0, 0, w, h);
|
|
222
|
+
|
|
223
|
+
const floorY = Math.round(h * 0.40);
|
|
224
|
+
const floorH = h - floorY;
|
|
225
|
+
const wallSafe = Math.round(h * 0.12);
|
|
226
|
+
|
|
227
|
+
// ── Wall decorations ──
|
|
228
|
+
this._drawHangingLamp(ctx, w * 0.30, 0);
|
|
229
|
+
this._drawHangingLamp(ctx, w * 0.70, 0);
|
|
230
|
+
this._drawMenuBoard(ctx, 25, wallSafe, 65, 44);
|
|
231
|
+
this._drawCafeArt(ctx, w * 0.55, wallSafe + 2, 50, 36);
|
|
232
|
+
this._drawClock(ctx, w - 40, wallSafe + 8);
|
|
233
|
+
|
|
234
|
+
// ── LEFT WALL: bookshelf + sofa ──
|
|
235
|
+
this._drawSideBookshelf(ctx, 0, floorY + 8, 22, 78, 'left');
|
|
236
|
+
this._drawSideSofa(ctx, 0, floorY + floorH * 0.46, 42, 50, 'left');
|
|
237
|
+
ctx.drawImage(this.gen.generateFurniture('plant'), 3, floorY + floorH * 0.28, 22, 30);
|
|
238
|
+
|
|
239
|
+
// ── RIGHT WALL: coffee counter + pastry case ──
|
|
240
|
+
this._drawSideCoffeeCounter(ctx, w - 25, floorY + 5, 25, 52, 'right');
|
|
241
|
+
this._drawSidePastryCase(ctx, w - 25, floorY + floorH * 0.25, 25, 75, 'right');
|
|
242
|
+
ctx.drawImage(this.gen.generateFurniture('plant'), w - 22, floorY + floorH * 0.68, 22, 30);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Render café chair (behind character) */
|
|
246
|
+
renderChair(ctx, s) {
|
|
247
|
+
const x = s.chairX, y = s.chairY, cw = s.chairW, ch = s.chairH;
|
|
248
|
+
// Wooden café chair with cushion
|
|
249
|
+
// Back rest — rounded wooden frame
|
|
250
|
+
ctx.fillStyle = '#8B5E3C';
|
|
251
|
+
ctx.fillRect(x + 4, y, cw - 8, ch * 0.45);
|
|
252
|
+
// Cross bar on back
|
|
253
|
+
ctx.fillStyle = '#7A4E2C';
|
|
254
|
+
ctx.fillRect(x + 6, y + 4, cw - 12, 2);
|
|
255
|
+
ctx.fillRect(x + 6, y + Math.floor(ch * 0.25), cw - 12, 2);
|
|
256
|
+
// Side posts
|
|
257
|
+
ctx.fillStyle = '#6B4226';
|
|
258
|
+
ctx.fillRect(x + 4, y, 3, ch * 0.65);
|
|
259
|
+
ctx.fillRect(x + cw - 7, y, 3, ch * 0.65);
|
|
260
|
+
// Seat — warm cushion
|
|
261
|
+
ctx.fillStyle = '#CD853F';
|
|
262
|
+
ctx.fillRect(x + 2, y + ch * 0.40, cw - 4, ch * 0.30);
|
|
263
|
+
// Cushion highlight
|
|
264
|
+
ctx.fillStyle = '#D4946A';
|
|
265
|
+
ctx.fillRect(x + 4, y + ch * 0.42, cw - 8, ch * 0.12);
|
|
266
|
+
// Cushion stitch
|
|
267
|
+
ctx.fillStyle = '#B8734A';
|
|
268
|
+
ctx.fillRect(x + cw / 2 - 1, y + ch * 0.44, 2, ch * 0.20);
|
|
269
|
+
// Legs
|
|
270
|
+
ctx.fillStyle = '#6B4226';
|
|
271
|
+
ctx.fillRect(x + 4, y + ch * 0.70, 3, ch * 0.30);
|
|
272
|
+
ctx.fillRect(x + cw - 7, y + ch * 0.70, 3, ch * 0.30);
|
|
273
|
+
// Front legs
|
|
274
|
+
ctx.fillStyle = '#7A4E2C';
|
|
275
|
+
ctx.fillRect(x + 6, y + ch - 4, 3, 4);
|
|
276
|
+
ctx.fillRect(x + cw - 9, y + ch - 4, 3, 4);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Render café table + laptop + latte (in front of character) */
|
|
280
|
+
renderDesk(ctx, s, agentState) {
|
|
281
|
+
const isWorking = ['typing', 'executing', 'browsing'].includes(agentState);
|
|
282
|
+
|
|
283
|
+
// ── Café table — warm wood with rounded feel ──
|
|
284
|
+
// Top surface
|
|
285
|
+
ctx.fillStyle = '#D4A56A';
|
|
286
|
+
ctx.fillRect(s.deskX, s.deskY, s.deskW, 7);
|
|
287
|
+
// Top highlight
|
|
288
|
+
ctx.fillStyle = '#E0B878';
|
|
289
|
+
ctx.fillRect(s.deskX + 2, s.deskY, s.deskW - 4, 2);
|
|
290
|
+
// Front face
|
|
291
|
+
ctx.fillStyle = '#8B5E3C';
|
|
292
|
+
ctx.fillRect(s.deskX, s.deskY + 7, s.deskW, s.deskH - 7);
|
|
293
|
+
// Apron trim
|
|
294
|
+
ctx.fillStyle = '#7A4E2C';
|
|
295
|
+
ctx.fillRect(s.deskX, s.deskY + 7, s.deskW, 3);
|
|
296
|
+
// Side edges
|
|
297
|
+
ctx.fillStyle = '#7A4E2C';
|
|
298
|
+
ctx.fillRect(s.deskX, s.deskY + 10, 2, s.deskH - 12);
|
|
299
|
+
ctx.fillRect(s.deskX + s.deskW - 2, s.deskY + 10, 2, s.deskH - 12);
|
|
300
|
+
// Bottom trim
|
|
301
|
+
ctx.fillStyle = '#6B4226';
|
|
302
|
+
ctx.fillRect(s.deskX + 2, s.deskY + s.deskH - 2, s.deskW - 4, 2);
|
|
303
|
+
// Legs — tapered wooden
|
|
304
|
+
ctx.fillStyle = '#6B4226';
|
|
305
|
+
ctx.fillRect(s.deskX + 8, s.deskY + s.deskH, 4, 5);
|
|
306
|
+
ctx.fillRect(s.deskX + s.deskW - 12, s.deskY + s.deskH, 4, 5);
|
|
307
|
+
// Wood grain hints
|
|
308
|
+
ctx.fillStyle = 'rgba(0,0,0,0.04)';
|
|
309
|
+
ctx.fillRect(s.deskX + 10, s.deskY + 2, 20, 1);
|
|
310
|
+
ctx.fillRect(s.deskX + 40, s.deskY + 4, 25, 1);
|
|
311
|
+
|
|
312
|
+
// ── Laptop on table ──
|
|
313
|
+
const monType = isWorking && Math.floor(this.screenFlicker / 400) % 2 === 0 ? 'laptop_active' : 'laptop';
|
|
314
|
+
ctx.drawImage(this.gen.generateFurniture(monType), s.monX, s.monY, s.monW, s.monH);
|
|
315
|
+
|
|
316
|
+
// Screen glow when working
|
|
317
|
+
if (isWorking) {
|
|
318
|
+
ctx.fillStyle = 'rgba(100,200,255,0.04)';
|
|
319
|
+
ctx.fillRect(s.monX - 6, s.monY - 3, s.monW + 12, s.monH + 6);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Latte with art ──
|
|
323
|
+
const lx = s.cupX, ly = s.cupY;
|
|
324
|
+
// Saucer
|
|
325
|
+
ctx.fillStyle = '#ECE5D8';
|
|
326
|
+
ctx.fillRect(lx - 3, ly + s.cupH - 2, s.cupW + 4, 4);
|
|
327
|
+
// Cup body — wider, shorter
|
|
328
|
+
ctx.fillStyle = '#FFFFFF';
|
|
329
|
+
ctx.fillRect(lx, ly + 2, s.cupW - 2, s.cupH - 2);
|
|
330
|
+
// Latte art (top)
|
|
331
|
+
ctx.fillStyle = '#D4A574';
|
|
332
|
+
ctx.fillRect(lx, ly + 2, s.cupW - 2, 4);
|
|
333
|
+
// Heart latte art
|
|
334
|
+
ctx.fillStyle = '#F5E6D3';
|
|
335
|
+
ctx.fillRect(lx + 3, ly + 3, 3, 2);
|
|
336
|
+
ctx.fillRect(lx + 8, ly + 3, 3, 2);
|
|
337
|
+
ctx.fillRect(lx + 4, ly + 5, 6, 1);
|
|
338
|
+
// Handle
|
|
339
|
+
ctx.fillStyle = '#DDD';
|
|
340
|
+
ctx.fillRect(lx + s.cupW - 2, ly + 6, 3, 5);
|
|
341
|
+
ctx.fillRect(lx + s.cupW, ly + 5, 2, 1);
|
|
342
|
+
ctx.fillRect(lx + s.cupW, ly + 11, 2, 1);
|
|
343
|
+
|
|
344
|
+
// ── Steam from latte ──
|
|
345
|
+
this.steamParticles.forEach(p => {
|
|
346
|
+
const px = lx + s.cupW / 2 + p.x;
|
|
347
|
+
const py = ly + p.y * 15;
|
|
348
|
+
const alpha = Math.max(0, 1 - p.life / p.maxLife) * 0.35;
|
|
349
|
+
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
|
|
350
|
+
ctx.beginPath();
|
|
351
|
+
ctx.arc(px, py, 2 + p.life * 0.02, 0, Math.PI * 2);
|
|
352
|
+
ctx.fill();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ── Small pastry plate ──
|
|
356
|
+
const ppX = s.deskX + s.deskW / 2 + 8;
|
|
357
|
+
const ppY = s.deskY - 10;
|
|
358
|
+
// Plate
|
|
359
|
+
ctx.fillStyle = '#F5F0E8';
|
|
360
|
+
ctx.fillRect(ppX, ppY + 6, 16, 4);
|
|
361
|
+
// Croissant
|
|
362
|
+
ctx.fillStyle = '#D4A056';
|
|
363
|
+
ctx.fillRect(ppX + 2, ppY + 2, 12, 5);
|
|
364
|
+
ctx.fillStyle = '#C4903E';
|
|
365
|
+
ctx.fillRect(ppX + 4, ppY + 3, 8, 3);
|
|
366
|
+
ctx.fillStyle = '#E0B878';
|
|
367
|
+
ctx.fillRect(ppX + 5, ppY + 2, 4, 1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── Wall decorations ──
|
|
371
|
+
|
|
372
|
+
_drawHangingLamp(ctx, x, y) {
|
|
373
|
+
// Warm pendant lamp
|
|
374
|
+
ctx.fillStyle = '#333';
|
|
375
|
+
ctx.fillRect(x - 1, y, 2, 28);
|
|
376
|
+
// Shade
|
|
377
|
+
ctx.fillStyle = '#F39C12';
|
|
378
|
+
ctx.beginPath();
|
|
379
|
+
ctx.moveTo(x - 12, 28);
|
|
380
|
+
ctx.lineTo(x + 12, 28);
|
|
381
|
+
ctx.lineTo(x + 6, 40);
|
|
382
|
+
ctx.lineTo(x - 6, 40);
|
|
383
|
+
ctx.closePath();
|
|
384
|
+
ctx.fill();
|
|
385
|
+
// Warm glow
|
|
386
|
+
const lampGlow = ctx.createRadialGradient(x, 40, 2, x, 40, 55);
|
|
387
|
+
lampGlow.addColorStop(0, 'rgba(255, 220, 130, 0.18)');
|
|
388
|
+
lampGlow.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
|
389
|
+
ctx.fillStyle = lampGlow;
|
|
390
|
+
ctx.fillRect(x - 55, 28, 110, 80);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
_drawMenuBoard(ctx, x, y, w, h) {
|
|
394
|
+
// Chalkboard menu
|
|
395
|
+
ctx.fillStyle = '#5D3D1A';
|
|
396
|
+
ctx.fillRect(x - 3, y - 3, w + 6, h + 6);
|
|
397
|
+
ctx.fillStyle = '#2C3E2C';
|
|
398
|
+
ctx.fillRect(x, y, w, h);
|
|
399
|
+
// Chalk text
|
|
400
|
+
ctx.fillStyle = 'rgba(255,255,255,0.8)';
|
|
401
|
+
ctx.font = '6px monospace';
|
|
402
|
+
ctx.fillText('MENU', x + 20, y + 10);
|
|
403
|
+
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
|
404
|
+
ctx.font = '5px monospace';
|
|
405
|
+
ctx.fillText('Latte $4', x + 4, y + 20);
|
|
406
|
+
ctx.fillText('Mocha $5', x + 4, y + 28);
|
|
407
|
+
ctx.fillText('Espresso $3', x + 4, y + 36);
|
|
408
|
+
// Chalk doodle
|
|
409
|
+
ctx.fillStyle = 'rgba(255,200,100,0.5)';
|
|
410
|
+
ctx.fillRect(x + 48, y + 18, 8, 8);
|
|
411
|
+
ctx.fillStyle = 'rgba(255,150,50,0.4)';
|
|
412
|
+
ctx.fillRect(x + 50, y + 20, 4, 4);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
_drawCafeArt(ctx, x, y, w, h) {
|
|
416
|
+
// Framed art print — coffee cup illustration
|
|
417
|
+
ctx.fillStyle = '#8B5E3C';
|
|
418
|
+
ctx.fillRect(x - 2, y - 2, w + 4, h + 4);
|
|
419
|
+
ctx.fillStyle = '#FFF8E8';
|
|
420
|
+
ctx.fillRect(x, y, w, h);
|
|
421
|
+
// Simple coffee cup illustration
|
|
422
|
+
ctx.fillStyle = '#D4A574';
|
|
423
|
+
ctx.fillRect(x + 15, y + 10, 20, 16);
|
|
424
|
+
ctx.fillStyle = '#8B5E3C';
|
|
425
|
+
ctx.fillRect(x + 15, y + 10, 20, 3);
|
|
426
|
+
ctx.fillStyle = '#ECE5D8';
|
|
427
|
+
ctx.fillRect(x + 12, y + 26, 26, 3);
|
|
428
|
+
// Steam swirls
|
|
429
|
+
ctx.fillStyle = 'rgba(180,160,140,0.4)';
|
|
430
|
+
ctx.fillRect(x + 20, y + 5, 2, 5);
|
|
431
|
+
ctx.fillRect(x + 25, y + 6, 2, 4);
|
|
432
|
+
ctx.fillRect(x + 29, y + 5, 2, 5);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
_drawClock(ctx, x, y) {
|
|
436
|
+
// Vintage round clock
|
|
437
|
+
ctx.fillStyle = '#5D3D1A';
|
|
438
|
+
ctx.beginPath(); ctx.arc(x, y + 10, 12, 0, Math.PI * 2); ctx.fill();
|
|
439
|
+
ctx.fillStyle = '#FFF8E8';
|
|
440
|
+
ctx.beginPath(); ctx.arc(x, y + 10, 10, 0, Math.PI * 2); ctx.fill();
|
|
441
|
+
// Hour marks
|
|
442
|
+
ctx.fillStyle = '#8B5E3C';
|
|
443
|
+
for (let i = 0; i < 12; i++) {
|
|
444
|
+
const a = i * Math.PI / 6;
|
|
445
|
+
ctx.fillRect(x + Math.cos(a) * 8 - 0.5, y + 10 + Math.sin(a) * 8 - 0.5, 1, 1);
|
|
446
|
+
}
|
|
447
|
+
const now = new Date(), hr = now.getHours() % 12, mn = now.getMinutes();
|
|
448
|
+
ctx.strokeStyle = '#5D3D1A'; ctx.lineWidth = 1.5;
|
|
449
|
+
const ha = (hr + mn / 60) * Math.PI / 6 - Math.PI / 2;
|
|
450
|
+
ctx.beginPath(); ctx.moveTo(x, y + 10); ctx.lineTo(x + Math.cos(ha) * 5, y + 10 + Math.sin(ha) * 5); ctx.stroke();
|
|
451
|
+
ctx.lineWidth = 1;
|
|
452
|
+
const ma = mn * Math.PI / 30 - Math.PI / 2;
|
|
453
|
+
ctx.beginPath(); ctx.moveTo(x, y + 10); ctx.lineTo(x + Math.cos(ma) * 7, y + 10 + Math.sin(ma) * 7); ctx.stroke();
|
|
454
|
+
ctx.fillStyle = '#8B5E3C';
|
|
455
|
+
ctx.beginPath(); ctx.arc(x, y + 10, 1.5, 0, Math.PI * 2); ctx.fill();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ── Side-view floor furniture ──
|
|
459
|
+
|
|
460
|
+
_drawSideBookshelf(ctx, x, y, depth, h, side) {
|
|
461
|
+
const frameColor = '#6B4226', backColor = '#8B5E3C', shelfColor = '#5D3A1A';
|
|
462
|
+
const bookColors = ['#C0392B', '#2980B9', '#27AE60', '#F39C12', '#8E44AD', '#16A085', '#D35400'];
|
|
463
|
+
ctx.fillStyle = frameColor; ctx.fillRect(x, y, depth, h);
|
|
464
|
+
ctx.fillStyle = backColor; ctx.fillRect(x + 2, y + 2, depth - 4, h - 4);
|
|
465
|
+
const edgeX = side === 'left' ? x + depth - 3 : x;
|
|
466
|
+
ctx.fillStyle = shelfColor; ctx.fillRect(edgeX, y, 3, h);
|
|
467
|
+
const shelfCount = 4;
|
|
468
|
+
const shelfGap = Math.floor((h - 6) / shelfCount);
|
|
469
|
+
for (let i = 0; i < shelfCount; i++) {
|
|
470
|
+
const sy = y + 4 + i * shelfGap;
|
|
471
|
+
ctx.fillStyle = shelfColor; ctx.fillRect(x + 2, sy, depth - 4, 2);
|
|
472
|
+
for (let b = 0; b < 3; b++) {
|
|
473
|
+
const bh = shelfGap - 6;
|
|
474
|
+
const bw = 2 + (b % 2);
|
|
475
|
+
const bx = x + 4 + b * (bw + 1);
|
|
476
|
+
ctx.fillStyle = bookColors[(i * 3 + b) % bookColors.length];
|
|
477
|
+
ctx.fillRect(bx, sy - bh, bw, bh);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
ctx.fillStyle = shelfColor;
|
|
481
|
+
ctx.fillRect(x - 1, y - 2, depth + 2, 3);
|
|
482
|
+
ctx.fillRect(x - 1, y + h - 1, depth + 2, 3);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
_drawSideSofa(ctx, x, y, depth, h, side) {
|
|
486
|
+
// Warm leather sofa
|
|
487
|
+
const sofaColor = '#A0522D', sofaDark = '#8B4513', sofaLight = '#B8623D';
|
|
488
|
+
ctx.fillStyle = sofaDark; ctx.fillRect(x, y, depth, h);
|
|
489
|
+
ctx.fillStyle = sofaColor; ctx.fillRect(x + 3, y + 6, depth - 6, h - 10);
|
|
490
|
+
// Back rest
|
|
491
|
+
ctx.fillStyle = sofaDark; ctx.fillRect(x + 2, y, depth - 4, 10);
|
|
492
|
+
ctx.fillStyle = sofaLight; ctx.fillRect(x + 4, y + 2, depth - 8, 6);
|
|
493
|
+
// Seat cushion
|
|
494
|
+
ctx.fillStyle = sofaLight; ctx.fillRect(x + 5, y + 14, depth - 10, h - 22);
|
|
495
|
+
// Seams
|
|
496
|
+
ctx.fillStyle = sofaDark;
|
|
497
|
+
ctx.fillRect(x + 5, y + Math.floor(h * 0.45), depth - 10, 1);
|
|
498
|
+
ctx.fillRect(x + 5, y + Math.floor(h * 0.65), depth - 10, 1);
|
|
499
|
+
// Legs
|
|
500
|
+
ctx.fillStyle = '#4A3728';
|
|
501
|
+
ctx.fillRect(x + 4, y + h - 2, 4, 4);
|
|
502
|
+
ctx.fillRect(x + depth - 8, y + h - 2, 4, 4);
|
|
503
|
+
// Throw pillow
|
|
504
|
+
ctx.fillStyle = '#E8D4B8';
|
|
505
|
+
const px = side === 'left' ? x + depth - 12 : x + 4;
|
|
506
|
+
ctx.fillRect(px, y + 4, 8, 12);
|
|
507
|
+
ctx.fillStyle = '#D4C0A4'; ctx.fillRect(px + 1, y + 5, 6, 10);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
_drawSideCoffeeCounter(ctx, x, y, depth, h, side) {
|
|
511
|
+
// Wooden coffee counter / bar
|
|
512
|
+
const woodColor = '#7B5B3A', woodDark = '#5D3A1A', woodLight = '#9B7B5A';
|
|
513
|
+
ctx.fillStyle = woodLight; ctx.fillRect(x, y, depth, 4);
|
|
514
|
+
ctx.fillStyle = woodColor; ctx.fillRect(x, y + 4, depth, h - 4);
|
|
515
|
+
// Wood grain
|
|
516
|
+
ctx.fillStyle = woodDark;
|
|
517
|
+
for (let ly = y + 12; ly < y + h; ly += 10) {
|
|
518
|
+
ctx.fillRect(x + 2, ly, depth - 4, 1);
|
|
519
|
+
}
|
|
520
|
+
// Front edge
|
|
521
|
+
const edgeX = side === 'right' ? x : x + depth - 3;
|
|
522
|
+
ctx.fillStyle = woodDark; ctx.fillRect(edgeX, y, 3, h);
|
|
523
|
+
// Top highlight
|
|
524
|
+
ctx.fillStyle = woodLight; ctx.fillRect(x, y, depth, 2);
|
|
525
|
+
// Coffee machine on top
|
|
526
|
+
ctx.fillStyle = '#708090'; ctx.fillRect(x + 3, y - 22, 14, 22);
|
|
527
|
+
ctx.fillStyle = '#5A6A7A'; ctx.fillRect(x + 5, y - 20, 10, 8);
|
|
528
|
+
// Power LED
|
|
529
|
+
ctx.fillStyle = '#2ECC71'; ctx.fillRect(x + 7, y - 15, 2, 2);
|
|
530
|
+
// Group head / drip area
|
|
531
|
+
ctx.fillStyle = '#555'; ctx.fillRect(x + 5, y - 10, 10, 8);
|
|
532
|
+
ctx.fillStyle = '#444'; ctx.fillRect(x + 7, y - 8, 6, 4);
|
|
533
|
+
// Tiny cup
|
|
534
|
+
ctx.fillStyle = '#FFF'; ctx.fillRect(x + 7, y - 4, 4, 3);
|
|
535
|
+
ctx.fillStyle = '#8B4513'; ctx.fillRect(x + 8, y - 3, 2, 1);
|
|
536
|
+
// Steam
|
|
537
|
+
ctx.fillStyle = 'rgba(200,200,200,0.4)';
|
|
538
|
+
ctx.fillRect(x + 8, y - 7, 1, 3);
|
|
539
|
+
ctx.fillRect(x + 11, y - 8, 1, 3);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
_drawSidePastryCase(ctx, x, y, depth, h, side) {
|
|
543
|
+
// Glass pastry display case
|
|
544
|
+
// Frame
|
|
545
|
+
ctx.fillStyle = '#8B5E3C';
|
|
546
|
+
ctx.fillRect(x, y, depth, h);
|
|
547
|
+
// Glass panel (translucent)
|
|
548
|
+
ctx.fillStyle = 'rgba(200, 220, 240, 0.25)';
|
|
549
|
+
ctx.fillRect(x + 2, y + 2, depth - 4, h - 4);
|
|
550
|
+
// Front edge
|
|
551
|
+
const edgeX = side === 'right' ? x : x + depth - 3;
|
|
552
|
+
ctx.fillStyle = '#6B4226';
|
|
553
|
+
ctx.fillRect(edgeX, y, 3, h);
|
|
554
|
+
// Shelves
|
|
555
|
+
ctx.fillStyle = '#7A4E2C';
|
|
556
|
+
ctx.fillRect(x + 2, y + Math.floor(h * 0.33), depth - 4, 2);
|
|
557
|
+
ctx.fillRect(x + 2, y + Math.floor(h * 0.66), depth - 4, 2);
|
|
558
|
+
// Pastries on each shelf
|
|
559
|
+
// Top shelf — muffins
|
|
560
|
+
ctx.fillStyle = '#D4A056';
|
|
561
|
+
ctx.fillRect(x + 5, y + 8, 6, 6);
|
|
562
|
+
ctx.fillRect(x + 13, y + 9, 5, 5);
|
|
563
|
+
ctx.fillStyle = '#8B4513';
|
|
564
|
+
ctx.fillRect(x + 5, y + 12, 6, 2);
|
|
565
|
+
// Middle shelf — croissants
|
|
566
|
+
const midY = y + Math.floor(h * 0.33) + 4;
|
|
567
|
+
ctx.fillStyle = '#E0B878';
|
|
568
|
+
ctx.fillRect(x + 4, midY, 8, 5);
|
|
569
|
+
ctx.fillRect(x + 14, midY + 1, 7, 4);
|
|
570
|
+
ctx.fillStyle = '#C4903E';
|
|
571
|
+
ctx.fillRect(x + 5, midY + 2, 6, 2);
|
|
572
|
+
// Bottom shelf — cookies
|
|
573
|
+
const botY = y + Math.floor(h * 0.66) + 4;
|
|
574
|
+
ctx.fillStyle = '#C49250';
|
|
575
|
+
for (let c = 0; c < 3; c++) {
|
|
576
|
+
ctx.fillRect(x + 4 + c * 7, botY, 5, 5);
|
|
577
|
+
}
|
|
578
|
+
// Chocolate chips
|
|
579
|
+
ctx.fillStyle = '#5D3A1A';
|
|
580
|
+
ctx.fillRect(x + 5, botY + 2, 2, 2);
|
|
581
|
+
ctx.fillRect(x + 13, botY + 1, 2, 2);
|
|
582
|
+
// Top surface
|
|
583
|
+
ctx.fillStyle = '#8B5E3C';
|
|
584
|
+
ctx.fillRect(x - 1, y - 2, depth + 2, 3);
|
|
585
|
+
// Small vase on top
|
|
586
|
+
ctx.fillStyle = '#C0392B';
|
|
587
|
+
ctx.fillRect(x + 8, y - 10, 6, 8);
|
|
588
|
+
ctx.fillStyle = '#27AE60';
|
|
589
|
+
ctx.fillRect(x + 9, y - 14, 2, 5);
|
|
590
|
+
ctx.fillRect(x + 12, y - 13, 2, 4);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (typeof window !== 'undefined') window.CafeScene = CafeScene;
|