canvasengine 2.0.0-beta.4 → 2.0.0-beta.41

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.
Files changed (142) hide show
  1. package/dist/DebugRenderer-BxfW34YG.js +172 -0
  2. package/dist/DebugRenderer-BxfW34YG.js.map +1 -0
  3. package/dist/components/Button.d.ts +183 -0
  4. package/dist/components/Button.d.ts.map +1 -0
  5. package/dist/components/Canvas.d.ts +18 -0
  6. package/dist/components/Canvas.d.ts.map +1 -0
  7. package/dist/components/DOMElement.d.ts +44 -0
  8. package/dist/components/DOMElement.d.ts.map +1 -0
  9. package/dist/components/Graphic.d.ts +65 -0
  10. package/dist/components/Graphic.d.ts.map +1 -0
  11. package/dist/components/Joystick.d.ts +36 -0
  12. package/dist/components/Joystick.d.ts.map +1 -0
  13. package/dist/components/NineSliceSprite.d.ts +17 -0
  14. package/dist/components/NineSliceSprite.d.ts.map +1 -0
  15. package/dist/components/ParticleEmitter.d.ts +5 -0
  16. package/dist/components/ParticleEmitter.d.ts.map +1 -0
  17. package/dist/components/Scene.d.ts +2 -0
  18. package/dist/components/Scene.d.ts.map +1 -0
  19. package/dist/components/Text.d.ts +26 -0
  20. package/dist/components/Text.d.ts.map +1 -0
  21. package/dist/components/TilingSprite.d.ts +18 -0
  22. package/dist/components/TilingSprite.d.ts.map +1 -0
  23. package/dist/components/Video.d.ts +15 -0
  24. package/dist/components/Video.d.ts.map +1 -0
  25. package/dist/components/index.d.ts +18 -0
  26. package/dist/components/index.d.ts.map +1 -0
  27. package/dist/components/types/DisplayObject.d.ts +110 -0
  28. package/dist/components/types/DisplayObject.d.ts.map +1 -0
  29. package/dist/components/types/MouseEvent.d.ts +4 -0
  30. package/dist/components/types/MouseEvent.d.ts.map +1 -0
  31. package/dist/components/types/Spritesheet.d.ts +248 -0
  32. package/dist/components/types/Spritesheet.d.ts.map +1 -0
  33. package/dist/components/types/index.d.ts +5 -0
  34. package/dist/components/types/index.d.ts.map +1 -0
  35. package/dist/directives/Controls.d.ts +113 -0
  36. package/dist/directives/Controls.d.ts.map +1 -0
  37. package/dist/directives/ControlsBase.d.ts +198 -0
  38. package/dist/directives/ControlsBase.d.ts.map +1 -0
  39. package/dist/directives/Drag.d.ts +70 -0
  40. package/dist/directives/Drag.d.ts.map +1 -0
  41. package/dist/directives/Flash.d.ts +117 -0
  42. package/dist/directives/Flash.d.ts.map +1 -0
  43. package/dist/directives/GamepadControls.d.ts +225 -0
  44. package/dist/directives/GamepadControls.d.ts.map +1 -0
  45. package/dist/directives/JoystickControls.d.ts +172 -0
  46. package/dist/directives/JoystickControls.d.ts.map +1 -0
  47. package/dist/directives/KeyboardControls.d.ts +219 -0
  48. package/dist/directives/KeyboardControls.d.ts.map +1 -0
  49. package/dist/directives/Scheduler.d.ts +36 -0
  50. package/dist/directives/Scheduler.d.ts.map +1 -0
  51. package/dist/directives/Shake.d.ts +98 -0
  52. package/dist/directives/Shake.d.ts.map +1 -0
  53. package/dist/directives/Sound.d.ts +26 -0
  54. package/dist/directives/Sound.d.ts.map +1 -0
  55. package/dist/directives/Transition.d.ts +11 -0
  56. package/dist/directives/Transition.d.ts.map +1 -0
  57. package/dist/directives/ViewportCull.d.ts +12 -0
  58. package/dist/directives/ViewportCull.d.ts.map +1 -0
  59. package/dist/directives/ViewportFollow.d.ts +19 -0
  60. package/dist/directives/ViewportFollow.d.ts.map +1 -0
  61. package/dist/directives/index.d.ts +13 -0
  62. package/dist/directives/index.d.ts.map +1 -0
  63. package/dist/engine/animation.d.ts +73 -0
  64. package/dist/engine/animation.d.ts.map +1 -0
  65. package/dist/engine/bootstrap.d.ts +16 -0
  66. package/dist/engine/bootstrap.d.ts.map +1 -0
  67. package/dist/engine/directive.d.ts +14 -0
  68. package/dist/engine/directive.d.ts.map +1 -0
  69. package/dist/engine/reactive.d.ts +105 -0
  70. package/dist/engine/reactive.d.ts.map +1 -0
  71. package/dist/engine/signal.d.ts +72 -0
  72. package/dist/engine/signal.d.ts.map +1 -0
  73. package/dist/engine/trigger.d.ts +54 -0
  74. package/dist/engine/trigger.d.ts.map +1 -0
  75. package/dist/engine/utils.d.ts +90 -0
  76. package/dist/engine/utils.d.ts.map +1 -0
  77. package/dist/hooks/addContext.d.ts +2 -0
  78. package/dist/hooks/addContext.d.ts.map +1 -0
  79. package/dist/hooks/useProps.d.ts +42 -0
  80. package/dist/hooks/useProps.d.ts.map +1 -0
  81. package/dist/hooks/useRef.d.ts +5 -0
  82. package/dist/hooks/useRef.d.ts.map +1 -0
  83. package/dist/index-BnuKipxl.js +12568 -0
  84. package/dist/index-BnuKipxl.js.map +1 -0
  85. package/dist/index.d.ts +15 -1083
  86. package/dist/index.d.ts.map +1 -0
  87. package/dist/index.global.js +29 -0
  88. package/dist/index.global.js.map +1 -0
  89. package/dist/index.js +81 -3041
  90. package/dist/index.js.map +1 -1
  91. package/dist/utils/Ease.d.ts +17 -0
  92. package/dist/utils/Ease.d.ts.map +1 -0
  93. package/dist/utils/GlobalAssetLoader.d.ts +141 -0
  94. package/dist/utils/GlobalAssetLoader.d.ts.map +1 -0
  95. package/dist/utils/RadialGradient.d.ts +58 -0
  96. package/dist/utils/RadialGradient.d.ts.map +1 -0
  97. package/dist/utils/functions.d.ts +2 -0
  98. package/dist/utils/functions.d.ts.map +1 -0
  99. package/package.json +13 -7
  100. package/src/components/Button.ts +396 -0
  101. package/src/components/Canvas.ts +61 -45
  102. package/src/components/Container.ts +21 -2
  103. package/src/components/DOMContainer.ts +123 -0
  104. package/src/components/DOMElement.ts +421 -0
  105. package/src/components/DisplayObject.ts +350 -197
  106. package/src/components/Graphic.ts +200 -34
  107. package/src/components/Joystick.ts +363 -0
  108. package/src/components/Mesh.ts +222 -0
  109. package/src/components/NineSliceSprite.ts +4 -1
  110. package/src/components/ParticleEmitter.ts +12 -8
  111. package/src/components/Sprite.ts +306 -30
  112. package/src/components/Text.ts +125 -18
  113. package/src/components/Video.ts +110 -0
  114. package/src/components/Viewport.ts +59 -43
  115. package/src/components/index.ts +8 -2
  116. package/src/components/types/DisplayObject.ts +34 -0
  117. package/src/components/types/Spritesheet.ts +0 -118
  118. package/src/directives/Controls.ts +254 -0
  119. package/src/directives/ControlsBase.ts +266 -0
  120. package/src/directives/Drag.ts +357 -52
  121. package/src/directives/Flash.ts +419 -0
  122. package/src/directives/GamepadControls.ts +537 -0
  123. package/src/directives/JoystickControls.ts +396 -0
  124. package/src/directives/KeyboardControls.ts +66 -424
  125. package/src/directives/Shake.ts +295 -0
  126. package/src/directives/Sound.ts +94 -31
  127. package/src/directives/ViewportFollow.ts +35 -7
  128. package/src/directives/index.ts +12 -6
  129. package/src/engine/animation.ts +175 -21
  130. package/src/engine/bootstrap.ts +23 -3
  131. package/src/engine/directive.ts +2 -2
  132. package/src/engine/reactive.ts +780 -177
  133. package/src/engine/signal.ts +35 -4
  134. package/src/engine/trigger.ts +34 -7
  135. package/src/engine/utils.ts +19 -3
  136. package/src/hooks/useProps.ts +1 -1
  137. package/src/index.ts +4 -2
  138. package/src/utils/GlobalAssetLoader.ts +257 -0
  139. package/src/utils/functions.ts +7 -0
  140. package/testing/index.ts +12 -0
  141. package/tsconfig.json +17 -0
  142. package/vite.config.ts +39 -0
