@guinetik/gcanvas 1.0.1 → 1.0.2
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/demos/coordinates.html +698 -0
- package/demos/cube3d.html +23 -0
- package/demos/demos.css +17 -3
- package/demos/dino.html +42 -0
- package/demos/gameobjects.html +626 -0
- package/demos/index.html +17 -7
- package/demos/js/coordinates.js +840 -0
- package/demos/js/cube3d.js +789 -0
- package/demos/js/dino.js +1420 -0
- package/demos/js/gameobjects.js +176 -0
- package/demos/js/plane3d.js +256 -0
- package/demos/js/platformer.js +1579 -0
- package/demos/js/sphere3d.js +229 -0
- package/demos/js/sprite.js +473 -0
- package/demos/js/tde/accretiondisk.js +3 -3
- package/demos/js/tde/tidalstream.js +2 -2
- package/demos/plane3d.html +24 -0
- package/demos/platformer.html +43 -0
- package/demos/sphere3d.html +24 -0
- package/demos/sprite.html +18 -0
- package/docs/concepts/coordinate-system.md +384 -0
- package/docs/concepts/shapes-vs-gameobjects.md +187 -0
- package/docs/fluid-dynamics.md +99 -97
- package/package.json +1 -1
- package/src/game/game.js +11 -5
- package/src/game/objects/index.js +3 -0
- package/src/game/objects/platformer-scene.js +411 -0
- package/src/game/objects/scene.js +14 -0
- package/src/game/objects/sprite.js +529 -0
- package/src/game/pipeline.js +20 -16
- package/src/game/ui/theme.js +123 -121
- package/src/io/input.js +75 -45
- package/src/io/mouse.js +44 -19
- package/src/io/touch.js +35 -12
- package/src/shapes/cube3d.js +599 -0
- package/src/shapes/index.js +2 -0
- package/src/shapes/plane3d.js +687 -0
- package/src/shapes/sphere3d.js +75 -6
- package/src/util/camera2d.js +315 -0
- package/src/util/index.js +1 -0
- package/src/webgl/shaders/plane-shaders.js +332 -0
- package/src/webgl/shaders/sphere-shaders.js +4 -2
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { GameObject } from './go.js';
|
|
2
|
+
import { Painter } from '../../painter/painter.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sprite - A MovieClip-style GameObject with timeline animation
|
|
6
|
+
*
|
|
7
|
+
* A Sprite manages a collection of Shapes as frames in a timeline.
|
|
8
|
+
* Each frame displays a different Shape, creating frame-by-frame animation
|
|
9
|
+
* similar to Adobe Flash MovieClip.
|
|
10
|
+
*
|
|
11
|
+
* Supports named animations for complex sprite sheets:
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const sprite = new Sprite(game, {
|
|
15
|
+
* frameRate: 12,
|
|
16
|
+
* loop: true
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* // Add shapes as frames
|
|
20
|
+
* sprite.addFrame(new Circle({ radius: 20, color: 'red' }));
|
|
21
|
+
* sprite.addFrame(new Circle({ radius: 25, color: 'orange' }));
|
|
22
|
+
* sprite.addFrame(new Circle({ radius: 20, color: 'red' }));
|
|
23
|
+
*
|
|
24
|
+
* // Control playback
|
|
25
|
+
* sprite.play();
|
|
26
|
+
* sprite.gotoAndStop(0);
|
|
27
|
+
* sprite.gotoAndPlay(1);
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Named animations
|
|
31
|
+
* sprite.addAnimation('idle', [idleShape]);
|
|
32
|
+
* sprite.addAnimation('walk', [walkFrame1, walkFrame2, walkFrame3]);
|
|
33
|
+
* sprite.addAnimation('jump', [jumpShape]);
|
|
34
|
+
*
|
|
35
|
+
* sprite.playAnimation('walk'); // Plays walk animation
|
|
36
|
+
* sprite.playAnimation('idle'); // Switch to idle
|
|
37
|
+
*/
|
|
38
|
+
export class Sprite extends GameObject {
|
|
39
|
+
/**
|
|
40
|
+
* Creates a new Sprite
|
|
41
|
+
* @param {Game} game - The game instance
|
|
42
|
+
* @param {Object} options - Configuration options
|
|
43
|
+
* @param {number} [options.frameRate=12] - Frames per second for playback
|
|
44
|
+
* @param {boolean} [options.loop=true] - Whether to loop the animation
|
|
45
|
+
* @param {boolean} [options.autoPlay=false] - Whether to start playing automatically
|
|
46
|
+
* @param {Array<Shape>} [options.frames=[]] - Initial frames to add
|
|
47
|
+
*/
|
|
48
|
+
constructor(game, options = {}) {
|
|
49
|
+
super(game, options);
|
|
50
|
+
|
|
51
|
+
// Timeline properties
|
|
52
|
+
this._frames = [];
|
|
53
|
+
this._currentFrame = 0;
|
|
54
|
+
this._frameAccumulator = 0;
|
|
55
|
+
this._isPlaying = options.autoPlay || false;
|
|
56
|
+
this._loop = options.loop !== undefined ? options.loop : true;
|
|
57
|
+
this._frameRate = options.frameRate || 12; // frames per second
|
|
58
|
+
this._frameDuration = 1 / this._frameRate; // seconds per frame
|
|
59
|
+
|
|
60
|
+
// Named animations support
|
|
61
|
+
this._animations = new Map(); // name -> { frames: Shape[], loop: boolean, frameRate: number }
|
|
62
|
+
this._currentAnimation = null; // current animation name
|
|
63
|
+
|
|
64
|
+
// Add initial frames if provided
|
|
65
|
+
if (options.frames && Array.isArray(options.frames)) {
|
|
66
|
+
options.frames.forEach(frame => this.addFrame(frame));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ==================== Named Animations ====================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Adds a named animation
|
|
74
|
+
* @param {string} name - Animation name (e.g., 'walk', 'idle', 'jump')
|
|
75
|
+
* @param {Array<Shape>} frames - Array of shapes for this animation
|
|
76
|
+
* @param {Object} [options] - Animation options
|
|
77
|
+
* @param {boolean} [options.loop=true] - Whether this animation loops
|
|
78
|
+
* @param {number} [options.frameRate] - Frame rate for this animation (uses sprite's default if not set)
|
|
79
|
+
* @returns {Sprite} This sprite for chaining
|
|
80
|
+
*/
|
|
81
|
+
addAnimation(name, frames, options = {}) {
|
|
82
|
+
if (!name || typeof name !== 'string') {
|
|
83
|
+
throw new Error('Sprite.addAnimation: name is required');
|
|
84
|
+
}
|
|
85
|
+
if (!frames || !Array.isArray(frames) || frames.length === 0) {
|
|
86
|
+
throw new Error('Sprite.addAnimation: frames array is required');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Set parent reference for all frames
|
|
90
|
+
frames.forEach(frame => {
|
|
91
|
+
frame.parent = this;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this._animations.set(name, {
|
|
95
|
+
frames,
|
|
96
|
+
loop: options.loop !== undefined ? options.loop : true,
|
|
97
|
+
frameRate: options.frameRate || null, // null means use sprite's default
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Removes a named animation
|
|
105
|
+
* @param {string} name - Animation name to remove
|
|
106
|
+
* @returns {boolean} True if animation was found and removed
|
|
107
|
+
*/
|
|
108
|
+
removeAnimation(name) {
|
|
109
|
+
const anim = this._animations.get(name);
|
|
110
|
+
if (anim) {
|
|
111
|
+
anim.frames.forEach(frame => {
|
|
112
|
+
frame.parent = null;
|
|
113
|
+
});
|
|
114
|
+
this._animations.delete(name);
|
|
115
|
+
|
|
116
|
+
// If this was the current animation, clear it
|
|
117
|
+
if (this._currentAnimation === name) {
|
|
118
|
+
this._currentAnimation = null;
|
|
119
|
+
this._frames = [];
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Plays a named animation
|
|
128
|
+
* @param {string} name - Animation name to play
|
|
129
|
+
* @param {boolean} [restart=false] - If true, restarts animation even if already playing
|
|
130
|
+
* @returns {Sprite} This sprite for chaining
|
|
131
|
+
*/
|
|
132
|
+
playAnimation(name, restart = false) {
|
|
133
|
+
const anim = this._animations.get(name);
|
|
134
|
+
if (!anim) {
|
|
135
|
+
console.warn(`Sprite.playAnimation: animation '${name}' not found`);
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// If already playing this animation and not forcing restart, do nothing
|
|
140
|
+
if (this._currentAnimation === name && this._isPlaying && !restart) {
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Switch to this animation's frames
|
|
145
|
+
this._currentAnimation = name;
|
|
146
|
+
this._frames = anim.frames;
|
|
147
|
+
this._loop = anim.loop;
|
|
148
|
+
|
|
149
|
+
// Use animation-specific frame rate if set
|
|
150
|
+
if (anim.frameRate !== null) {
|
|
151
|
+
this._frameRate = anim.frameRate;
|
|
152
|
+
this._frameDuration = 1 / this._frameRate;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Reset and play
|
|
156
|
+
this._currentFrame = 0;
|
|
157
|
+
this._frameAccumulator = 0;
|
|
158
|
+
this._isPlaying = true;
|
|
159
|
+
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Stops and switches to a named animation at frame 0
|
|
165
|
+
* @param {string} name - Animation name
|
|
166
|
+
* @returns {Sprite} This sprite for chaining
|
|
167
|
+
*/
|
|
168
|
+
stopAnimation(name) {
|
|
169
|
+
const anim = this._animations.get(name);
|
|
170
|
+
if (!anim) {
|
|
171
|
+
console.warn(`Sprite.stopAnimation: animation '${name}' not found`);
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this._currentAnimation = name;
|
|
176
|
+
this._frames = anim.frames;
|
|
177
|
+
this._loop = anim.loop;
|
|
178
|
+
this._currentFrame = 0;
|
|
179
|
+
this._frameAccumulator = 0;
|
|
180
|
+
this._isPlaying = false;
|
|
181
|
+
|
|
182
|
+
return this;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Gets the current animation name
|
|
187
|
+
* @returns {string|null}
|
|
188
|
+
*/
|
|
189
|
+
get currentAnimationName() {
|
|
190
|
+
return this._currentAnimation;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Gets all animation names
|
|
195
|
+
* @returns {string[]}
|
|
196
|
+
*/
|
|
197
|
+
get animationNames() {
|
|
198
|
+
return Array.from(this._animations.keys());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Checks if an animation exists
|
|
203
|
+
* @param {string} name - Animation name
|
|
204
|
+
* @returns {boolean}
|
|
205
|
+
*/
|
|
206
|
+
hasAnimation(name) {
|
|
207
|
+
return this._animations.has(name);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Adds a Shape as a new frame to the timeline
|
|
212
|
+
* @param {Shape} shape - The Shape to add as a frame
|
|
213
|
+
* @returns {number} The frame index
|
|
214
|
+
*/
|
|
215
|
+
addFrame(shape) {
|
|
216
|
+
if (!shape) {
|
|
217
|
+
throw new Error('Sprite.addFrame: shape is required');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Set parent reference for transform hierarchy
|
|
221
|
+
shape.parent = this;
|
|
222
|
+
|
|
223
|
+
// Add to frames collection
|
|
224
|
+
this._frames.push(shape);
|
|
225
|
+
|
|
226
|
+
// Mark bounds as dirty since we added a new shape
|
|
227
|
+
this.markBoundsDirty();
|
|
228
|
+
|
|
229
|
+
return this._frames.length - 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Removes a frame at the specified index
|
|
234
|
+
* @param {number} index - The frame index to remove
|
|
235
|
+
* @returns {Shape|null} The removed shape, or null if index is invalid
|
|
236
|
+
*/
|
|
237
|
+
removeFrame(index) {
|
|
238
|
+
if (index < 0 || index >= this._frames.length) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const removed = this._frames.splice(index, 1)[0];
|
|
243
|
+
if (removed) {
|
|
244
|
+
removed.parent = null;
|
|
245
|
+
this.markBoundsDirty();
|
|
246
|
+
|
|
247
|
+
// Adjust current frame if necessary
|
|
248
|
+
if (this._currentFrame >= this._frames.length && this._frames.length > 0) {
|
|
249
|
+
this._currentFrame = this._frames.length - 1;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return removed;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Removes all frames from the timeline
|
|
258
|
+
*/
|
|
259
|
+
clearFrames() {
|
|
260
|
+
this._frames.forEach(frame => {
|
|
261
|
+
frame.parent = null;
|
|
262
|
+
});
|
|
263
|
+
this._frames = [];
|
|
264
|
+
this._currentFrame = 0;
|
|
265
|
+
this.markBoundsDirty();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Gets the total number of frames
|
|
270
|
+
* @returns {number}
|
|
271
|
+
*/
|
|
272
|
+
get totalFrames() {
|
|
273
|
+
return this._frames.length;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Gets the current frame index
|
|
278
|
+
* @returns {number}
|
|
279
|
+
*/
|
|
280
|
+
get currentFrame() {
|
|
281
|
+
return this._currentFrame;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Gets the current frame Shape
|
|
286
|
+
* @returns {Shape|null}
|
|
287
|
+
*/
|
|
288
|
+
get currentShape() {
|
|
289
|
+
return this._frames[this._currentFrame] || null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Gets all frames
|
|
294
|
+
* @returns {Array<Shape>}
|
|
295
|
+
*/
|
|
296
|
+
get frames() {
|
|
297
|
+
return this._frames;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Gets whether the sprite is currently playing
|
|
302
|
+
* @returns {boolean}
|
|
303
|
+
*/
|
|
304
|
+
get isPlaying() {
|
|
305
|
+
return this._isPlaying;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Gets whether the sprite will loop
|
|
310
|
+
* @returns {boolean}
|
|
311
|
+
*/
|
|
312
|
+
get loop() {
|
|
313
|
+
return this._loop;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Sets whether the sprite will loop
|
|
318
|
+
* @param {boolean} value
|
|
319
|
+
*/
|
|
320
|
+
set loop(value) {
|
|
321
|
+
this._loop = value;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Gets the frame rate (frames per second)
|
|
326
|
+
* @returns {number}
|
|
327
|
+
*/
|
|
328
|
+
get frameRate() {
|
|
329
|
+
return this._frameRate;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Sets the frame rate (frames per second)
|
|
334
|
+
* @param {number} value
|
|
335
|
+
*/
|
|
336
|
+
set frameRate(value) {
|
|
337
|
+
if (value <= 0) {
|
|
338
|
+
throw new Error('Sprite.frameRate must be greater than 0');
|
|
339
|
+
}
|
|
340
|
+
this._frameRate = value;
|
|
341
|
+
this._frameDuration = 1 / value;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Starts playing the timeline
|
|
346
|
+
* @returns {Sprite} This sprite for chaining
|
|
347
|
+
*/
|
|
348
|
+
play() {
|
|
349
|
+
this._isPlaying = true;
|
|
350
|
+
return this;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Pauses the timeline at the current frame
|
|
355
|
+
* @returns {Sprite} This sprite for chaining
|
|
356
|
+
*/
|
|
357
|
+
pause() {
|
|
358
|
+
this._isPlaying = false;
|
|
359
|
+
return this;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Stops the timeline and resets to frame 0
|
|
364
|
+
* @returns {Sprite} This sprite for chaining
|
|
365
|
+
*/
|
|
366
|
+
stop() {
|
|
367
|
+
this._isPlaying = false;
|
|
368
|
+
this._currentFrame = 0;
|
|
369
|
+
this._frameAccumulator = 0;
|
|
370
|
+
return this;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Rewinds to the first frame without stopping playback
|
|
375
|
+
* @returns {Sprite} This sprite for chaining
|
|
376
|
+
*/
|
|
377
|
+
rewind() {
|
|
378
|
+
this._currentFrame = 0;
|
|
379
|
+
this._frameAccumulator = 0;
|
|
380
|
+
return this;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Jumps to a specific frame without affecting playback state
|
|
385
|
+
* @param {number} frame - The frame index to jump to
|
|
386
|
+
* @returns {Sprite} This sprite for chaining
|
|
387
|
+
*/
|
|
388
|
+
goto(frame) {
|
|
389
|
+
if (this._frames.length === 0) {
|
|
390
|
+
return this;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Clamp frame to valid range
|
|
394
|
+
this._currentFrame = Math.max(0, Math.min(frame, this._frames.length - 1));
|
|
395
|
+
this._frameAccumulator = 0;
|
|
396
|
+
return this;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Jumps to a specific frame and stops playback
|
|
401
|
+
* @param {number} frame - The frame index to jump to
|
|
402
|
+
* @returns {Sprite} This sprite for chaining
|
|
403
|
+
*/
|
|
404
|
+
gotoAndStop(frame) {
|
|
405
|
+
this.goto(frame);
|
|
406
|
+
this.pause();
|
|
407
|
+
return this;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Jumps to a specific frame and starts playback
|
|
412
|
+
* @param {number} frame - The frame index to jump to
|
|
413
|
+
* @returns {Sprite} This sprite for chaining
|
|
414
|
+
*/
|
|
415
|
+
gotoAndPlay(frame) {
|
|
416
|
+
this.goto(frame);
|
|
417
|
+
this.play();
|
|
418
|
+
return this;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Updates the sprite timeline
|
|
423
|
+
* @param {number} dt - Delta time in seconds
|
|
424
|
+
*/
|
|
425
|
+
update(dt) {
|
|
426
|
+
super.update(dt);
|
|
427
|
+
|
|
428
|
+
if (!this._isPlaying || this._frames.length === 0) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Accumulate time
|
|
433
|
+
this._frameAccumulator += dt;
|
|
434
|
+
|
|
435
|
+
// Advance frames if enough time has passed
|
|
436
|
+
while (this._frameAccumulator >= this._frameDuration) {
|
|
437
|
+
this._frameAccumulator -= this._frameDuration;
|
|
438
|
+
this._advanceFrame();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Update current frame if it has an update method
|
|
442
|
+
const currentShape = this.currentShape;
|
|
443
|
+
if (currentShape && typeof currentShape.update === 'function') {
|
|
444
|
+
currentShape.update(dt);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Advances to the next frame
|
|
450
|
+
* @private
|
|
451
|
+
*/
|
|
452
|
+
_advanceFrame() {
|
|
453
|
+
this._currentFrame++;
|
|
454
|
+
|
|
455
|
+
// Handle end of timeline
|
|
456
|
+
if (this._currentFrame >= this._frames.length) {
|
|
457
|
+
if (this._loop) {
|
|
458
|
+
this._currentFrame = 0;
|
|
459
|
+
} else {
|
|
460
|
+
this._currentFrame = this._frames.length - 1;
|
|
461
|
+
this._isPlaying = false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Renders the current frame
|
|
468
|
+
*/
|
|
469
|
+
draw() {
|
|
470
|
+
super.draw();
|
|
471
|
+
|
|
472
|
+
const currentShape = this.currentShape;
|
|
473
|
+
if (!currentShape) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Only render if the shape is visible
|
|
478
|
+
if (currentShape.visible === false) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Render the current frame's shape
|
|
483
|
+
Painter.save();
|
|
484
|
+
|
|
485
|
+
// Apply shape's transform if needed (shapes handle their own transforms in render())
|
|
486
|
+
currentShape.render();
|
|
487
|
+
|
|
488
|
+
Painter.restore();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Calculates bounds based on all frames
|
|
493
|
+
* @returns {Object} Bounding box {x, y, width, height}
|
|
494
|
+
*/
|
|
495
|
+
calculateBounds() {
|
|
496
|
+
if (this._frames.length === 0) {
|
|
497
|
+
return { x: this.x, y: this.y, width: 0, height: 0 };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Calculate union of all frame bounds
|
|
501
|
+
let minX = Infinity;
|
|
502
|
+
let minY = Infinity;
|
|
503
|
+
let maxX = -Infinity;
|
|
504
|
+
let maxY = -Infinity;
|
|
505
|
+
|
|
506
|
+
this._frames.forEach(frame => {
|
|
507
|
+
const bounds = frame.getBounds();
|
|
508
|
+
minX = Math.min(minX, bounds.x);
|
|
509
|
+
minY = Math.min(minY, bounds.y);
|
|
510
|
+
maxX = Math.max(maxX, bounds.x + bounds.width);
|
|
511
|
+
maxY = Math.max(maxY, bounds.y + bounds.height);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
x: minX + this.x,
|
|
516
|
+
y: minY + this.y,
|
|
517
|
+
width: maxX - minX,
|
|
518
|
+
height: maxY - minY
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Creates a string representation of the Sprite
|
|
524
|
+
* @returns {string}
|
|
525
|
+
*/
|
|
526
|
+
toString() {
|
|
527
|
+
return `[Sprite frames=${this.totalFrames} current=${this.currentFrame} playing=${this.isPlaying}]`;
|
|
528
|
+
}
|
|
529
|
+
}
|
package/src/game/pipeline.js
CHANGED
|
@@ -68,22 +68,25 @@ export class Pipeline extends Loggable {
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Recursively checks all children of a Scene for hover state.
|
|
71
|
+
* Also checks if the Scene itself is interactive.
|
|
71
72
|
* @param {Scene} scene - The scene whose children will be hover-tested.
|
|
72
73
|
* @param {object} e - Event data containing pointer coordinates (e.x, e.y).
|
|
73
74
|
* @private
|
|
74
75
|
*/
|
|
75
76
|
_hoverScene(scene, e) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
// First check children
|
|
78
|
+
if (scene.children && scene.children.length > 0) {
|
|
79
|
+
for (let i = scene.children.length - 1; i >= 0; i--) {
|
|
80
|
+
const child = scene.children[i];
|
|
81
|
+
if (child instanceof Scene) {
|
|
82
|
+
this._hoverScene(child, e); // recurse into nested scenes
|
|
83
|
+
} else {
|
|
84
|
+
this._hoverObject(child, e); // hover actual objects
|
|
85
|
+
}
|
|
85
86
|
}
|
|
86
87
|
}
|
|
88
|
+
// Also check the Scene itself for hover state
|
|
89
|
+
this._hoverObject(scene, e);
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
/**
|
|
@@ -98,9 +101,6 @@ export class Pipeline extends Loggable {
|
|
|
98
101
|
// Check from topmost to bottommost object to find the first that was hit.
|
|
99
102
|
for (let i = this.gameObjects.length - 1; i >= 0; i--) {
|
|
100
103
|
const obj = this.gameObjects[i];
|
|
101
|
-
if (type === "inputdown") {
|
|
102
|
-
//this.logger.log("inputdown", obj);
|
|
103
|
-
}
|
|
104
104
|
if (obj instanceof Scene) {
|
|
105
105
|
// If it's a Scene, see if any of its children were hit.
|
|
106
106
|
if (this._dispatchToScene(obj, type, e)) {
|
|
@@ -141,30 +141,34 @@ export class Pipeline extends Loggable {
|
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
143
|
* Recursively dispatch an event to a Scene and possibly its nested child Scenes.
|
|
144
|
+
* Also checks if the Scene itself is interactive and was hit.
|
|
144
145
|
* @param {Scene} scene - The scene to dispatch the event to.
|
|
145
146
|
* @param {string} type - The type of pointer event ("inputdown", "inputup", etc).
|
|
146
147
|
* @param {object} e - Event data with pointer coordinates.
|
|
147
|
-
* @returns {boolean} True if the event was handled by a child, false otherwise.
|
|
148
|
+
* @returns {boolean} True if the event was handled by a child or the scene itself, false otherwise.
|
|
148
149
|
* @private
|
|
149
150
|
*/
|
|
150
151
|
_dispatchToScene(scene, type, e) {
|
|
151
|
-
//
|
|
152
|
+
// First check children (they render on top, so should get priority)
|
|
152
153
|
for (let i = scene.children.length - 1; i >= 0; i--) {
|
|
153
154
|
const child = scene.children[i];
|
|
154
155
|
if (child instanceof Scene) {
|
|
155
156
|
// Recurse deeper if child is also a Scene
|
|
156
157
|
const hit = this._dispatchToScene(child, type, e);
|
|
157
158
|
if (hit) {
|
|
158
|
-
//if(type === "inputdown") this.logger.log("HIT", child, type);
|
|
159
159
|
return true;
|
|
160
160
|
}
|
|
161
161
|
} else if (child.interactive && child._hitTest?.(e.x, e.y)) {
|
|
162
162
|
// Found a child that was hit
|
|
163
|
-
//if(type === "inputdown") this.logger.log("Dispatching to child", child, type);
|
|
164
163
|
child.events.emit(type, e);
|
|
165
164
|
return true;
|
|
166
165
|
}
|
|
167
166
|
}
|
|
167
|
+
// If no children handled the event, check if the Scene itself is interactive and hit
|
|
168
|
+
if (scene.interactive && scene._hitTest?.(e.x, e.y)) {
|
|
169
|
+
scene.events.emit(type, e);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
168
172
|
return false;
|
|
169
173
|
}
|
|
170
174
|
|