canvasengine 2.0.0-beta.5 → 2.0.0-beta.51

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 (172) hide show
  1. package/dist/components/Button.d.ts +185 -0
  2. package/dist/components/Button.d.ts.map +1 -0
  3. package/dist/components/Canvas.d.ts +17 -0
  4. package/dist/components/Canvas.d.ts.map +1 -0
  5. package/dist/components/Container.d.ts +86 -0
  6. package/dist/components/Container.d.ts.map +1 -0
  7. package/dist/components/DOMContainer.d.ts +98 -0
  8. package/dist/components/DOMContainer.d.ts.map +1 -0
  9. package/dist/components/DOMElement.d.ts +54 -0
  10. package/dist/components/DOMElement.d.ts.map +1 -0
  11. package/dist/components/DOMSprite.d.ts +127 -0
  12. package/dist/components/DOMSprite.d.ts.map +1 -0
  13. package/dist/components/DisplayObject.d.ts +94 -0
  14. package/dist/components/DisplayObject.d.ts.map +1 -0
  15. package/dist/components/FocusContainer.d.ts +129 -0
  16. package/dist/components/FocusContainer.d.ts.map +1 -0
  17. package/dist/components/Graphic.d.ts +64 -0
  18. package/dist/components/Graphic.d.ts.map +1 -0
  19. package/dist/components/Joystick.d.ts +36 -0
  20. package/dist/components/Joystick.d.ts.map +1 -0
  21. package/dist/components/Mesh.d.ts +208 -0
  22. package/dist/components/Mesh.d.ts.map +1 -0
  23. package/dist/components/NineSliceSprite.d.ts +16 -0
  24. package/dist/components/NineSliceSprite.d.ts.map +1 -0
  25. package/dist/components/ParticleEmitter.d.ts +4 -0
  26. package/dist/components/ParticleEmitter.d.ts.map +1 -0
  27. package/dist/components/Scene.d.ts +2 -0
  28. package/dist/components/Scene.d.ts.map +1 -0
  29. package/dist/components/Sprite.d.ts +242 -0
  30. package/dist/components/Sprite.d.ts.map +1 -0
  31. package/dist/components/Text.d.ts +25 -0
  32. package/dist/components/Text.d.ts.map +1 -0
  33. package/dist/components/TilingSprite.d.ts +17 -0
  34. package/dist/components/TilingSprite.d.ts.map +1 -0
  35. package/dist/components/Video.d.ts +14 -0
  36. package/dist/components/Video.d.ts.map +1 -0
  37. package/dist/components/Viewport.d.ts +121 -0
  38. package/dist/components/Viewport.d.ts.map +1 -0
  39. package/dist/components/index.d.ts +20 -0
  40. package/dist/components/index.d.ts.map +1 -0
  41. package/dist/components/types/DisplayObject.d.ts +106 -0
  42. package/dist/components/types/DisplayObject.d.ts.map +1 -0
  43. package/dist/components/types/MouseEvent.d.ts +4 -0
  44. package/dist/components/types/MouseEvent.d.ts.map +1 -0
  45. package/dist/components/types/Spritesheet.d.ts +248 -0
  46. package/dist/components/types/Spritesheet.d.ts.map +1 -0
  47. package/dist/components/types/index.d.ts +4 -0
  48. package/dist/components/types/index.d.ts.map +1 -0
  49. package/dist/directives/Controls.d.ts +112 -0
  50. package/dist/directives/Controls.d.ts.map +1 -0
  51. package/dist/directives/ControlsBase.d.ts +199 -0
  52. package/dist/directives/ControlsBase.d.ts.map +1 -0
  53. package/dist/directives/Drag.d.ts +69 -0
  54. package/dist/directives/Drag.d.ts.map +1 -0
  55. package/dist/directives/Flash.d.ts +116 -0
  56. package/dist/directives/Flash.d.ts.map +1 -0
  57. package/dist/directives/FocusNavigation.d.ts +52 -0
  58. package/dist/directives/FocusNavigation.d.ts.map +1 -0
  59. package/dist/directives/GamepadControls.d.ts +224 -0
  60. package/dist/directives/GamepadControls.d.ts.map +1 -0
  61. package/dist/directives/JoystickControls.d.ts +171 -0
  62. package/dist/directives/JoystickControls.d.ts.map +1 -0
  63. package/dist/directives/KeyboardControls.d.ts +219 -0
  64. package/dist/directives/KeyboardControls.d.ts.map +1 -0
  65. package/dist/directives/Scheduler.d.ts +35 -0
  66. package/dist/directives/Scheduler.d.ts.map +1 -0
  67. package/dist/directives/Shake.d.ts +98 -0
  68. package/dist/directives/Shake.d.ts.map +1 -0
  69. package/dist/directives/Sound.d.ts +25 -0
  70. package/dist/directives/Sound.d.ts.map +1 -0
  71. package/dist/directives/Transition.d.ts +10 -0
  72. package/dist/directives/Transition.d.ts.map +1 -0
  73. package/dist/directives/ViewportCull.d.ts +11 -0
  74. package/dist/directives/ViewportCull.d.ts.map +1 -0
  75. package/dist/directives/ViewportFollow.d.ts +18 -0
  76. package/dist/directives/ViewportFollow.d.ts.map +1 -0
  77. package/dist/directives/index.d.ts +13 -0
  78. package/dist/directives/index.d.ts.map +1 -0
  79. package/dist/engine/FocusManager.d.ts +174 -0
  80. package/dist/engine/FocusManager.d.ts.map +1 -0
  81. package/dist/engine/animation.d.ts +72 -0
  82. package/dist/engine/animation.d.ts.map +1 -0
  83. package/dist/engine/bootstrap.d.ts +48 -0
  84. package/dist/engine/bootstrap.d.ts.map +1 -0
  85. package/dist/engine/directive.d.ts +13 -0
  86. package/dist/engine/directive.d.ts.map +1 -0
  87. package/dist/engine/reactive.d.ts +134 -0
  88. package/dist/engine/reactive.d.ts.map +1 -0
  89. package/dist/engine/signal.d.ts +71 -0
  90. package/dist/engine/signal.d.ts.map +1 -0
  91. package/dist/engine/trigger.d.ts +54 -0
  92. package/dist/engine/trigger.d.ts.map +1 -0
  93. package/dist/engine/utils.d.ts +89 -0
  94. package/dist/engine/utils.d.ts.map +1 -0
  95. package/dist/hooks/addContext.d.ts +2 -0
  96. package/dist/hooks/addContext.d.ts.map +1 -0
  97. package/dist/hooks/useFocus.d.ts +60 -0
  98. package/dist/hooks/useFocus.d.ts.map +1 -0
  99. package/dist/hooks/useProps.d.ts +42 -0
  100. package/dist/hooks/useProps.d.ts.map +1 -0
  101. package/dist/hooks/useRef.d.ts +4 -0
  102. package/dist/hooks/useRef.d.ts.map +1 -0
  103. package/dist/index-DaGekQUW.js +2218 -0
  104. package/dist/index-DaGekQUW.js.map +1 -0
  105. package/dist/index.d.ts +19 -1099
  106. package/dist/index.d.ts.map +1 -0
  107. package/dist/index.global.js +5 -0
  108. package/dist/index.global.js.map +1 -0
  109. package/dist/index.js +11749 -2901
  110. package/dist/index.js.map +1 -1
  111. package/dist/utils/Ease.d.ts +17 -0
  112. package/dist/utils/Ease.d.ts.map +1 -0
  113. package/dist/utils/GlobalAssetLoader.d.ts +141 -0
  114. package/dist/utils/GlobalAssetLoader.d.ts.map +1 -0
  115. package/dist/utils/RadialGradient.d.ts +57 -0
  116. package/dist/utils/RadialGradient.d.ts.map +1 -0
  117. package/dist/utils/functions.d.ts +2 -0
  118. package/dist/utils/functions.d.ts.map +1 -0
  119. package/dist/utils/tabindex.d.ts +16 -0
  120. package/dist/utils/tabindex.d.ts.map +1 -0
  121. package/package.json +13 -7
  122. package/src/components/Button.ts +399 -0
  123. package/src/components/Canvas.ts +62 -46
  124. package/src/components/Container.ts +21 -2
  125. package/src/components/DOMContainer.ts +379 -0
  126. package/src/components/DOMElement.ts +556 -0
  127. package/src/components/DOMSprite.ts +1040 -0
  128. package/src/components/DisplayObject.ts +392 -201
  129. package/src/components/FocusContainer.ts +368 -0
  130. package/src/components/Graphic.ts +227 -66
  131. package/src/components/Joystick.ts +363 -0
  132. package/src/components/Mesh.ts +222 -0
  133. package/src/components/NineSliceSprite.ts +4 -1
  134. package/src/components/ParticleEmitter.ts +12 -8
  135. package/src/components/Sprite.ts +297 -31
  136. package/src/components/Text.ts +125 -18
  137. package/src/components/Video.ts +2 -2
  138. package/src/components/Viewport.ts +118 -63
  139. package/src/components/index.ts +9 -2
  140. package/src/components/types/DisplayObject.ts +41 -4
  141. package/src/components/types/Spritesheet.ts +0 -118
  142. package/src/directives/Controls.ts +254 -0
  143. package/src/directives/ControlsBase.ts +267 -0
  144. package/src/directives/Drag.ts +357 -52
  145. package/src/directives/Flash.ts +419 -0
  146. package/src/directives/FocusNavigation.ts +113 -0
  147. package/src/directives/GamepadControls.ts +537 -0
  148. package/src/directives/JoystickControls.ts +396 -0
  149. package/src/directives/KeyboardControls.ts +85 -430
  150. package/src/directives/Scheduler.ts +12 -4
  151. package/src/directives/Shake.ts +298 -0
  152. package/src/directives/Sound.ts +94 -31
  153. package/src/directives/ViewportFollow.ts +40 -9
  154. package/src/directives/index.ts +12 -6
  155. package/src/engine/FocusManager.ts +510 -0
  156. package/src/engine/animation.ts +175 -21
  157. package/src/engine/bootstrap.ts +93 -3
  158. package/src/engine/directive.ts +4 -4
  159. package/src/engine/reactive.ts +901 -161
  160. package/src/engine/signal.ts +113 -25
  161. package/src/engine/trigger.ts +34 -7
  162. package/src/engine/utils.ts +19 -3
  163. package/src/hooks/useFocus.ts +91 -0
  164. package/src/hooks/useProps.ts +1 -1
  165. package/src/index.ts +8 -2
  166. package/src/types/pixi-cull.d.ts +7 -0
  167. package/src/utils/GlobalAssetLoader.ts +257 -0
  168. package/src/utils/functions.ts +7 -0
  169. package/src/utils/tabindex.ts +70 -0
  170. package/testing/index.ts +35 -4
  171. package/tsconfig.json +18 -0
  172. package/vite.config.ts +39 -0
