@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.
@@ -0,0 +1,927 @@
1
+ /**
2
+ * SpriteGenerator.js — Programmatic pixel sprite generation
3
+ * Generates 32x32 pixel characters and scene elements using Canvas 2D API
4
+ * All art is code-generated, no external assets needed
5
+ */
6
+ class SpriteGenerator {
7
+ constructor() {
8
+ this.canvas = document.createElement('canvas');
9
+ this.ctx = this.canvas.getContext('2d');
10
+ this.cache = new Map();
11
+ }
12
+
13
+ // ── Color Palettes ────────────────────────────────────
14
+ static SKIN_TONES = ['#FFDFC4','#F0C08A','#D2956A','#8D5524','#4A2912'];
15
+ static HAIR_COLORS = ['#1A1A2E','#4A3728','#8B4513','#DAA520','#C41E3A','#2E8B57','#6A5ACD'];
16
+ static OUTFIT_COLORS = {
17
+ hoodie: ['#4A90D9','#E74C3C','#2ECC71','#9B59B6','#F39C12'],
18
+ shirt: ['#ECF0F1','#3498DB','#1ABC9C','#E67E22','#8E44AD'],
19
+ suit: ['#2C3E50','#34495E','#1A1A2E','#4A4A4A','#192a56'],
20
+ labcoat: ['#FFFFFF','#F5F5F5','#E8E8E8','#D4E6F1','#FDEBD0'],
21
+ tshirt: ['#E74C3C','#3498DB','#2ECC71','#F1C40F','#E91E63']
22
+ };
23
+
24
+ // ── Pixel Drawing Helpers ─────────────────────────────
25
+ px(x, y, color) {
26
+ this.ctx.fillStyle = color;
27
+ this.ctx.fillRect(x, y, 1, 1);
28
+ }
29
+
30
+ rect(x, y, w, h, color) {
31
+ this.ctx.fillStyle = color;
32
+ this.ctx.fillRect(x, y, w, h);
33
+ }
34
+
35
+ clear() {
36
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
37
+ }
38
+
39
+ // ── Darken/Lighten Color ──────────────────────────────
40
+ static shadeColor(color, percent) {
41
+ let r = parseInt(color.slice(1,3), 16);
42
+ let g = parseInt(color.slice(3,5), 16);
43
+ let b = parseInt(color.slice(5,7), 16);
44
+ r = Math.min(255, Math.max(0, Math.floor(r * (1 + percent))));
45
+ g = Math.min(255, Math.max(0, Math.floor(g * (1 + percent))));
46
+ b = Math.min(255, Math.max(0, Math.floor(b * (1 + percent))));
47
+ return `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`;
48
+ }
49
+
50
+ // ══════════════════════════════════════════════════════
51
+ // CHARACTER GENERATION
52
+ // ══════════════════════════════════════════════════════
53
+
54
+ /**
55
+ * Generate a complete character sprite sheet
56
+ * @param {Object} config - Character configuration
57
+ * @returns {HTMLCanvasElement} Sprite sheet with all animation frames
58
+ */
59
+ generateCharacter(config) {
60
+ const cacheKey = JSON.stringify(config);
61
+ if (this.cache.has(cacheKey)) return this.cache.get(cacheKey);
62
+
63
+ const states = ['idle','idle2','typing','typing2','thinking','thinking2',
64
+ 'walking','walking2','sleeping','error','waving','waving2',
65
+ 'coffee','coffee2','browsing','browsing2'];
66
+ const sheetW = 32 * states.length;
67
+ const sheetH = 32;
68
+
69
+ const sheet = document.createElement('canvas');
70
+ sheet.width = sheetW;
71
+ sheet.height = sheetH;
72
+ const sctx = sheet.getContext('2d');
73
+
74
+ states.forEach((state, i) => {
75
+ this.canvas.width = 32;
76
+ this.canvas.height = 32;
77
+ this.clear();
78
+ this._drawCharacterFrame(config, state);
79
+ sctx.drawImage(this.canvas, i * 32, 0);
80
+ });
81
+
82
+ this.cache.set(cacheKey, sheet);
83
+ return sheet;
84
+ }
85
+
86
+ _drawCharacterFrame(cfg, state) {
87
+ const skin = cfg.skinColor || SpriteGenerator.SKIN_TONES[0];
88
+ const skinDark = SpriteGenerator.shadeColor(skin, -0.15);
89
+ const hairColor = cfg.hairColor || SpriteGenerator.HAIR_COLORS[0];
90
+ const outfitType = cfg.outfitType || 'hoodie';
91
+ const outfitIdx = cfg.outfitColorIdx || 0;
92
+ const outfitColor = (SpriteGenerator.OUTFIT_COLORS[outfitType] || SpriteGenerator.OUTFIT_COLORS.hoodie)[outfitIdx];
93
+ const outfitDark = SpriteGenerator.shadeColor(outfitColor, -0.2);
94
+
95
+ // Base sitting position for most states
96
+ const isSitting = !['walking','walking2','waving','waving2','executing'].includes(state);
97
+ const isSleeping = state === 'sleeping';
98
+
99
+ if (isSleeping) {
100
+ this._drawSleepingCharacter(skin, skinDark, hairColor, outfitColor, outfitDark, cfg);
101
+ return;
102
+ }
103
+
104
+ if (state.startsWith('walking')) {
105
+ this._drawWalkingCharacter(skin, skinDark, hairColor, outfitColor, outfitDark, cfg, state);
106
+ return;
107
+ }
108
+
109
+ // ── Shadow ──
110
+ this.rect(10, 29, 12, 2, 'rgba(0,0,0,0.15)');
111
+
112
+ // ── Legs (sitting) ──
113
+ this.rect(12, 25, 3, 4, '#2C3E50'); // left leg
114
+ this.rect(17, 25, 3, 4, '#2C3E50'); // right leg
115
+ // Shoes
116
+ this.rect(11, 28, 4, 2, '#1A1A2E');
117
+ this.rect(17, 28, 4, 2, '#1A1A2E');
118
+
119
+ // ── Body/Torso ──
120
+ this.rect(11, 17, 10, 8, outfitColor);
121
+ // Collar / neckline
122
+ this.rect(14, 17, 4, 1, skinDark);
123
+ // Outfit detail
124
+ if (outfitType === 'hoodie') {
125
+ this.rect(15, 18, 2, 3, outfitDark); // zipper
126
+ this.rect(11, 17, 2, 2, outfitDark); // hood shadow
127
+ this.rect(19, 17, 2, 2, outfitDark);
128
+ } else if (outfitType === 'suit') {
129
+ this.rect(15, 18, 2, 6, '#ECF0F1'); // shirt underneath
130
+ this.px(15, 19, '#E74C3C'); // tie knot
131
+ this.px(15, 20, '#E74C3C');
132
+ this.px(15, 21, '#E74C3C');
133
+ } else if (outfitType === 'labcoat') {
134
+ this.rect(11, 17, 10, 8, '#FFFFFF');
135
+ this.rect(14, 18, 4, 5, '#D4E6F1'); // inner shirt
136
+ this.rect(11, 22, 4, 3, SpriteGenerator.shadeColor('#FFFFFF', -0.05));
137
+ this.rect(17, 22, 4, 3, SpriteGenerator.shadeColor('#FFFFFF', -0.05));
138
+ }
139
+
140
+ // ── Arms ──
141
+ const frame2 = state.endsWith('2');
142
+ if (state.startsWith('typing')) {
143
+ // Arms forward on desk
144
+ const armY = frame2 ? 22 : 21;
145
+ this.rect(8, 20, 3, 3, outfitColor);
146
+ this.rect(7, armY, 2, 2, skin);
147
+ this.rect(21, 20, 3, 3, outfitColor);
148
+ this.rect(23, armY, 2, 2, skin);
149
+ } else if (state.startsWith('waving')) {
150
+ // Left arm normal, right arm up waving
151
+ this.rect(8, 20, 3, 4, outfitColor);
152
+ this.rect(8, 24, 2, 1, skin);
153
+ const waveY = frame2 ? 11 : 13;
154
+ this.rect(21, 17, 3, 3, outfitColor);
155
+ this.rect(22, waveY, 2, 4, outfitColor);
156
+ this.rect(22, waveY - 1, 2, 2, skin); // hand
157
+ } else if (state.startsWith('coffee')) {
158
+ // Left arm holds coffee
159
+ this.rect(8, 19, 3, 4, outfitColor);
160
+ this.rect(7, 20, 2, 2, skin);
161
+ // Coffee cup in hand
162
+ this.rect(5, 19, 3, 3, '#8B4513');
163
+ this.rect(5, 18, 3, 1, '#D4A574');
164
+ if (!frame2) this.px(6, 17, '#CCCCCC'); // steam
165
+ if (frame2) { this.px(5, 16, '#CCCCCC'); this.px(7, 17, '#CCCCCC'); }
166
+ // Right arm
167
+ this.rect(21, 20, 3, 4, outfitColor);
168
+ this.rect(22, 24, 2, 1, skin);
169
+ } else {
170
+ // Default arms at sides
171
+ this.rect(8, 19, 3, 5, outfitColor);
172
+ this.rect(8, 24, 2, 1, skin);
173
+ this.rect(21, 19, 3, 5, outfitColor);
174
+ this.rect(22, 24, 2, 1, skin);
175
+ }
176
+
177
+ // ── Head ──
178
+ this.rect(12, 8, 8, 9, skin);
179
+ // Ears
180
+ this.rect(11, 11, 1, 3, skin);
181
+ this.rect(20, 11, 1, 3, skin);
182
+ // Neck
183
+ this.rect(14, 16, 4, 2, skin);
184
+
185
+ // ── Eyes ──
186
+ if (state.startsWith('error')) {
187
+ // X eyes
188
+ this.px(14, 12, '#E74C3C'); this.px(15, 13, '#E74C3C');
189
+ this.px(15, 12, '#E74C3C'); this.px(14, 13, '#E74C3C');
190
+ this.px(17, 12, '#E74C3C'); this.px(18, 13, '#E74C3C');
191
+ this.px(18, 12, '#E74C3C'); this.px(17, 13, '#E74C3C');
192
+ } else if (state.startsWith('thinking')) {
193
+ // Looking up
194
+ this.rect(14, 11, 2, 2, '#1A1A2E');
195
+ this.rect(17, 11, 2, 2, '#1A1A2E');
196
+ this.px(14, 11, '#FFFFFF');
197
+ this.px(17, 11, '#FFFFFF');
198
+ } else {
199
+ // Normal eyes
200
+ this.rect(14, 12, 2, 2, '#FFFFFF');
201
+ this.rect(17, 12, 2, 2, '#FFFFFF');
202
+ this.px(15, 12, '#1A1A2E'); // pupil
203
+ this.px(18, 12, '#1A1A2E');
204
+ this.px(15, 13, '#1A1A2E');
205
+ this.px(18, 13, '#1A1A2E');
206
+ }
207
+
208
+ // ── Mouth ──
209
+ if (state.startsWith('error')) {
210
+ this.rect(15, 15, 3, 1, '#E74C3C'); // frown
211
+ } else if (state.startsWith('typing') || state.startsWith('browsing')) {
212
+ this.px(16, 15, '#C0392B'); // focused dot
213
+ } else {
214
+ this.rect(15, 15, 2, 1, '#C0392B'); // normal smile
215
+ }
216
+
217
+ // ── Hair ──
218
+ this._drawHair(cfg.hairType || 0, hairColor);
219
+
220
+ // ── Accessory ──
221
+ this._drawAccessory(cfg.accessory, skin);
222
+
223
+ // ── State-specific overlays ──
224
+ if (state.startsWith('thinking')) {
225
+ // Thought bubble
226
+ const bx = 22; const by = frame2 ? 3 : 5;
227
+ this.rect(bx, by, 6, 4, '#FFFFFF');
228
+ this.rect(bx+1, by-1, 4, 1, '#FFFFFF');
229
+ this.rect(bx+1, by+4, 4, 1, '#FFFFFF');
230
+ this.ctx.fillStyle = '#666';
231
+ this.ctx.fillRect(bx+1, by+1, 1, 1);
232
+ this.ctx.fillRect(bx+3, by+1, 1, 1);
233
+ this.ctx.fillRect(bx+5, by+1, 1, 1);
234
+ // dots leading to bubble
235
+ this.px(21, by+4, '#FFFFFF');
236
+ this.px(20, by+5, '#CCCCCC');
237
+ }
238
+
239
+ if (state.startsWith('error')) {
240
+ // Red X above head
241
+ const ex = 14; const ey = frame2 ? 1 : 2;
242
+ this.px(ex, ey, '#E74C3C'); this.px(ex+4, ey, '#E74C3C');
243
+ this.px(ex+1, ey+1, '#E74C3C'); this.px(ex+3, ey+1, '#E74C3C');
244
+ this.px(ex+2, ey+2, '#E74C3C');
245
+ this.px(ex+1, ey+3, '#E74C3C'); this.px(ex+3, ey+3, '#E74C3C');
246
+ this.px(ex, ey+4, '#E74C3C'); this.px(ex+4, ey+4, '#E74C3C');
247
+ }
248
+ }
249
+
250
+ _drawSleepingCharacter(skin, skinDark, hairColor, outfitColor, outfitDark, cfg) {
251
+ // Character slumped on desk
252
+ this.rect(6, 27, 20, 3, 'rgba(0,0,0,0.1)'); // shadow
253
+
254
+ // Desk surface
255
+ this.rect(4, 22, 24, 3, '#8B6914');
256
+ this.rect(4, 25, 2, 5, '#6B4F12');
257
+ this.rect(24, 25, 2, 5, '#6B4F12');
258
+
259
+ // Body slumped forward
260
+ this.rect(10, 18, 10, 5, outfitColor);
261
+ // Arms on desk
262
+ this.rect(7, 19, 4, 3, outfitColor);
263
+ this.rect(6, 20, 2, 2, skin);
264
+ this.rect(19, 19, 4, 3, outfitColor);
265
+ this.rect(22, 20, 2, 2, skin);
266
+
267
+ // Head on arms (rotated/tilted)
268
+ this.rect(11, 13, 8, 6, skin);
269
+ this.rect(10, 15, 1, 2, skin);
270
+
271
+ // Closed eyes
272
+ this.rect(13, 16, 2, 1, '#1A1A2E');
273
+ this.rect(17, 16, 2, 1, '#1A1A2E');
274
+
275
+ // Hair
276
+ this.rect(11, 12, 8, 2, hairColor);
277
+ this.rect(10, 13, 2, 3, hairColor);
278
+
279
+ // ZZZ
280
+ this.ctx.fillStyle = '#87CEEB';
281
+ this.ctx.font = '5px monospace';
282
+ this.ctx.fillText('z', 22, 12);
283
+ this.ctx.fillText('Z', 24, 8);
284
+ this.ctx.fillText('Z', 26, 4);
285
+ }
286
+
287
+ _drawWalkingCharacter(skin, skinDark, hairColor, outfitColor, outfitDark, cfg, state) {
288
+ const frame2 = state.endsWith('2');
289
+
290
+ // Shadow
291
+ this.rect(10, 30, 12, 1, 'rgba(0,0,0,0.15)');
292
+
293
+ // Legs walking animation
294
+ if (frame2) {
295
+ this.rect(13, 25, 3, 4, '#2C3E50');
296
+ this.rect(17, 23, 3, 4, '#2C3E50');
297
+ this.rect(12, 29, 4, 2, '#1A1A2E');
298
+ this.rect(17, 27, 4, 2, '#1A1A2E');
299
+ } else {
300
+ this.rect(13, 23, 3, 4, '#2C3E50');
301
+ this.rect(17, 25, 3, 4, '#2C3E50');
302
+ this.rect(13, 27, 4, 2, '#1A1A2E');
303
+ this.rect(16, 29, 4, 2, '#1A1A2E');
304
+ }
305
+
306
+ // Body
307
+ this.rect(11, 16, 10, 8, outfitColor);
308
+ this.rect(14, 16, 4, 1, skinDark);
309
+
310
+ // Arms swinging
311
+ if (frame2) {
312
+ this.rect(8, 17, 3, 6, outfitColor);
313
+ this.rect(8, 23, 2, 1, skin);
314
+ this.rect(21, 18, 3, 5, outfitColor);
315
+ this.rect(22, 23, 2, 1, skin);
316
+ } else {
317
+ this.rect(8, 18, 3, 5, outfitColor);
318
+ this.rect(8, 23, 2, 1, skin);
319
+ this.rect(21, 17, 3, 6, outfitColor);
320
+ this.rect(22, 23, 2, 1, skin);
321
+ }
322
+
323
+ // Head
324
+ this.rect(12, 7, 8, 9, skin);
325
+ this.rect(11, 10, 1, 3, skin);
326
+ this.rect(20, 10, 1, 3, skin);
327
+ this.rect(14, 15, 4, 2, skin);
328
+
329
+ // Eyes
330
+ this.rect(14, 11, 2, 2, '#FFFFFF');
331
+ this.rect(17, 11, 2, 2, '#FFFFFF');
332
+ this.px(15, 12, '#1A1A2E');
333
+ this.px(18, 12, '#1A1A2E');
334
+
335
+ // Mouth
336
+ this.rect(15, 14, 2, 1, '#C0392B');
337
+
338
+ // Hair
339
+ this._drawHair(cfg.hairType || 0, hairColor, -1);
340
+
341
+ // Accessory
342
+ this._drawAccessory(cfg.accessory, skin);
343
+ }
344
+
345
+ _drawHair(type, color, yOffset = 0) {
346
+ const y = 7 + yOffset;
347
+ const dark = SpriteGenerator.shadeColor(color, -0.2);
348
+
349
+ switch (type) {
350
+ case 0: // Short messy
351
+ this.rect(11, y, 10, 3, color);
352
+ this.rect(10, y+1, 1, 2, color);
353
+ this.rect(21, y+1, 1, 2, color);
354
+ this.px(12, y-1, color);
355
+ this.px(15, y-1, color);
356
+ this.px(18, y-1, color);
357
+ this.rect(11, y, 2, 1, dark);
358
+ break;
359
+ case 1: // Spiky
360
+ this.rect(11, y, 10, 3, color);
361
+ this.px(12, y-2, color); this.px(14, y-2, color);
362
+ this.px(17, y-2, color); this.px(19, y-2, color);
363
+ this.px(11, y-1, color); this.px(13, y-1, color);
364
+ this.px(16, y-1, color); this.px(18, y-1, color); this.px(20, y-1, color);
365
+ break;
366
+ case 2: // Long
367
+ this.rect(11, y, 10, 3, color);
368
+ this.rect(10, y, 1, 8, color);
369
+ this.rect(21, y, 1, 8, color);
370
+ this.rect(10, y-1, 12, 1, color);
371
+ this.px(9, y+2, color);
372
+ this.px(22, y+2, color);
373
+ break;
374
+ case 3: // Curly
375
+ this.rect(11, y, 10, 3, color);
376
+ this.rect(10, y-1, 12, 2, color);
377
+ this.px(9, y, color); this.px(22, y, color);
378
+ this.px(10, y+3, color); this.px(21, y+3, color);
379
+ this.px(11, y+4, color); this.px(20, y+4, color);
380
+ this.rect(10, y, 1, 4, color);
381
+ this.rect(21, y, 1, 4, color);
382
+ break;
383
+ case 4: // Bald / buzz cut
384
+ this.rect(12, y, 8, 2, dark);
385
+ this.rect(11, y+1, 1, 1, dark);
386
+ this.rect(20, y+1, 1, 1, dark);
387
+ break;
388
+ }
389
+ }
390
+
391
+ _drawAccessory(type, skinColor) {
392
+ switch (type) {
393
+ case 'glasses':
394
+ this.rect(13, 12, 3, 2, '#333');
395
+ this.rect(16, 12, 1, 1, '#666');
396
+ this.rect(17, 12, 3, 2, '#333');
397
+ this.px(13, 12, '#87CEEB');
398
+ this.px(17, 12, '#87CEEB');
399
+ break;
400
+ case 'hat':
401
+ this.rect(10, 5, 12, 3, '#2C3E50');
402
+ this.rect(8, 7, 16, 2, '#2C3E50');
403
+ this.rect(12, 5, 8, 1, '#E74C3C'); // band
404
+ break;
405
+ case 'headphones':
406
+ this.rect(10, 7, 2, 5, '#333');
407
+ this.rect(20, 7, 2, 5, '#333');
408
+ this.rect(10, 5, 12, 2, '#444');
409
+ this.rect(9, 8, 2, 3, '#E74C3C');
410
+ this.rect(21, 8, 2, 3, '#E74C3C');
411
+ break;
412
+ case 'cap':
413
+ this.rect(10, 6, 12, 3, '#3498DB');
414
+ this.rect(8, 8, 6, 2, '#3498DB');
415
+ this.rect(11, 6, 3, 1, SpriteGenerator.shadeColor('#3498DB', -0.2));
416
+ break;
417
+ }
418
+ }
419
+
420
+ // ══════════════════════════════════════════════════════
421
+ // PET GENERATION (16x16)
422
+ // ══════════════════════════════════════════════════════
423
+
424
+ generatePet(type, frame = 0, color = null) {
425
+ const key = `pet_${type}_${frame}_${color || 'default'}`;
426
+ if (this.cache.has(key)) return this.cache.get(key);
427
+
428
+ const c = document.createElement('canvas');
429
+ c.width = 16; c.height = 16;
430
+ const ctx = c.getContext('2d');
431
+
432
+ if (type === 'cat') this._drawCat(ctx, frame, color);
433
+ else if (type === 'dog') this._drawDog(ctx, frame, color);
434
+ else if (type === 'robot') this._drawRobotPet(ctx, frame, color);
435
+ else if (type === 'bird') this._drawBird(ctx, frame, color);
436
+ else if (type === 'hamster') this._drawHamster(ctx, frame, color);
437
+
438
+ this.cache.set(key, c);
439
+ return c;
440
+ }
441
+
442
+ _drawCat(ctx, frame, baseColor) {
443
+ const f = (x,y,w,h,c) => { ctx.fillStyle = c; ctx.fillRect(x,y,w,h); };
444
+ const color = baseColor || '#FF9900';
445
+ const dark = SpriteGenerator.shadeColor(color, -0.25);
446
+ // Body
447
+ f(4, 8, 8, 5, color);
448
+ // Head
449
+ f(3, 3, 8, 6, color);
450
+ // Ears
451
+ f(3, 1, 2, 3, color); f(9, 1, 2, 3, color);
452
+ f(4, 2, 1, 1, '#FFB6C1'); f(9, 2, 1, 1, '#FFB6C1');
453
+ // Eyes
454
+ f(4, 5, 2, 2, '#2ECC71');
455
+ f(8, 5, 2, 2, '#2ECC71');
456
+ f(5, 5, 1, 1, '#000'); f(9, 5, 1, 1, '#000');
457
+ // Nose
458
+ f(6, 7, 2, 1, '#FFB6C1');
459
+ // Tail
460
+ const ty = frame % 2 === 0 ? 6 : 7;
461
+ f(12, ty, 2, 1, color); f(13, ty-1, 1, 2, color); f(14, ty-2, 1, 2, color);
462
+ // Legs
463
+ f(4, 13, 2, 2, dark); f(9, 13, 2, 2, dark);
464
+ // Stripes
465
+ f(5, 4, 1, 1, dark); f(8, 4, 1, 1, dark);
466
+ f(5, 9, 1, 1, dark); f(7, 9, 1, 1, dark); f(9, 9, 1, 1, dark);
467
+ }
468
+
469
+ _drawDog(ctx, frame, baseColor) {
470
+ const f = (x,y,w,h,c) => { ctx.fillStyle = c; ctx.fillRect(x,y,w,h); };
471
+ const color = baseColor || '#D2956A';
472
+ const dark = SpriteGenerator.shadeColor(color, -0.25);
473
+ // Body
474
+ f(4, 8, 8, 5, color);
475
+ // Head
476
+ f(3, 3, 8, 6, color);
477
+ // Floppy ears
478
+ f(2, 4, 2, 4, dark); f(10, 4, 2, 4, dark);
479
+ // Eyes
480
+ f(4, 5, 2, 2, '#000'); f(8, 5, 2, 2, '#000');
481
+ f(4, 5, 1, 1, '#FFF'); f(8, 5, 1, 1, '#FFF');
482
+ // Nose
483
+ f(6, 7, 2, 1, '#000');
484
+ // Tongue
485
+ if (frame % 2 === 0) f(7, 8, 1, 2, '#FF6B8A');
486
+ // Tail
487
+ const ty = frame % 2 === 0 ? 5 : 7;
488
+ f(12, ty, 1, 3, color); f(13, ty, 1, 2, dark);
489
+ // Legs
490
+ f(4, 13, 2, 2, dark); f(9, 13, 2, 2, dark);
491
+ // Spots
492
+ f(6, 4, 2, 1, '#FFF'); f(5, 9, 3, 2, '#FFF');
493
+ }
494
+
495
+ _drawRobotPet(ctx, frame, baseColor) {
496
+ const f = (x,y,w,h,c) => { ctx.fillStyle = c; ctx.fillRect(x,y,w,h); };
497
+ const bodyColor = baseColor || '#95A5A6';
498
+ const headColor = SpriteGenerator.shadeColor(bodyColor, 0.15);
499
+ const darkColor = SpriteGenerator.shadeColor(bodyColor, -0.15);
500
+ // Body
501
+ f(4, 7, 8, 6, bodyColor);
502
+ f(5, 8, 6, 4, darkColor);
503
+ // Head
504
+ f(4, 2, 8, 6, headColor);
505
+ f(5, 3, 6, 4, bodyColor);
506
+ // Antenna
507
+ f(7, 0, 2, 3, darkColor);
508
+ f(6, 0, 4, 1, frame % 2 === 0 ? '#E74C3C' : '#2ECC71');
509
+ // Eyes (LED)
510
+ const eyeColor = frame % 2 === 0 ? '#00FF00' : '#00CC00';
511
+ f(5, 4, 2, 2, eyeColor); f(9, 4, 2, 2, eyeColor);
512
+ // Mouth grid
513
+ f(6, 6, 4, 1, '#555');
514
+ f(6, 6, 1, 1, eyeColor); f(8, 6, 1, 1, eyeColor);
515
+ // Legs
516
+ f(5, 13, 2, 2, darkColor); f(9, 13, 2, 2, darkColor);
517
+ // Bolts
518
+ f(4, 9, 1, 1, '#F1C40F'); f(11, 9, 1, 1, '#F1C40F');
519
+ }
520
+
521
+ _drawBird(ctx, frame, baseColor) {
522
+ const f = (x,y,w,h,c) => { ctx.fillStyle = c; ctx.fillRect(x,y,w,h); };
523
+ const body = baseColor || '#4FC3F7';
524
+ const dark = SpriteGenerator.shadeColor(body, -0.35);
525
+ const belly = SpriteGenerator.shadeColor(body, 0.40);
526
+ // Body (centered)
527
+ f(5, 6, 6, 5, body);
528
+ f(6, 7, 4, 3, belly);
529
+ // Head (left side — bird faces LEFT like all other pets)
530
+ f(5, 3, 5, 4, body);
531
+ // Eye
532
+ f(6, 4, 2, 2, '#FFF');
533
+ f(6, 4, 1, 1, '#000');
534
+ // Beak (left side)
535
+ f(3, 5, 2, 1, '#FF9800');
536
+ f(4, 6, 1, 1, '#FF9800');
537
+ // Wings (animated)
538
+ if (frame % 2 === 0) {
539
+ // Wings up
540
+ f(10, 4, 2, 3, dark);
541
+ f(4, 4, 1, 3, dark);
542
+ } else {
543
+ // Wings down
544
+ f(10, 7, 2, 3, dark);
545
+ f(4, 7, 1, 3, dark);
546
+ }
547
+ // Tail feathers (right side)
548
+ f(11, 7, 2, 2, dark);
549
+ f(12, 6, 2, 1, dark);
550
+ // Legs (tiny)
551
+ f(6, 11, 1, 2, '#FF9800');
552
+ f(8, 11, 1, 2, '#FF9800');
553
+ // Feet
554
+ f(5, 13, 2, 1, '#FF9800');
555
+ f(8, 13, 2, 1, '#FF9800');
556
+ }
557
+
558
+ _drawHamster(ctx, frame, baseColor) {
559
+ const f = (x,y,w,h,c) => { ctx.fillStyle = c; ctx.fillRect(x,y,w,h); };
560
+ const body = baseColor || '#F5DEB3';
561
+ const dark = SpriteGenerator.shadeColor(body, -0.15);
562
+ const cheek = '#FFB6C1';
563
+ // Body (round)
564
+ f(4, 7, 8, 6, body);
565
+ f(3, 8, 1, 4, body);
566
+ f(12, 8, 1, 4, body);
567
+ // Head
568
+ f(4, 3, 8, 5, body);
569
+ f(3, 4, 1, 3, body);
570
+ f(12, 4, 1, 3, body);
571
+ // Ears (round)
572
+ f(4, 1, 2, 3, body);
573
+ f(10, 1, 2, 3, body);
574
+ f(5, 2, 1, 1, cheek);
575
+ f(10, 2, 1, 1, cheek);
576
+ // Eyes (beady)
577
+ f(5, 5, 2, 2, '#000');
578
+ f(9, 5, 2, 2, '#000');
579
+ f(5, 5, 1, 1, '#FFF');
580
+ f(9, 5, 1, 1, '#FFF');
581
+ // Nose
582
+ f(7, 6, 2, 1, '#FF9999');
583
+ // Cheeks (puffy)
584
+ f(3, 6, 2, 2, cheek);
585
+ f(11, 6, 2, 2, cheek);
586
+ // Whiskers
587
+ f(2, 6, 1, 1, '#AAA');
588
+ f(13, 6, 1, 1, '#AAA');
589
+ // Legs
590
+ f(5, 13, 2, 2, dark);
591
+ f(9, 13, 2, 2, dark);
592
+ // Belly stripe
593
+ f(6, 9, 4, 3, '#FFF5E6');
594
+ // Tail (tiny)
595
+ const ty = frame % 2 === 0 ? 10 : 11;
596
+ f(12, ty, 2, 1, dark);
597
+ f(13, ty - 1, 1, 1, dark);
598
+ }
599
+
600
+ // ══════════════════════════════════════════════════════
601
+ // FURNITURE & SCENE ELEMENTS
602
+ // ══════════════════════════════════════════════════════
603
+
604
+ generateFurniture(type) {
605
+ const key = `furn_${type}`;
606
+ if (this.cache.has(key)) return this.cache.get(key);
607
+
608
+ const c = document.createElement('canvas');
609
+ const ctx = c.getContext('2d');
610
+ const f = (x,y,w,h,color) => { ctx.fillStyle = color; ctx.fillRect(x,y,w,h); };
611
+
612
+ switch(type) {
613
+ case 'desk':
614
+ // 3/4 top-down view desk — top surface visible + front face
615
+ c.width = 48; c.height = 20;
616
+ // Top surface (visible from above) — light wood
617
+ f(2, 0, 44, 8, '#D4A56A');
618
+ f(0, 2, 2, 4, '#D4A56A'); // left bevel
619
+ f(46, 2, 2, 4, '#D4A56A'); // right bevel
620
+ // Surface edge highlight
621
+ f(2, 0, 44, 1, '#E0B878');
622
+ // Front face — darker wood
623
+ f(2, 8, 44, 10, '#8B5E3C');
624
+ f(0, 8, 2, 8, '#7A5232'); // left side face
625
+ f(46, 8, 2, 8, '#7A5232'); // right side face
626
+ // Front face shadow/detail
627
+ f(2, 16, 44, 2, '#6B4422');
628
+ // Legs visible at bottom
629
+ f(4, 18, 3, 2, '#6B4422');
630
+ f(41, 18, 3, 2, '#6B4422');
631
+ // Wood grain on top
632
+ f(8, 2, 12, 1, 'rgba(0,0,0,0.06)');
633
+ f(24, 4, 16, 1, 'rgba(0,0,0,0.06)');
634
+ break;
635
+
636
+ case 'monitor':
637
+ case 'laptop':
638
+ // iMac BACK view — screen faces character (away from viewer)
639
+ // We see: silver aluminum back, Apple logo, thin edge, stand + base
640
+ c.width = 20; c.height = 20;
641
+ // Main back panel — silver aluminum
642
+ f(2, 0, 16, 13, '#C8CDD3');
643
+ // Slight edge shading
644
+ f(2, 0, 1, 13, '#B8BEC5'); // left edge
645
+ f(17, 0, 1, 13, '#D4D8DD'); // right edge highlight
646
+ f(2, 0, 16, 1, '#D8DDE2'); // top edge highlight
647
+ // Bottom edge (slightly curved/tapered)
648
+ f(3, 12, 14, 1, '#B0B6BD');
649
+ // Apple logo — centered on back (glowing white-ish)
650
+ f(9, 4, 2, 1, '#E8ECF0'); // apple top
651
+ f(8, 5, 4, 3, '#E8ECF0'); // apple body
652
+ f(9, 8, 2, 1, '#E8ECF0'); // apple bottom
653
+ f(10, 3, 1, 1, '#E8ECF0'); // stem
654
+ f(11, 3, 1, 1, '#E8ECF0'); // leaf
655
+ // Subtle surface texture lines
656
+ f(4, 3, 3, 1, 'rgba(0,0,0,0.03)');
657
+ f(13, 6, 3, 1, 'rgba(0,0,0,0.03)');
658
+ f(5, 10, 4, 1, 'rgba(0,0,0,0.03)');
659
+ // Stand neck
660
+ f(9, 13, 2, 3, '#A8AEB5');
661
+ f(8, 13, 1, 2, '#9EA5AC');
662
+ f(11, 13, 1, 2, '#B0B6BD');
663
+ // Stand base (ellipse)
664
+ f(5, 16, 10, 2, '#B8BEC5');
665
+ f(4, 17, 12, 2, '#A8AEB5');
666
+ f(6, 16, 8, 1, '#D0D4D8'); // highlight
667
+ break;
668
+
669
+ case 'monitor_active':
670
+ case 'laptop_active':
671
+ // iMac BACK view — active (Apple logo glows brighter, subtle screen light spill)
672
+ c.width = 20; c.height = 20;
673
+ // Main back panel
674
+ f(2, 0, 16, 13, '#C8CDD3');
675
+ f(2, 0, 1, 13, '#B8BEC5');
676
+ f(17, 0, 1, 13, '#D4D8DD');
677
+ f(2, 0, 16, 1, '#D8DDE2');
678
+ f(3, 12, 14, 1, '#B0B6BD');
679
+ // Apple logo — glowing brighter when active
680
+ f(9, 4, 2, 1, '#FFFFFF');
681
+ f(8, 5, 4, 3, '#FFFFFF');
682
+ f(9, 8, 2, 1, '#FFFFFF');
683
+ f(10, 3, 1, 1, '#FFFFFF');
684
+ f(11, 3, 1, 1, '#FFFFFF');
685
+ // Logo glow aura
686
+ f(7, 4, 1, 4, 'rgba(255,255,255,0.3)');
687
+ f(12, 4, 1, 4, 'rgba(255,255,255,0.3)');
688
+ f(8, 3, 4, 1, 'rgba(255,255,255,0.2)');
689
+ f(8, 9, 4, 1, 'rgba(255,255,255,0.2)');
690
+ // Screen light spilling from front edge (bottom glow)
691
+ f(3, 13, 14, 1, 'rgba(100,200,255,0.15)');
692
+ f(4, 12, 12, 1, 'rgba(100,200,255,0.08)');
693
+ // Surface texture
694
+ f(4, 3, 3, 1, 'rgba(0,0,0,0.03)');
695
+ f(13, 6, 3, 1, 'rgba(0,0,0,0.03)');
696
+ // Stand
697
+ f(9, 13, 2, 3, '#A8AEB5');
698
+ f(8, 13, 1, 2, '#9EA5AC');
699
+ f(11, 13, 1, 2, '#B0B6BD');
700
+ // Base
701
+ f(5, 16, 10, 2, '#B8BEC5');
702
+ f(4, 17, 12, 2, '#A8AEB5');
703
+ f(6, 16, 8, 1, '#D0D4D8');
704
+ break;
705
+
706
+ case 'chair':
707
+ // 3/4 view chair — seat visible from above + backrest
708
+ c.width = 16; c.height = 18;
709
+ // Backrest (behind, visible top)
710
+ f(3, 0, 10, 3, '#8B4513');
711
+ f(2, 1, 1, 2, '#7A3A10');
712
+ f(13, 1, 1, 2, '#7A3A10');
713
+ // Seat (visible from above — ellipse-ish)
714
+ f(2, 5, 12, 6, '#A0522D');
715
+ f(1, 6, 1, 4, '#8B4513');
716
+ f(14, 6, 1, 4, '#8B4513');
717
+ // Seat cushion highlight
718
+ f(4, 6, 8, 3, '#B0623D');
719
+ // Legs (visible below seat)
720
+ f(3, 11, 2, 5, '#6B3410');
721
+ f(11, 11, 2, 5, '#6B3410');
722
+ // Wheel dots at bottom
723
+ f(2, 16, 2, 2, '#555');
724
+ f(12, 16, 2, 2, '#555');
725
+ f(7, 17, 2, 1, '#555');
726
+ break;
727
+
728
+ case 'coffee_cup':
729
+ c.width = 8; c.height = 10;
730
+ f(1, 3, 5, 6, '#FFFFFF');
731
+ f(1, 3, 5, 1, '#D4A574');
732
+ f(6, 5, 2, 2, '#FFFFFF');
733
+ f(0, 9, 7, 1, '#CCCCCC');
734
+ // Steam
735
+ f(2, 1, 1, 2, 'rgba(200,200,200,0.5)');
736
+ f(4, 0, 1, 3, 'rgba(200,200,200,0.5)');
737
+ break;
738
+
739
+ case 'plant':
740
+ c.width = 12; c.height = 16;
741
+ f(4, 10, 4, 6, '#8B4513'); // pot
742
+ f(3, 10, 6, 2, '#A0522D');
743
+ f(5, 4, 2, 6, '#228B22'); // stem
744
+ f(2, 2, 3, 4, '#32CD32'); // leaves
745
+ f(7, 3, 3, 3, '#2ECC71');
746
+ f(4, 1, 4, 2, '#3CB371');
747
+ f(1, 4, 2, 2, '#228B22');
748
+ f(9, 2, 2, 2, '#228B22');
749
+ break;
750
+
751
+ case 'server_rack':
752
+ c.width = 20; c.height = 32;
753
+ f(0, 0, 20, 32, '#2C3E50');
754
+ f(1, 1, 18, 30, '#1A1A2E');
755
+ for (let i = 0; i < 6; i++) {
756
+ f(2, 2 + i*5, 16, 4, '#34495E');
757
+ f(3, 3 + i*5, 2, 2, i % 2 === 0 ? '#2ECC71' : '#3498DB');
758
+ f(14, 3 + i*5, 3, 1, '#555');
759
+ f(14, 4 + i*5, 3, 1, '#555');
760
+ }
761
+ break;
762
+
763
+ case 'bookshelf':
764
+ c.width = 24; c.height = 32;
765
+ f(0, 0, 24, 32, '#5D3D1A');
766
+ f(0, 7, 24, 2, '#6B4F12');
767
+ f(0, 15, 24, 2, '#6B4F12');
768
+ f(0, 23, 24, 2, '#6B4F12');
769
+ // Books
770
+ const bookColors = ['#E74C3C','#3498DB','#2ECC71','#F39C12','#9B59B6','#1ABC9C','#E67E22'];
771
+ for (let shelf = 0; shelf < 3; shelf++) {
772
+ let bx = 1;
773
+ for (let b = 0; b < 5; b++) {
774
+ const bw = 2 + Math.floor(Math.random() * 2);
775
+ const bh = 5 + Math.floor(Math.random() * 2);
776
+ const by = (shelf * 8) + (7 - bh);
777
+ f(bx, by, bw, bh, bookColors[(shelf*5+b) % bookColors.length]);
778
+ bx += bw + 1;
779
+ }
780
+ }
781
+ break;
782
+
783
+ case 'lamp':
784
+ c.width = 10; c.height = 16;
785
+ f(4, 12, 3, 4, '#8B4513');
786
+ f(3, 12, 5, 1, '#A0522D');
787
+ f(5, 4, 1, 8, '#666');
788
+ f(2, 1, 7, 4, '#F39C12');
789
+ f(3, 0, 5, 1, '#F1C40F');
790
+ f(3, 2, 5, 2, '#FFE082');
791
+ break;
792
+ }
793
+
794
+ this.cache.set(key, c);
795
+ return c;
796
+ }
797
+
798
+ // ══════════════════════════════════════════════════════
799
+ // SCENE BACKGROUND GENERATION
800
+ // ══════════════════════════════════════════════════════
801
+
802
+ generateSceneBg(type, width, height) {
803
+ const c = document.createElement('canvas');
804
+ c.width = width; c.height = height;
805
+ const ctx = c.getContext('2d');
806
+ const f = (x,y,w,h,color) => { ctx.fillStyle = color; ctx.fillRect(x,y,w,h); };
807
+
808
+ switch(type) {
809
+ case 'office': {
810
+ const floorY = Math.round(height * 0.40);
811
+ const floorH = height - floorY;
812
+ // Wall — subtle vertical gradient for depth
813
+ const wallGrad = ctx.createLinearGradient(0, 0, 0, floorY);
814
+ wallGrad.addColorStop(0, '#F0E6D4');
815
+ wallGrad.addColorStop(1, '#E2D4BE');
816
+ ctx.fillStyle = wallGrad;
817
+ ctx.fillRect(0, 0, width, floorY);
818
+ // Wall subtle texture — faint horizontal lines
819
+ for (let y = 20; y < floorY; y += 40) {
820
+ f(0, y, width, 1, 'rgba(0,0,0,0.02)');
821
+ }
822
+ // Baseboard / 踢脚线
823
+ f(0, floorY - 6, width, 6, '#8B7355');
824
+ f(0, floorY - 7, width, 1, '#9C8465');
825
+ // Floor — gradient for perspective depth
826
+ const floorGrad = ctx.createLinearGradient(0, floorY, 0, height);
827
+ floorGrad.addColorStop(0, '#C4A56E');
828
+ floorGrad.addColorStop(0.5, '#B89860');
829
+ floorGrad.addColorStop(1, '#A88B52');
830
+ ctx.fillStyle = floorGrad;
831
+ ctx.fillRect(0, floorY, width, floorH);
832
+ // Floor line
833
+ f(0, floorY, width, 2, '#A08050');
834
+ // Floor wood planks — horizontal boards
835
+ for (let y = floorY + 28; y < height; y += 28) {
836
+ f(0, y, width, 1, 'rgba(0,0,0,0.06)');
837
+ }
838
+ // Floor vertical grain lines
839
+ for (let x = 0; x < width; x += 32) {
840
+ f(x, floorY, 1, floorH, 'rgba(0,0,0,0.03)');
841
+ }
842
+ // Subtle floor highlight near wall
843
+ ctx.fillStyle = 'rgba(255,255,220,0.06)';
844
+ ctx.fillRect(0, floorY, width, 20);
845
+ }
846
+ // Window — pushed down to avoid UI overlay
847
+ const ww = 80; const wh = 70; const wx = (width - ww) / 2;
848
+ const wy = Math.round(height * 0.08);
849
+ f(wx-2, wy, ww+4, wh+4, '#5D3D1A');
850
+ f(wx, wy+2, ww, wh, '#87CEEB');
851
+ f(wx + ww/2 - 1, wy+2, 2, wh, '#5D3D1A'); // divider
852
+ f(wx, wy+2 + wh/2 - 1, ww, 2, '#5D3D1A');
853
+ // Clouds
854
+ f(wx + 10, wy+15, 20, 8, '#FFFFFF');
855
+ f(wx + 14, wy+12, 12, 5, '#FFFFFF');
856
+ f(wx + 55, wy+25, 15, 6, '#F0F0F0');
857
+ f(wx + 58, wy+22, 9, 4, '#F0F0F0');
858
+ // Sun
859
+ f(wx + ww - 20, wy+8, 8, 8, '#F1C40F');
860
+ f(wx + ww - 18, wy+6, 4, 2, '#F39C12');
861
+ f(wx + ww - 22, wy+10, 2, 4, '#F39C12');
862
+ break;
863
+
864
+ case 'hacker':
865
+ // Dark room
866
+ f(0, 0, width, height, '#0D0D1A');
867
+ // Floor
868
+ f(0, height * 0.65, width, height * 0.35, '#151525');
869
+ // Grid lines on floor (perspective)
870
+ ctx.strokeStyle = 'rgba(0,255,255,0.05)';
871
+ ctx.lineWidth = 1;
872
+ for (let x = 0; x < width; x += 20) {
873
+ ctx.beginPath();
874
+ ctx.moveTo(x, height * 0.65);
875
+ ctx.lineTo(x, height);
876
+ ctx.stroke();
877
+ }
878
+ // Ambient glow
879
+ const grad = ctx.createRadialGradient(width/2, height * 0.5, 10, width/2, height * 0.5, width * 0.6);
880
+ grad.addColorStop(0, 'rgba(100, 0, 200, 0.1)');
881
+ grad.addColorStop(1, 'rgba(0, 0, 0, 0)');
882
+ ctx.fillStyle = grad;
883
+ ctx.fillRect(0, 0, width, height);
884
+ // Neon strip lights
885
+ f(0, height * 0.65 - 1, width, 1, '#00FFFF');
886
+ ctx.fillStyle = 'rgba(0, 255, 255, 0.03)';
887
+ ctx.fillRect(0, height * 0.55, width, height * 0.15);
888
+ break;
889
+
890
+ case 'cafe':
891
+ // Warm interior wall
892
+ f(0, 0, width, height * 0.55, '#8B6D5C');
893
+ f(0, 0, width, height * 0.55, 'rgba(255,200,100,0.1)');
894
+ // Wainscoting
895
+ f(0, height * 0.35, width, height * 0.2, '#6B4D3C');
896
+ // Floor
897
+ f(0, height * 0.55, width, height * 0.45, '#654321');
898
+ // Floor boards
899
+ for (let x = 0; x < width; x += 24) {
900
+ f(x, height * 0.55, 1, height * 0.45, 'rgba(0,0,0,0.1)');
901
+ }
902
+ // Large window
903
+ const cww = 120; const cwh = 70; const cwx = width - cww - 30;
904
+ f(cwx-3, 15, cww+6, cwh+6, '#5D3D1A');
905
+ f(cwx, 18, cww, cwh, '#4A6885');
906
+ // Rain outside
907
+ ctx.fillStyle = 'rgba(150,180,200,0.3)';
908
+ for (let i = 0; i < 30; i++) {
909
+ const rx = cwx + Math.random() * cww;
910
+ const ry = 18 + Math.random() * cwh;
911
+ ctx.fillRect(rx, ry, 1, 3);
912
+ }
913
+ // Warm light glow
914
+ const warmGrad = ctx.createRadialGradient(width * 0.3, height * 0.3, 5, width * 0.3, height * 0.3, 150);
915
+ warmGrad.addColorStop(0, 'rgba(255, 200, 100, 0.15)');
916
+ warmGrad.addColorStop(1, 'rgba(0, 0, 0, 0)');
917
+ ctx.fillStyle = warmGrad;
918
+ ctx.fillRect(0, 0, width, height);
919
+ break;
920
+ }
921
+
922
+ return c;
923
+ }
924
+ }
925
+
926
+ // Export for use
927
+ if (typeof window !== 'undefined') window.SpriteGenerator = SpriteGenerator;