@@ -1,5 +1,7 @@
1
- import { computed, effect, isSignal, Signal, WritableSignal } from "@signe/reactive";
1
+ import { Howl } from 'howler';
2
+ import { computed, effect, isSignal, Signal } from "@signe/reactive";
2
3
  import {
4
+ Application,
3
5
  Assets,
4
6
  Container,
5
7
  Sprite as PixiSprite,
@@ -10,7 +12,9 @@ import { Subscription } from "rxjs";
10
12
  import {
11
13
  Element,
12
14
  createComponent,
15
+ isElement,
13
16
  registerComponent,
17
+ isElementFrozen,
14
18
  } from "../engine/reactive";
15
19
  import { arrayEquals, isFunction } from "../engine/utils";
16
20
  import { DisplayObject } from "./DisplayObject";
@@ -24,6 +28,8 @@ import {
24
28
  import { ComponentFunction } from "../engine/signal";
25
29
  import { DisplayObjectProps } from "./types/DisplayObject";
26
30
  import { AnimatedSignal, isAnimatedSignal } from "../engine/animation";
31
+ import { Layout } from '@pixi/layout';
32
+ import { GlobalAssetLoader } from "../utils/GlobalAssetLoader";
27
33
 
28
34
  const log = console.log;
29
35
 
@@ -73,15 +79,127 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
73
79
  private subscriptionSheet: Subscription[] = [];
74
80
  private sheetParams: any = {};
75
81
  private sheetCurrentAnimation: string = StandardAnimation.Stand;
82
+ private app: Application | null = null;
76
83
  onFinish: () => void;
84
+ private globalLoader: GlobalAssetLoader | null = null;
85
+ private trackedAssetIds: Set<string> = new Set();
86
+
87
+ get renderer() {
88
+ return this.app?.renderer;
89
+ }
77
90
 
78
91
  private currentAnimationContainer: Container | null = null;
79
92
 
93
+ /**
94
+ * Auto-detects image dimensions by loading the image and reading its natural size
95
+ * This is used when width/height are not explicitly provided in the spritesheet definition
96
+ *
97
+ * @param imagePath - Path to the image file
98
+ * @returns Object containing the detected width and height of the image
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const { width, height } = await sprite.detectImageDimensions('path/to/image.png');
103
+ * // width: 256, height: 128
104
+ * ```
105
+ */
106
+ private async detectImageDimensions(imagePath: string): Promise<{ width: number; height: number }> {
107
+ if (!imagePath || typeof imagePath !== 'string' || imagePath.trim() === '') {
108
+ throw new Error(`Invalid image path provided to detectImageDimensions: ${imagePath}`);
109
+ }
110
+
111
+ // Register asset in global loader if available
112
+ let assetId: string | null = null;
113
+ if (this.globalLoader) {
114
+ assetId = this.globalLoader.registerAsset(imagePath);
115
+ this.trackedAssetIds.add(assetId);
116
+ }
117
+
118
+ const texture = await Assets.load(imagePath, (progress) => {
119
+ if (this.globalLoader && assetId) {
120
+ this.globalLoader.updateProgress(assetId, progress);
121
+ }
122
+ });
123
+
124
+ // Mark as complete
125
+ if (this.globalLoader && assetId) {
126
+ this.globalLoader.completeAsset(assetId);
127
+ }
128
+
129
+ return {
130
+ width: texture.width,
131
+ height: texture.height,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Creates textures from a spritesheet image by cutting it into frames
137
+ * Automatically detects image dimensions if width/height are not provided
138
+ *
139
+ * @param options - Texture options containing image path, dimensions, and frame configuration
140
+ * @returns A 2D array of textures organized by rows and columns
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * // With explicit dimensions
145
+ * const textures = await sprite.createTextures({
146
+ * image: 'path/to/image.png',
147
+ * width: 256,
148
+ * height: 128,
149
+ * framesWidth: 4,
150
+ * framesHeight: 2,
151
+ * spriteWidth: 64,
152
+ * spriteHeight: 64
153
+ * });
154
+ *
155
+ * // Without dimensions (automatically detected)
156
+ * const textures = await sprite.createTextures({
157
+ * image: 'path/to/image.png',
158
+ * framesWidth: 4,
159
+ * framesHeight: 2,
160
+ * spriteWidth: 64,
161
+ * spriteHeight: 64
162
+ * });
163
+ * ```
164
+ */
80
165
  private async createTextures(
81
166
  options: Required<TextureOptionsMerging>
82
167
  ): Promise<Texture[][]> {
83
- const { width, height, framesHeight, framesWidth, image, offset } = options;
84
- const texture = await Assets.load(image);
168
+ let { width, height, framesHeight, framesWidth, image, offset } = options;
169
+
170
+ if (!image || typeof image !== 'string' || image.trim() === '') {
171
+ console.warn('Invalid image path provided to createTextures:', image);
172
+ return [];
173
+ }
174
+
175
+ // Register asset in global loader if available
176
+ let assetId: string | null = null;
177
+ if (this.globalLoader) {
178
+ assetId = this.globalLoader.registerAsset(image);
179
+ this.trackedAssetIds.add(assetId);
180
+ }
181
+
182
+ const texture = await Assets.load(image, (progress) => {
183
+ if (this.globalLoader && assetId) {
184
+ this.globalLoader.updateProgress(assetId, progress);
185
+ }
186
+ });
187
+
188
+ // Mark as complete
189
+ if (this.globalLoader && assetId) {
190
+ this.globalLoader.completeAsset(assetId);
191
+ }
192
+
193
+ // Auto-detect width and height from the image if not provided
194
+ if (!width || width <= 0) {
195
+ width = texture.width;
196
+ options.width = width;
197
+ }
198
+ if (!height || height <= 0) {
199
+ height = texture.height;
200
+ options.height = height;
201
+ }
202
+
85
203
  const spriteWidth = options.spriteWidth;
86
204
  const spriteHeight = options.spriteHeight;
87
205
  const frames: Texture[][] = [];
@@ -140,12 +258,30 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
140
258
  } as any;
141
259
  const {
142
260
  rectWidth,
143
- width = 0,
261
+ width: widthOption = 0,
144
262
  framesWidth = 1,
145
263
  rectHeight,
146
- height = 0,
264
+ height: heightOption = 0,
147
265
  framesHeight = 1,
266
+ image,
148
267
  } = optionsTextures;
268
+
269
+ // Auto-detect width and height from the image if not provided
270
+ let width = widthOption;
271
+ let height = heightOption;
272
+
273
+ if (image && ((!width || width <= 0) || (!height || height <= 0))) {
274
+ const dimensions = await this.detectImageDimensions(image);
275
+ if (!width || width <= 0) {
276
+ width = dimensions.width;
277
+ optionsTextures.width = width;
278
+ }
279
+ if (!height || height <= 0) {
280
+ height = dimensions.height;
281
+ optionsTextures.height = height;
282
+ }
283
+ }
284
+
149
285
  optionsTextures.spriteWidth = rectWidth ? rectWidth : width / framesWidth;
150
286
  optionsTextures.spriteHeight = rectHeight
151
287
  ? rectHeight
@@ -164,17 +300,26 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
164
300
  }
165
301
 
166
302
  async onMount(params: Element<CanvasSprite>) {
303
+ // Set #element manually for freeze checking before calling super.onMount
304
+ // We need to set it early so update() can check freeze state
305
+ (this as any)['#element'] = params;
306
+
167
307
  const { props, propObservables } = params;
168
308
  const tick: Signal = props.context.tick;
169
309
  const sheet = props.sheet ?? {};
310
+ const definition = props.sheet?.definition ?? {};
311
+ this.app = props.context.app();
312
+ // Get global loader from context if available
313
+ this.globalLoader = props.context?.globalLoader || null;
170
314
  if (sheet?.onFinish) {
171
315
  this.onFinish = sheet.onFinish;
172
316
  }
173
317
  this.subscriptionTick = tick.observable.subscribe((value) => {
318
+ if (this.destroyed) return
174
319
  this.update(value);
175
320
  });
176
- if (props.sheet?.definition) {
177
- this.spritesheet = props.sheet.definition;
321
+ if (definition) {
322
+ this.spritesheet = definition.value ?? definition;
178
323
  await this.createAnimations();
179
324
  }
180
325
  if (sheet.params) {
@@ -218,49 +363,123 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
218
363
  this.sheetCurrentAnimation = StandardAnimation.Stand;
219
364
  }
220
365
 
221
- this.play(this.sheetCurrentAnimation, [this.sheetParams]);
366
+ if (this.spritesheet && this.has(this.sheetCurrentAnimation)) this.play(this.sheetCurrentAnimation, [this.sheetParams]);
222
367
  });
223
-
224
368
  super.onMount(params);
225
369
  }
226
370
 
227
371
  async onUpdate(props) {
372
+ if (this.destroyed) return
228
373
  super.onUpdate(props);
229
374
 
230
- const sheet = props.sheet;
375
+ // Initialize globalLoader from context if not already set
376
+ if (!this.globalLoader && props.context?.globalLoader) {
377
+ this.globalLoader = props.context.globalLoader;
378
+ }
379
+
380
+ const setTexture = async (image: string) => {
381
+ if (!image || typeof image !== 'string' || image.trim() === '') {
382
+ console.warn('Invalid image path provided to setTexture:', image);
383
+ return null;
384
+ }
385
+
386
+ // Register asset in global loader if available
387
+ let assetId: string | null = null;
388
+ if (this.globalLoader) {
389
+ assetId = this.globalLoader.registerAsset(image);
390
+ this.trackedAssetIds.add(assetId);
391
+ }
392
+
393
+ const onProgress = this.fullProps.loader?.onProgress;
394
+ const texture = await Assets.load(image, (progress) => {
395
+ // Update global loader progress
396
+ if (this.globalLoader && assetId) {
397
+ this.globalLoader.updateProgress(assetId, progress);
398
+ }
399
+ // Call local loader callback if provided
400
+ if (onProgress) onProgress(progress);
401
+ if (progress == 1) {
402
+ // Mark as complete in global loader
403
+ if (this.globalLoader && assetId) {
404
+ this.globalLoader.completeAsset(assetId);
405
+ }
406
+ const onComplete = this.fullProps.loader?.onComplete;
407
+ if (onComplete) {
408
+ // hack to memoize the texture
409
+ setTimeout(() => {
410
+ onComplete(texture);
411
+ });
412
+ }
413
+ }
414
+ });
415
+
416
+ return texture
417
+ }
418
+
419
+ const sheet = props.sheet
420
+ const definition = props.sheet?.definition ?? {};
421
+
422
+ if (definition?.type === 'reset') {
423
+ this.spritesheet = definition.value ?? definition;
424
+ await this.resetAnimations();
425
+ }
426
+
231
427
  if (sheet?.params) this.sheetParams = sheet?.params;
232
428
 
233
- if (sheet?.playing && this.isMounted) {
429
+ if (sheet?.playing && this.isMounted && this.spritesheet && this.animations.size > 0) {
234
430
  this.sheetCurrentAnimation = sheet?.playing;
235
431
  this.play(this.sheetCurrentAnimation, [this.sheetParams]);
236
432
  }
237
433
 
238
- if (props.hitbox) this.hitbox = props.hitbox;
434
+ if (props.hitbox) this.hitbox = props.hitbox.value ?? props.hitbox;
239
435
 
240
436
  if (props.scaleMode) this.baseTexture.scaleMode = props.scaleMode;
241
437
  else if (props.image && this.fullProps.rectangle === undefined) {
242
- this.texture = await Assets.load(this.fullProps.image);
438
+ const texture = await setTexture(this.fullProps.image);
439
+ if (texture) {
440
+ this.texture = texture;
441
+ }
243
442
  } else if (props.texture) {
244
- this.texture = props.texture;
443
+ if (isElement(props.texture)) {
444
+ const textureInstance = props.texture.componentInstance;
445
+ textureInstance.subjectInit
446
+ .subscribe()
447
+ this.texture = this.renderer?.generateTexture(props.texture.componentInstance);
448
+ } else {
449
+ this.texture = props.texture;
450
+ }
245
451
  }
246
-
247
452
  if (props.rectangle !== undefined) {
248
453
  const { x, y, width, height } = props.rectangle?.value ?? props.rectangle;
249
- const texture = await Assets.load(this.fullProps.image);
250
- this.texture = new Texture({
251
- source: texture.source,
252
- frame: new Rectangle(x, y, width, height),
253
- });
454
+ const texture = await setTexture(this.fullProps.image);
455
+ if (texture) {
456
+ this.texture = new Texture({
457
+ source: texture.source,
458
+ frame: new Rectangle(x, y, width, height),
459
+ });
460
+ }
254
461
  }
255
462
  }
256
463
 
257
- onDestroy(): void {
258
- super.onDestroy();
259
- this.subscriptionSheet.forEach((sub) => sub.unsubscribe());
260
- this.subscriptionTick.unsubscribe();
261
- if (this.currentAnimationContainer && this.parent instanceof Container) {
262
- this.parent.removeChild(this.currentAnimationContainer);
263
- }
464
+ async onDestroy(parent: Element, afterDestroy: () => void): Promise<void> {
465
+ const _afterDestroy = async () => {
466
+ // Clean up tracked assets from global loader
467
+ if (this.globalLoader) {
468
+ this.trackedAssetIds.forEach((assetId) => {
469
+ this.globalLoader!.removeAsset(assetId);
470
+ });
471
+ this.trackedAssetIds.clear();
472
+ }
473
+ this.subscriptionSheet.forEach((sub) => sub.unsubscribe());
474
+ this.subscriptionTick.unsubscribe();
475
+ if (this.currentAnimationContainer && this.parent instanceof Container) {
476
+ this.parent.removeChild(this.currentAnimationContainer);
477
+ }
478
+ if (afterDestroy) {
479
+ afterDestroy();
480
+ }
481
+ };
482
+ await super.onDestroy(parent, _afterDestroy);
264
483
  }
265
484
 
266
485
  has(name: string): boolean {
@@ -279,7 +498,6 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
279
498
 
280
499
  stop() {
281
500
  this.currentAnimation = null;
282
- this.destroy();
283
501
  }
284
502
 
285
503
  play(name: string, params: any[] = []) {
@@ -321,7 +539,12 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
321
539
  const sound = this.currentAnimation.data.sound;
322
540
 
323
541
  if (sound) {
324
- //RpgSound.get(sound).play()
542
+ new Howl({
543
+ src: sound,
544
+ autoplay: true,
545
+ loop: false,
546
+ volume: 1,
547
+ })
325
548
  }
326
549
 
327
550
  // Updates immediately to avoid flickering
@@ -330,7 +553,51 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
330
553
  });
331
554
  }
332
555
 
556
+ /**
557
+ * Resets the sprite by destroying and recreating all animations
558
+ * This method clears the current animation state, destroys existing textures,
559
+ * and recreates all animations from the spritesheet
560
+ *
561
+ * @example
562
+ * ```typescript
563
+ * // Reset all animations to their initial state
564
+ * sprite.resetAnimations();
565
+ *
566
+ * // Reset and then play a specific animation
567
+ * await sprite.resetAnimations();
568
+ * sprite.play('walk');
569
+ * ```
570
+ */
571
+ async resetAnimations(): Promise<void> {
572
+ // Stop current animation
573
+ this.stop();
574
+
575
+ // Clear all animations and textures
576
+ this.animations.clear();
577
+
578
+ // Reset animation state
579
+ this.currentAnimation = null;
580
+ this.currentAnimationContainer = null;
581
+ this.time = 0;
582
+ this.frameIndex = 0;
583
+
584
+ // Clear children
585
+ this.removeChildren();
586
+
587
+ // Recreate animations from spritesheet
588
+ if (this.spritesheet) {
589
+ await this.createAnimations();
590
+ this.play(this.sheetCurrentAnimation, [this.sheetParams]);
591
+ }
592
+ }
593
+
333
594
  update({ deltaRatio }) {
595
+ // Block animation update if element is frozen
596
+ const element = this.getElement();
597
+ if (element && isElementFrozen(element)) {
598
+ return;
599
+ }
600
+
334
601
  if (
335
602
  !this.isPlaying() ||
336
603
  !this.currentAnimation ||
@@ -396,6 +663,7 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
396
663
  const widthOfSprite =
397
664
  typeof realSize == "number" ? realSize : realSize?.width;
398
665
 
666
+
399
667
  const applyAnchorBySize = () => {
400
668
  if (heightOfSprite && this.hitbox) {
401
669
  const { spriteWidth, spriteHeight } = data;
@@ -440,7 +708,9 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
440
708
  }
441
709
  }
442
710
 
443
- export interface CanvasSprite extends PixiSprite {}
711
+ export interface CanvasSprite extends PixiSprite {
712
+ layout: Layout | null;
713
+ }
444
714
 
445
715
  registerComponent("Sprite", CanvasSprite);
446
716
 
@@ -483,11 +753,17 @@ export interface SpritePropsWithSheet
483
753
  params?: any;
484
754
  onFinish?: () => void;
485
755
  };
756
+ loader?: {
757
+ onProgress?: (progress: number) => void;
758
+ onComplete?: (texture: Texture) => void;
759
+ };
486
760
  }
487
761
 
488
762
  export type SpritePropTypes = SpritePropsWithImage | SpritePropsWithSheet;
489
763
 
490
764
  // Update the Sprite function to use the props interface
491
765
  export const Sprite: ComponentFunction<SpritePropTypes> = (props) => {
766
+ // Ensure component is registered in test environments where module cache may differ
767
+ registerComponent("Sprite", CanvasSprite);
492
768
  return createComponent("Sprite", props);
493
769
  };
@@ -1,15 +1,16 @@
1
1
  import { Text as PixiText, TextStyle } from "pixi.js";
2
- import { createComponent, registerComponent } from "../engine/reactive";
3
- import { DisplayObject } from "./DisplayObject";
2
+ import { createComponent, registerComponent, Element, Props } from "../engine/reactive";
3
+ import { DisplayObject, ComponentInstance } from "./DisplayObject";
4
4
  import { DisplayObjectProps } from "./types/DisplayObject";
5
5
  import { Signal } from "@signe/reactive";
6
- import { on } from "../engine/trigger";
6
+ import { on, isTrigger } from "../engine/trigger";
7
+ import { Howl } from "howler";
7
8
 
8
9
  enum TextEffect {
9
10
  Typewriter = "typewriter",
10
11
  }
11
12
 
12
- interface TextProps extends DisplayObjectProps {
13
+ export interface TextProps extends DisplayObjectProps {
13
14
  text?: string;
14
15
  style?: Partial<TextStyle>;
15
16
  color?: string;
@@ -20,7 +21,13 @@ interface TextProps extends DisplayObjectProps {
20
21
  start?: () => void;
21
22
  onComplete?: () => void;
22
23
  skip?: () => void;
24
+ sound?: {
25
+ src: string;
26
+ volume?: number;
27
+ rate?: number;
28
+ };
23
29
  };
30
+ context?: any; // Ensure context is available, ideally typed from a base prop or injected
24
31
  }
25
32
 
26
33
  class CanvasText extends DisplayObject(PixiText) {
@@ -31,10 +38,19 @@ class CanvasText extends DisplayObject(PixiText) {
31
38
  private _wordWrapWidth: number = 0;
32
39
  private typewriterOptions: any = {};
33
40
  private skipSignal?: () => void;
41
+ private typewriterSound?: Howl;
42
+ private lastSoundTime: number = 0;
43
+ private soundDuration: number = 0; // Duration of the sound in milliseconds
34
44
 
35
- onMount(args) {
36
- super.onMount(args);
37
- const { props } = args;
45
+ /**
46
+ * Called when the component is mounted to the scene graph.
47
+ * Initializes the typewriter effect if configured.
48
+ * @param {Element<CanvasText>} element - The element being mounted with parent and props.
49
+ * @param {number} [index] - The index of the component among its siblings.
50
+ */
51
+ async onMount(element: Element<CanvasText>, index?: number): Promise<void> {
52
+ const { props } = element;
53
+ await super.onMount(element, index);
38
54
  const tick: Signal = props.context.tick;
39
55
 
40
56
  if (props.text && props.typewriter) {
@@ -44,12 +60,18 @@ class CanvasText extends DisplayObject(PixiText) {
44
60
  // Set typewriter options
45
61
  if (props.typewriter) {
46
62
  this.typewriterOptions = props.typewriter;
47
- if (this.typewriterOptions.skip) {
63
+ if (this.typewriterOptions.skip && isTrigger(this.typewriterOptions.skip)) {
48
64
  on(this.typewriterOptions.skip, () => {
49
65
  this.skipTypewriter();
50
66
  });
51
67
  }
68
+ // Initialize typewriter sound if configured
69
+ if (this.typewriterOptions.sound) {
70
+ this.initializeTypewriterSound();
71
+ }
52
72
  }
73
+ // Update layout after initializing typewriter
74
+ this.updateLayout();
53
75
  }
54
76
  this.subscriptionTick = tick.observable.subscribe(() => {
55
77
  if (props.typewriter) {
@@ -63,15 +85,21 @@ class CanvasText extends DisplayObject(PixiText) {
63
85
  if (props.typewriter) {
64
86
  if (props.typewriter) {
65
87
  this.typewriterOptions = props.typewriter;
88
+ // Reinitialize sound if sound configuration changed
89
+ if (props.typewriter.sound) {
90
+ this.initializeTypewriterSound();
91
+ }
66
92
  }
67
93
  }
68
- if (props.text) {
69
- this.text = props.text;
94
+ if (props.text !== undefined) {
95
+ this.text = ''+props.text;
70
96
  }
71
97
  if (props.text !== undefined && props.text !== this.fullText && this.fullProps.typewriter) {
72
98
  this.text = "";
73
99
  this.currentIndex = 0;
74
100
  this.fullText = props.text;
101
+ // Update layout after resetting typewriter
102
+ this.updateLayout();
75
103
  }
76
104
  if (props.style) {
77
105
  for (const key in props.style) {
@@ -90,6 +118,59 @@ class CanvasText extends DisplayObject(PixiText) {
90
118
  if (props.fontFamily) {
91
119
  this.style.fontFamily = props.fontFamily;
92
120
  }
121
+
122
+ // Use the centralized layout update method
123
+ this.updateLayout();
124
+ }
125
+
126
+ get onCompleteCallback() {
127
+ return this.typewriterOptions.onComplete;
128
+ }
129
+
130
+ /**
131
+ * Initializes the typewriter sound effect using Howler.
132
+ * Creates a Howl instance with the configured sound settings.
133
+ * Calculates the sound duration to prevent overlapping sounds.
134
+ */
135
+ private initializeTypewriterSound() {
136
+ if (!this.typewriterOptions.sound?.src) return;
137
+
138
+ this.typewriterSound = new Howl({
139
+ src: [this.typewriterOptions.sound.src],
140
+ volume: this.typewriterOptions.sound.volume ?? 0.5,
141
+ rate: this.typewriterOptions.sound.rate ?? 1.0,
142
+ preload: true,
143
+ onload: () => {
144
+ // Calculate sound duration in milliseconds
145
+ if (this.typewriterSound) {
146
+ const duration = this.typewriterSound.duration();
147
+ const rate = this.typewriterOptions.sound?.rate ?? 1.0;
148
+ this.soundDuration = (duration / rate) * 1000;
149
+ }
150
+ }
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Plays the typewriter sound with duration-based cooldown to prevent overlapping sounds.
156
+ * @param {number} currentTime - The current timestamp to check against sound duration.
157
+ */
158
+ private playTypewriterSound(currentTime: number) {
159
+ if (!this.typewriterSound || !this.typewriterOptions.sound) return;
160
+
161
+ // Check if enough time has passed since the last sound play
162
+ // Use the actual sound duration to prevent overlap
163
+ if (this.soundDuration > 0 && currentTime - this.lastSoundTime < this.soundDuration) return;
164
+
165
+ this.typewriterSound.play();
166
+ this.lastSoundTime = currentTime;
167
+ }
168
+
169
+ /**
170
+ * Updates the layout properties of the text component.
171
+ * This method ensures consistent width, height and word wrap behavior.
172
+ */
173
+ private updateLayout() {
93
174
  if (this._wordWrapWidth) {
94
175
  this.setWidth(this._wordWrapWidth);
95
176
  } else {
@@ -98,10 +179,6 @@ class CanvasText extends DisplayObject(PixiText) {
98
179
  this.setHeight(this.height);
99
180
  }
100
181
 
101
- get onCompleteCallback() {
102
- return this.typewriterOptions.onComplete;
103
- }
104
-
105
182
  private typewriterEffect() {
106
183
  if (this.currentIndex < this.fullText.length) {
107
184
  const nextIndex = Math.min(
@@ -111,6 +188,14 @@ class CanvasText extends DisplayObject(PixiText) {
111
188
  this.text = this.fullText.slice(0, nextIndex);
112
189
  this.currentIndex = nextIndex;
113
190
 
191
+ // Play typewriter sound if configured
192
+ if (this.typewriterOptions.sound) {
193
+ this.playTypewriterSound(Date.now());
194
+ }
195
+
196
+ // Update layout after text change to maintain proper word wrap and dimensions
197
+ this.updateLayout();
198
+
114
199
  // Check if typewriter effect is complete
115
200
  if (
116
201
  this.currentIndex === this.fullText.length &&
@@ -128,15 +213,37 @@ class CanvasText extends DisplayObject(PixiText) {
128
213
  }
129
214
  this.text = this.fullText;
130
215
  this.currentIndex = this.fullText.length;
216
+
217
+ // Update layout after setting full text to maintain proper word wrap and dimensions
218
+ this.updateLayout();
131
219
  }
132
220
 
133
- onDestroy(): void {
134
- super.onDestroy();
135
- this.subscriptionTick.unsubscribe();
221
+ /**
222
+ * Called when the component is about to be destroyed.
223
+ * Unsubscribes from the tick observable and cleans up sound resources.
224
+ * @param {Element<any>} parent - The parent element.
225
+ * @param {() => void} [afterDestroy] - An optional callback function to be executed after the component's own destruction logic.
226
+ */
227
+ async onDestroy(parent: Element<any>, afterDestroy?: () => void): Promise<void> {
228
+ const _afterDestroy = async () => {
229
+ if (this.subscriptionTick) {
230
+ this.subscriptionTick.unsubscribe();
231
+ }
232
+ // Clean up typewriter sound
233
+ if (this.typewriterSound) {
234
+ this.typewriterSound.stop();
235
+ this.typewriterSound.unload();
236
+ this.typewriterSound = undefined;
237
+ }
238
+ if (afterDestroy) {
239
+ afterDestroy();
240
+ }
241
+ }
242
+ await super.onDestroy(parent, _afterDestroy);
136
243
  }
137
244
  }
138
245
 
139
- interface CanvasText extends PixiText {}
246
+ // interface CanvasText extends PixiText {} // Removed as it's redundant and causes type conflicts
140
247
 
141
248
  registerComponent("Text", CanvasText);
142
249