@@ -0,0 +1,298 @@
1
+ import { Container, Point } from 'pixi.js';
2
+ import { Directive, registerDirective } from '../engine/directive';
3
+ import { Element } from '../engine/reactive';
4
+ import { effect, isSignal } from '@signe/reactive';
5
+ import { on, isTrigger, Trigger } from '../engine/trigger';
6
+ import { useProps } from '../hooks/useProps';
7
+ import { SignalOrPrimitive } from '../components/types';
8
+ import { animatedSignal, AnimatedSignal } from '../engine/animation';
9
+ import { Subscription } from 'rxjs';
10
+
11
+ export type ShakeProps = {
12
+ /**
13
+ * Trigger that activates the shake animation
14
+ * When the trigger is activated, the shake animation will start
15
+ */
16
+ trigger?: Trigger<any>;
17
+ /**
18
+ * Intensity of the shake effect (in pixels)
19
+ * @default 10
20
+ */
21
+ intensity?: SignalOrPrimitive<number>;
22
+ /**
23
+ * Duration of the shake animation in milliseconds
24
+ * @default 500
25
+ */
26
+ duration?: SignalOrPrimitive<number>;
27
+ /**
28
+ * Number of shake oscillations during the animation
29
+ * Higher values create more rapid shaking
30
+ * @default 10
31
+ */
32
+ frequency?: SignalOrPrimitive<number>;
33
+ /**
34
+ * Direction of the shake: 'x', 'y', or 'both'
35
+ * @default 'both'
36
+ */
37
+ direction?: SignalOrPrimitive<'x' | 'y' | 'both'>;
38
+ /**
39
+ * Callback function called when shake starts
40
+ */
41
+ onStart?: () => void;
42
+ /**
43
+ * Callback function called when shake completes
44
+ */
45
+ onComplete?: () => void;
46
+ }
47
+
48
+ /**
49
+ * Shake directive that animates a display object's position when a trigger is activated.
50
+ * Creates a shake effect by rapidly oscillating the x and/or y position.
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * // Basic usage with trigger
55
+ * const shakeTrigger = trigger();
56
+ *
57
+ * onMount(element) {
58
+ * // Element will shake when trigger is activated
59
+ * element.props.shake = { trigger: shakeTrigger };
60
+ * }
61
+ *
62
+ * // Trigger the shake
63
+ * shakeTrigger.start();
64
+ * ```
65
+ */
66
+ export class Shake extends Directive {
67
+ private elementRef: Element<Container> | null = null;
68
+ private originalPosition: Point = new Point();
69
+ private progressSignal: AnimatedSignal<number> | null = null;
70
+ private shakeSubscription: any = null;
71
+ private positionEffect: Subscription | null = null;
72
+ private currentShakeConfig: {
73
+ intensity: number;
74
+ frequency: number;
75
+ direction: 'x' | 'y' | 'both';
76
+ randomSeed: number;
77
+ } | null = null;
78
+
79
+ /**
80
+ * Initializes the shake directive
81
+ * @param element - The element to attach the shake effect to
82
+ */
83
+ onInit(element: Element<Container>) {
84
+ this.elementRef = element;
85
+ }
86
+
87
+ /**
88
+ * Mounts the shake directive and sets up trigger listener
89
+ * @param element - The element being mounted
90
+ */
91
+ onMount(element: Element<Container>) {
92
+ const instance = element.componentInstance;
93
+ if (!instance) return;
94
+
95
+ const shakeProps = this.shakeProps;
96
+
97
+ // Check if trigger is provided
98
+ if (!shakeProps.trigger || !isTrigger(shakeProps.trigger)) {
99
+ return;
100
+ }
101
+
102
+ // Store original position
103
+ this.originalPosition.set(instance.position.x, instance.position.y);
104
+
105
+ // Clean up previous subscription if it exists
106
+ if (this.shakeSubscription) {
107
+ this.shakeSubscription.unsubscribe();
108
+ this.shakeSubscription = null;
109
+ }
110
+
111
+ // Listen to trigger activation
112
+ this.shakeSubscription = on(shakeProps.trigger, async (data) => {
113
+ await this.performShake(data);
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Gets the shake props with default values
119
+ * @returns ShakeProps with defaults applied
120
+ */
121
+ get shakeProps(): ShakeProps {
122
+ const shake = this.elementRef?.props.shake;
123
+ return useProps(shake?.value ?? shake, {
124
+ intensity: 10,
125
+ duration: 500,
126
+ frequency: 10,
127
+ direction: 'both',
128
+ });
129
+ }
130
+
131
+ private resolveSignalValue<T>(value: SignalOrPrimitive<T>): T {
132
+ return (isSignal(value as any) ? (value as any)() : value) as T;
133
+ }
134
+
135
+ /**
136
+ * Performs the shake animation using animatedSignal
137
+ * @param data - Optional data passed from the trigger that can override default options
138
+ */
139
+ private async performShake(data?: any): Promise<void> {
140
+ if (!this.elementRef?.componentInstance) return;
141
+
142
+ const instance = this.elementRef.componentInstance;
143
+ const shakeProps = this.shakeProps;
144
+
145
+ // Use data from trigger to override defaults if provided
146
+ const intensity = data?.intensity ?? this.resolveSignalValue(shakeProps.intensity);
147
+ const duration = data?.duration ?? this.resolveSignalValue(shakeProps.duration);
148
+ const frequency = data?.frequency ?? this.resolveSignalValue(shakeProps.frequency);
149
+ const direction = data?.direction ?? this.resolveSignalValue(shakeProps.direction);
150
+
151
+ // Stop any existing animation and clean up
152
+ if (this.positionEffect) {
153
+ this.positionEffect.unsubscribe();
154
+ this.positionEffect = null;
155
+ }
156
+
157
+ // Only update originalPosition if it hasn't been properly initialized (still at 0,0 and no parent was present when saved)
158
+ // OR if the current position is different from the stored original (element was moved)
159
+ const isOriginalUninitialized = this.originalPosition.x === 0 && this.originalPosition.y === 0 && instance.parent;
160
+ const hasPositionChanged = instance.position.x !== this.originalPosition.x || instance.position.y !== this.originalPosition.y;
161
+
162
+ if (isOriginalUninitialized || hasPositionChanged) {
163
+ this.originalPosition.set(instance.position.x, instance.position.y);
164
+ }
165
+
166
+ // Reset position to original before starting new shake
167
+ instance.position.x = this.originalPosition.x;
168
+ instance.position.y = this.originalPosition.y;
169
+
170
+ // Call onStart callback
171
+ shakeProps.onStart?.();
172
+
173
+ // Store current shake configuration for use in effect
174
+ this.currentShakeConfig = {
175
+ intensity,
176
+ frequency,
177
+ direction,
178
+ randomSeed: Math.random() * 1000, // Fixed random seed for this shake
179
+ };
180
+
181
+ // Create or recreate progress signal for shake animation
182
+ // We recreate it to ensure a fresh animation state
183
+ if (this.progressSignal) {
184
+ // Reset to 0 immediately without animation and wait for completion
185
+ await this.progressSignal.set(0, { duration: 0 });
186
+ } else {
187
+ this.progressSignal = animatedSignal(0, {
188
+ duration: duration,
189
+ ease: (t) => t, // Linear ease, we'll handle oscillation in effect
190
+ });
191
+ }
192
+
193
+ const shakeX = direction === 'y' ? false : true;
194
+ const shakeY = direction === 'x' ? false : true;
195
+
196
+ // Create effect to update position based on progress
197
+ this.positionEffect = effect(() => {
198
+ if (!instance || !this.progressSignal || !this.currentShakeConfig) return;
199
+
200
+ const progress = this.progressSignal();
201
+ const config = this.currentShakeConfig;
202
+
203
+ // Calculate decay factor (shake intensity decreases over time)
204
+ const decay = 1 - progress;
205
+
206
+ // Generate oscillation based on progress and frequency
207
+ // progress goes from 0 to 1, so we multiply by frequency to get oscillations
208
+ const time = progress * config.frequency;
209
+ const oscillation = Math.sin(time * Math.PI * 2);
210
+
211
+ // Apply shake with decay and consistent random variation
212
+ // Use the stored random seed for consistency during this shake
213
+ const randomValue = (Math.sin(config.randomSeed + progress * 10) * 0.25 + 1); // Between 0.75 and 1.25
214
+ const offsetX = shakeX ? oscillation * config.intensity * decay * randomValue : 0;
215
+ const offsetY = shakeY ? oscillation * config.intensity * decay * randomValue : 0;
216
+
217
+ // Update position
218
+ instance.position.x = this.originalPosition.x + offsetX;
219
+ instance.position.y = this.originalPosition.y + offsetY;
220
+ }).subscription;
221
+
222
+ // Start animation and wait for completion
223
+ // Note: animatedSignal.set() replaces onComplete with resolve, so we call onComplete after await
224
+ await this.progressSignal.set(1, {
225
+ duration: duration,
226
+ });
227
+
228
+ // Animation completed - clean up and call callbacks
229
+ // Reset to original position
230
+ if (instance) {
231
+ instance.position.x = this.originalPosition.x;
232
+ instance.position.y = this.originalPosition.y;
233
+ }
234
+
235
+ // Clean up position effect
236
+ if (this.positionEffect) {
237
+ this.positionEffect.unsubscribe();
238
+ this.positionEffect = null;
239
+ }
240
+
241
+ // Clear shake config
242
+ this.currentShakeConfig = null;
243
+
244
+ // Call onComplete callback
245
+ shakeProps.onComplete?.();
246
+ }
247
+
248
+ /**
249
+ * Updates the shake directive when props change
250
+ * @param props - Updated props
251
+ */
252
+ onUpdate(props: any) {
253
+ // Re-mount if props change significantly
254
+ if (props.type && props.type === 'reset') {
255
+ this.onDestroy();
256
+ if (this.elementRef) {
257
+ this.onMount(this.elementRef);
258
+ }
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Cleans up the shake directive
264
+ */
265
+ onDestroy() {
266
+ // Stop any running animation by resetting progress
267
+ if (this.progressSignal) {
268
+ this.progressSignal.set(0, { duration: 0 });
269
+ this.progressSignal = null;
270
+ }
271
+
272
+ // Clean up position effect
273
+ if (this.positionEffect) {
274
+ this.positionEffect.unsubscribe();
275
+ this.positionEffect = null;
276
+ }
277
+
278
+ // Clear shake config
279
+ this.currentShakeConfig = null;
280
+
281
+ // Reset position to original
282
+ if (this.elementRef?.componentInstance) {
283
+ const instance = this.elementRef.componentInstance;
284
+ instance.position.x = this.originalPosition.x;
285
+ instance.position.y = this.originalPosition.y;
286
+ }
287
+
288
+ // Clean up subscription
289
+ if (this.shakeSubscription) {
290
+ this.shakeSubscription.unsubscribe();
291
+ this.shakeSubscription = null;
292
+ }
293
+
294
+ this.elementRef = null;
295
+ }
296
+ }
297
+
298
+ registerDirective('shake', Shake);
@@ -8,8 +8,18 @@ import { calculateDistance, error } from '../engine/utils';
8
8
 
9
9
  const EVENTS = ['load', 'loaderror', 'playerror', 'play', 'end', 'pause', 'stop', 'mute', 'volume', 'rate', 'seek', 'fade', 'unlock']
10
10
 
11
+ /**
12
+ * Sound directive for playing audio with support for spatial audio and multiple sound sources
13
+ *
14
+ * This directive manages audio playback using Howler.js library. It supports:
15
+ * - Single or multiple sound sources
16
+ * - Spatial audio with distance-based volume calculation
17
+ * - All standard audio controls (play, pause, volume, etc.)
18
+ * - Event handling for audio lifecycle
19
+ *
20
+ */
11
21
  export class Sound extends Directive {
12
- private sound: Howl
22
+ private sounds: Howl[] = []
13
23
  private eventsFn: ((...args: any[]) => void)[] = []
14
24
  private maxVolume: number = 1
15
25
  private maxDistance: number = 100
@@ -20,21 +30,42 @@ export class Sound extends Directive {
20
30
  onMount(element: Element<Container>) {
21
31
  const { props } = element
22
32
  const tick = props.context.tick
23
- const { src, autoplay, loop, volume, spatial } = props.sound
24
- this.sound = new Howl({
25
- src,
26
- autoplay,
27
- loop,
28
- volume
29
- })
30
- for (let event of EVENTS) {
31
- if (!props.sound[event]) continue
32
- const fn = props.sound[event]
33
- this.eventsFn.push(fn)
34
- this.sound.on(event, fn);
33
+ const propsSound = props.sound.value ?? props.sound
34
+
35
+ // Check if src is null or undefined
36
+ if (!propsSound.src) {
37
+ return
35
38
  }
36
39
 
37
- if (spatial) {
40
+ const { src, autoplay, loop, volume, spatial } = propsSound
41
+
42
+ // Handle multiple sources
43
+ const sources = Array.isArray(src) ? src : [src]
44
+
45
+ // Create Howl instances for each source
46
+ for (const source of sources) {
47
+ if (!source) continue // Skip null/undefined sources
48
+
49
+ const sound = new Howl({
50
+ src: source,
51
+ autoplay,
52
+ loop,
53
+ volume
54
+ })
55
+
56
+ // Add event listeners for each sound
57
+ for (let event of EVENTS) {
58
+ if (!propsSound[event]) continue
59
+ const fn = propsSound[event]
60
+ this.eventsFn.push(fn)
61
+ sound.on(event, fn);
62
+ }
63
+
64
+ this.sounds.push(sound)
65
+ }
66
+
67
+ // Setup spatial audio if enabled
68
+ if (spatial && this.sounds.length > 0) {
38
69
  const { soundListenerPosition } = props.context
39
70
  if (!soundListenerPosition) {
40
71
  throw new error('SoundListenerPosition directive is required for spatial sound in component parent')
@@ -45,39 +76,71 @@ export class Sound extends Directive {
45
76
  const { x, y } = element.componentInstance
46
77
  const distance = calculateDistance(x, y, listenerX(), listenerY());
47
78
  const volume = Math.max(this.maxVolume - (distance / this.maxDistance), 0)
48
- this.sound.volume(volume)
79
+
80
+ // Apply volume to all sounds
81
+ this.sounds.forEach(sound => sound.volume(volume))
49
82
  }).subscription
50
83
  }
84
+
85
+ this.onUpdate(propsSound)
51
86
  }
52
87
 
53
88
  onUpdate(props: any) {
54
- const { volume, loop, mute, seek, playing, rate, spatial } = props
55
- if (volume != undefined) this.sound.volume(volume)
56
- if (loop != undefined) this.sound.loop(loop)
57
- if (mute != undefined) this.sound.mute(mute)
58
- if (seek != undefined) this.sound.seek(seek)
59
- if (playing != undefined) {
60
- if (playing) this.sound.play()
61
- else this.sound.pause()
62
- }
89
+ const soundProps = props.value ?? props
90
+ const { volume, loop, mute, seek, playing, rate, spatial } = soundProps
91
+ // Apply updates to all sounds
92
+ this.sounds.forEach(sound => {
93
+ if (volume !== undefined) sound.volume(volume)
94
+ if (loop !== undefined) sound.loop(loop)
95
+ if (mute !== undefined) sound.mute(mute)
96
+ if (seek !== undefined) sound.seek(seek)
97
+ if (playing !== undefined) {
98
+ if (playing) sound.play()
99
+ else sound.pause()
100
+ }
101
+ if (rate !== undefined) sound.rate(rate)
102
+ })
103
+
104
+ // Update spatial audio settings
63
105
  if (spatial) {
64
106
  this.maxVolume = spatial.maxVolume ?? this.maxVolume
65
107
  this.maxDistance = spatial.maxDistance ?? this.maxDistance
66
108
  }
67
- if (rate != undefined) this.sound.rate(rate)
68
109
  }
69
110
 
70
111
  onDestroy() {
71
- this.sound.stop()
72
- this.tickSubscription?.unsubscribe()
73
- for (let event of EVENTS) {
74
- if (this.eventsFn[event]) {
75
- this.sound.off(event, this.eventsFn[event]);
112
+ // Stop and clean up all sounds
113
+ this.sounds.forEach(sound => {
114
+ sound.stop()
115
+
116
+ // Remove event listeners
117
+ for (let event of EVENTS) {
118
+ const eventFn = this.eventsFn.find(fn => fn === this.eventsFn[event])
119
+ if (eventFn) {
120
+ sound.off(event, eventFn);
121
+ }
76
122
  }
77
- }
123
+ })
124
+
125
+ this.sounds = []
126
+ this.eventsFn = []
127
+ this.tickSubscription?.unsubscribe()
78
128
  }
79
129
  }
80
130
 
131
+ /**
132
+ * SoundListenerPosition directive for spatial audio
133
+ *
134
+ * This directive provides the listener position for spatial audio calculations.
135
+ * It should be placed on a parent component that contains spatial sound sources.
136
+ *
137
+ * @example
138
+ * ```tsx
139
+ * <Player soundListenerPosition={{ x: playerX, y: playerY }}>
140
+ * <Enemy sound={{ src: 'growl.mp3', spatial: { maxDistance: 100 } }} />
141
+ * </Player>
142
+ * ```
143
+ */
81
144
  class SoundListenerPosition extends Directive {
82
145
  onMount(element: Element<any>) {
83
146
  element.props.context.soundListenerPosition = element.propObservables?.soundListenerPosition
@@ -1,25 +1,56 @@
1
1
  import { ComponentInstance } from '../components/DisplayObject';
2
+ import { SignalOrPrimitive } from '../components/types';
2
3
  import { Directive, registerDirective } from '../engine/directive';
3
4
  import { Element } from '../engine/reactive';
4
5
  import { error } from '../engine/utils';
6
+ import { useProps } from '../hooks/useProps';
7
+
8
+ export type ViewportFollowProps = {
9
+ viewportFollow?: boolean | {
10
+ speed?: SignalOrPrimitive<number>;
11
+ acceleration?: SignalOrPrimitive<number>;
12
+ radius?: SignalOrPrimitive<number>;
13
+ };
14
+ }
5
15
 
6
16
  export class ViewportFollow extends Directive {
7
17
  onInit(element: Element<ComponentInstance>) {
8
18
 
9
19
  }
10
20
  onMount(element: Element) {
11
- const { viewportFollow } = element.props
12
- const { viewport } = element.props.context
21
+ this.onUpdate(element.props.viewportFollow, element)
22
+ }
23
+ onUpdate(viewportFollow: any, element: Element) {
24
+ const viewport = element.props.context?.viewport
13
25
  if (!viewport) {
14
- throw error('ViewportFollow directive requires a Viewport component to be mounted in the same context')
26
+ if (viewportFollow) {
27
+ throw error('ViewportFollow directive requires a Viewport component to be mounted in the same context')
28
+ }
29
+ return
30
+ }
31
+ if (viewportFollow) {
32
+ if (viewportFollow === true) {
33
+ viewport.follow(element.componentInstance)
34
+ } else {
35
+ const options = useProps(viewportFollow, {
36
+ speed: undefined,
37
+ acceleration: undefined,
38
+ radius: undefined
39
+ })
40
+ viewport.follow(element.componentInstance, {
41
+ speed: options.speed(),
42
+ acceleration: options.acceleration(),
43
+ radius: options.radius()
44
+ })
45
+ }
46
+ } else if (viewportFollow === null) {
47
+ viewport.plugins.remove('follow')
15
48
  }
16
- viewport.follow(element.componentInstance)
17
- }
18
- onUpdate(props: any) {
19
-
20
49
  }
21
- onDestroy() {
22
-
50
+ onDestroy(element: Element) {
51
+ const { viewportFollow } = element.props
52
+ const viewport = element.props.context?.viewport
53
+ if (viewportFollow && viewport) viewport.plugins.remove('follow')
23
54
  }
24
55
  }
25
56
 
@@ -1,7 +1,13 @@
1
- import './KeyboardControls'
2
- import './Scheduler'
3
- import './ViewportFollow'
1
+ export * from './ControlsBase'
2
+ export * from './KeyboardControls'
3
+ export * from './GamepadControls'
4
+ export * from './JoystickControls'
5
+ export * from './Controls'
6
+ export * from './Scheduler'
7
+ export * from './ViewportFollow'
4
8
  //import './ViewportCull'
5
- import './Sound'
6
- import './Drag'
7
- import './Transition'
9
+ export * from './Sound'
10
+ export * from './Drag'
11
+ export * from './Transition'
12
+ export * from './Shake'
13
+ export * from './Flash'