canvasengine 2.0.0-beta.3 → 2.0.0-beta.30

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 (130) hide show
  1. package/dist/DebugRenderer-DcvJLrrD.js +172 -0
  2. package/dist/DebugRenderer-DcvJLrrD.js.map +1 -0
  3. package/dist/components/Button.d.ts +136 -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/Container.d.ts +80 -0
  8. package/dist/components/Container.d.ts.map +1 -0
  9. package/dist/components/DOMContainer.d.ts +77 -0
  10. package/dist/components/DOMContainer.d.ts.map +1 -0
  11. package/dist/components/DOMElement.d.ts +44 -0
  12. package/dist/components/DOMElement.d.ts.map +1 -0
  13. package/dist/components/DisplayObject.d.ts +82 -0
  14. package/dist/components/DisplayObject.d.ts.map +1 -0
  15. package/dist/components/Graphic.d.ts +65 -0
  16. package/dist/components/Graphic.d.ts.map +1 -0
  17. package/dist/components/Mesh.d.ts +202 -0
  18. package/dist/components/Mesh.d.ts.map +1 -0
  19. package/dist/components/NineSliceSprite.d.ts +17 -0
  20. package/dist/components/NineSliceSprite.d.ts.map +1 -0
  21. package/dist/components/ParticleEmitter.d.ts +5 -0
  22. package/dist/components/ParticleEmitter.d.ts.map +1 -0
  23. package/dist/components/Scene.d.ts +2 -0
  24. package/dist/components/Scene.d.ts.map +1 -0
  25. package/dist/components/Sprite.d.ts +174 -0
  26. package/dist/components/Sprite.d.ts.map +1 -0
  27. package/dist/components/Text.d.ts +21 -0
  28. package/dist/components/Text.d.ts.map +1 -0
  29. package/dist/components/TilingSprite.d.ts +18 -0
  30. package/dist/components/TilingSprite.d.ts.map +1 -0
  31. package/dist/components/Video.d.ts +15 -0
  32. package/dist/components/Video.d.ts.map +1 -0
  33. package/dist/components/Viewport.d.ts +106 -0
  34. package/dist/components/Viewport.d.ts.map +1 -0
  35. package/dist/components/index.d.ts +17 -0
  36. package/dist/components/index.d.ts.map +1 -0
  37. package/dist/components/types/DisplayObject.d.ts +106 -0
  38. package/dist/components/types/DisplayObject.d.ts.map +1 -0
  39. package/dist/components/types/MouseEvent.d.ts +4 -0
  40. package/dist/components/types/MouseEvent.d.ts.map +1 -0
  41. package/dist/components/types/Spritesheet.d.ts +366 -0
  42. package/dist/components/types/Spritesheet.d.ts.map +1 -0
  43. package/dist/components/types/index.d.ts +5 -0
  44. package/dist/components/types/index.d.ts.map +1 -0
  45. package/dist/directives/Drag.d.ts +70 -0
  46. package/dist/directives/Drag.d.ts.map +1 -0
  47. package/dist/directives/KeyboardControls.d.ts +530 -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/Sound.d.ts +26 -0
  52. package/dist/directives/Sound.d.ts.map +1 -0
  53. package/dist/directives/Transition.d.ts +11 -0
  54. package/dist/directives/Transition.d.ts.map +1 -0
  55. package/dist/directives/ViewportCull.d.ts +12 -0
  56. package/dist/directives/ViewportCull.d.ts.map +1 -0
  57. package/dist/directives/ViewportFollow.d.ts +19 -0
  58. package/dist/directives/ViewportFollow.d.ts.map +1 -0
  59. package/dist/directives/index.d.ts +2 -0
  60. package/dist/directives/index.d.ts.map +1 -0
  61. package/dist/engine/animation.d.ts +59 -0
  62. package/dist/engine/animation.d.ts.map +1 -0
  63. package/dist/engine/bootstrap.d.ts +16 -0
  64. package/dist/engine/bootstrap.d.ts.map +1 -0
  65. package/dist/engine/directive.d.ts +14 -0
  66. package/dist/engine/directive.d.ts.map +1 -0
  67. package/dist/engine/reactive.d.ts +95 -0
  68. package/dist/engine/reactive.d.ts.map +1 -0
  69. package/dist/engine/signal.d.ts +72 -0
  70. package/dist/engine/signal.d.ts.map +1 -0
  71. package/dist/engine/trigger.d.ts +51 -0
  72. package/dist/engine/trigger.d.ts.map +1 -0
  73. package/dist/engine/utils.d.ts +90 -0
  74. package/dist/engine/utils.d.ts.map +1 -0
  75. package/dist/hooks/addContext.d.ts +2 -0
  76. package/dist/hooks/addContext.d.ts.map +1 -0
  77. package/dist/hooks/useProps.d.ts +42 -0
  78. package/dist/hooks/useProps.d.ts.map +1 -0
  79. package/dist/hooks/useRef.d.ts +5 -0
  80. package/dist/hooks/useRef.d.ts.map +1 -0
  81. package/dist/index-C-iY-lCt.js +11080 -0
  82. package/dist/index-C-iY-lCt.js.map +1 -0
  83. package/dist/index.d.ts +15 -919
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.global.js +29 -0
  86. package/dist/index.global.js.map +1 -0
  87. package/dist/index.js +63 -2950
  88. package/dist/index.js.map +1 -1
  89. package/dist/utils/Ease.d.ts +17 -0
  90. package/dist/utils/Ease.d.ts.map +1 -0
  91. package/dist/utils/RadialGradient.d.ts +58 -0
  92. package/dist/utils/RadialGradient.d.ts.map +1 -0
  93. package/dist/utils/functions.d.ts +2 -0
  94. package/dist/utils/functions.d.ts.map +1 -0
  95. package/index.d.ts +4 -0
  96. package/package.json +12 -7
  97. package/src/components/Button.ts +269 -0
  98. package/src/components/Canvas.ts +53 -45
  99. package/src/components/Container.ts +2 -2
  100. package/src/components/DOMContainer.ts +123 -0
  101. package/src/components/DOMElement.ts +421 -0
  102. package/src/components/DisplayObject.ts +283 -190
  103. package/src/components/Graphic.ts +200 -34
  104. package/src/components/Mesh.ts +222 -0
  105. package/src/components/NineSliceSprite.ts +4 -1
  106. package/src/components/ParticleEmitter.ts +12 -8
  107. package/src/components/Sprite.ts +92 -22
  108. package/src/components/Text.ts +34 -14
  109. package/src/components/Video.ts +110 -0
  110. package/src/components/Viewport.ts +59 -43
  111. package/src/components/index.ts +7 -2
  112. package/src/components/types/DisplayObject.ts +30 -0
  113. package/src/directives/Drag.ts +357 -52
  114. package/src/directives/KeyboardControls.ts +3 -1
  115. package/src/directives/Sound.ts +94 -31
  116. package/src/directives/ViewportFollow.ts +35 -7
  117. package/src/engine/animation.ts +41 -5
  118. package/src/engine/bootstrap.ts +23 -3
  119. package/src/engine/directive.ts +2 -2
  120. package/src/engine/reactive.ts +472 -172
  121. package/src/engine/signal.ts +18 -2
  122. package/src/engine/trigger.ts +65 -9
  123. package/src/engine/utils.ts +97 -9
  124. package/src/hooks/useProps.ts +1 -1
  125. package/src/index.ts +4 -1
  126. package/src/utils/RadialGradient.ts +29 -0
  127. package/src/utils/functions.ts +7 -0
  128. package/testing/index.ts +12 -0
  129. package/tsconfig.json +17 -0
  130. package/vite.config.ts +39 -0
@@ -1,84 +1,389 @@
1
- import { effect, isSignal } from '@signe/reactive';
2
- import { Container, Rectangle } from 'pixi.js';
1
+ import { effect, isComputed, isSignal, signal } from '@signe/reactive';
2
+ import { Container, Rectangle, Point, FederatedPointerEvent } from 'pixi.js';
3
3
  import { Directive, registerDirective } from '../engine/directive';
4
4
  import { Element } from '../engine/reactive';
5
5
  import { snap } from 'popmotion';
6
6
  import { addContext } from '../hooks/addContext';
7
+ import { Subscription } from 'rxjs';
8
+ import { useProps } from '../hooks/useProps';
9
+ import { SignalOrPrimitive } from '../components/types';
10
+
11
+ export type DragProps = {
12
+ move?: (event: FederatedPointerEvent) => void;
13
+ start?: () => void;
14
+ end?: () => void;
15
+ snap?: SignalOrPrimitive<number>;
16
+ direction?: SignalOrPrimitive<'x' | 'y' | 'all'>;
17
+ keyToPress?: SignalOrPrimitive<string[]>;
18
+ viewport?: {
19
+ edgeThreshold?: SignalOrPrimitive<number>;
20
+ maxSpeed?: SignalOrPrimitive<number>;
21
+ };
22
+ }
7
23
 
8
24
  export class Drop extends Directive {
9
- onInit(element: Element<Container>) {}
25
+ private elementRef: Element<Container> | null = null;
26
+
27
+ onInit(element: Element<Container>) {
28
+ this.elementRef = element;
29
+ }
10
30
 
11
31
  onMount(element: Element<Container>) {
12
- addContext(element, 'drop', element)
32
+ addContext(element, 'drop', element);
13
33
  }
14
34
 
15
35
  onUpdate() {}
16
36
 
17
- onDestroy() {}
37
+ onDestroy() {
38
+ this.elementRef = null;
39
+ }
18
40
  }
19
41
 
20
42
  export class Drag extends Directive {
21
- onInit(element: Element<Container>) {}
43
+ private elementRef: Element<Container> | null = null;
44
+ private stageRef: Container | null = null;
45
+ private offsetInParent = new Point();
46
+ private isDragging = false;
47
+ private viewport: any | null = null;
48
+ private animationFrameId: number | null = null;
49
+ private lastPointerPosition: Point = new Point();
50
+ private pressedKeys: Set<string> = new Set();
51
+ private pointerIsDown = false;
52
+
53
+ private onDragMoveHandler: (event: FederatedPointerEvent) => void = () => {};
54
+ private onDragEndHandler: () => void = () => {};
55
+ private onDragStartHandler: (event: FederatedPointerEvent) => void = () => {};
56
+ private onKeyDownHandler: (event: KeyboardEvent) => void = () => {};
57
+ private onKeyUpHandler: (event: KeyboardEvent) => void = () => {};
58
+
59
+ private subscriptions: Subscription[] = [];
60
+
61
+ onInit(element: Element<Container>) {
62
+ this.elementRef = element;
63
+ this.onDragMoveHandler = this.onDragMove.bind(this);
64
+ this.onDragEndHandler = this.onDragEnd.bind(this);
65
+ this.onDragStartHandler = this.onPointerDown.bind(this);
66
+ this.onKeyDownHandler = this.onKeyDown.bind(this);
67
+ this.onKeyUpHandler = this.onKeyUp.bind(this);
68
+ }
22
69
 
23
70
  onMount(element: Element<Container>) {
24
- const { rootElement, canvasSize } = element.props.context
25
- const { propObservables } = element
26
- const { drag } = element.props
27
- const instance = element.componentInstance
28
- const stage = rootElement.componentInstance
29
- instance.eventMode = 'static'
30
- stage.eventMode = 'static'
31
-
32
- const snapTo = snap(drag?.snap ?? 0);
33
-
34
- effect(() => {
35
- stage.hitArea = new Rectangle(0, 0, canvasSize().width, canvasSize().height)
36
- })
37
-
38
- let x = 0
39
- let y = 0
40
-
41
- const onDragMove = (event) => {
42
- drag.move?.(event)
43
- x += event.movementX
44
- y += event.movementY
45
- if (drag?.snap) {
46
- instance.position.x = snapTo(x)
47
- instance.position.y = snapTo(y)
48
- } else {
49
- instance.position.x = x
50
- instance.position.y = y
71
+ const { rootElement, canvasSize, viewport, tick } = element.props.context;
72
+ const instance = element.componentInstance;
73
+ const dragProps = this.dragProps;
74
+ const haveNotProps = Object.keys(dragProps).length === 0;
75
+
76
+ if (haveNotProps) {
77
+ this.onDestroy();
78
+ return;
79
+ }
80
+
81
+ if (!instance) return;
82
+ this.stageRef = rootElement.componentInstance;
83
+ if (!this.stageRef) return;
84
+ this.viewport = viewport;
85
+
86
+ instance.eventMode = 'static';
87
+ this.stageRef.eventMode = 'static';
88
+
89
+ const _effect = effect(() => {
90
+ if (this.stageRef) {
91
+ this.stageRef.hitArea = new Rectangle(0, 0, canvasSize().width, canvasSize().height);
51
92
  }
52
- const { x: xProp, y: yProp } = propObservables as any
53
- if (xProp !== undefined && isSignal(xProp)) {
54
- xProp.set(instance.position.x)
93
+ });
94
+
95
+ instance.on('pointerdown', this.onDragStartHandler);
96
+ this.stageRef.on('pointerup', this.onDragEndHandler);
97
+ this.stageRef.on('pointerupoutside', this.onDragEndHandler);
98
+
99
+ const keysToPress = dragProps.keyToPress ? dragProps.keyToPress : [];
100
+
101
+ // Always add keyboard event listeners to track pressed keys
102
+ window.addEventListener('keydown', this.onKeyDownHandler);
103
+ window.addEventListener('keyup', this.onKeyUpHandler);
104
+
105
+ this.subscriptions = [
106
+ tick.observable.subscribe(() => {
107
+ if (this.isDragging && this.viewport) {
108
+ this.updateViewportPosition(this.lastPointerPosition);
109
+ }
110
+ }),
111
+ _effect.subscription
112
+ ]
113
+ }
114
+
115
+ get dragProps() {
116
+ const drag = this.elementRef?.props.drag
117
+ const options = useProps(drag?.value ?? drag, {
118
+ snap: 0,
119
+ viewport: {},
120
+ direction: 'all',
121
+ keyToPress: []
122
+ });
123
+ options.viewport = useProps(options.viewport, {
124
+ edgeThreshold: 300,
125
+ maxSpeed: 40
126
+ });
127
+ return options;
128
+ }
129
+
130
+ get axis() {
131
+ const direction = this.dragProps.direction();
132
+ const axis = {
133
+ x: true,
134
+ y: true,
135
+ }
136
+ if (direction === 'x') {
137
+ axis.y = false;
138
+ }
139
+ if (direction === 'y') {
140
+ axis.x = false;
141
+ }
142
+ return axis;
143
+ }
144
+
145
+ /**
146
+ * Updates element position when dragging and starts continuous viewport movement
147
+ * @param event The pointer event that triggered the drag move
148
+ */
149
+ private onDragMove(event: FederatedPointerEvent) {
150
+ if (!this.isDragging || !this.elementRef?.componentInstance || !this.elementRef.componentInstance.parent) return;
151
+
152
+ const instance = this.elementRef.componentInstance;
153
+ const parent = instance.parent;
154
+ const dragProps = this.dragProps;
155
+ const propObservables = this.elementRef.propObservables;
156
+ const snapTo = snap(dragProps?.snap() ?? 0);
157
+
158
+ dragProps?.move?.(event);
159
+
160
+ const currentParentLocalPointer = parent.toLocal(event.global);
161
+
162
+ const newX = currentParentLocalPointer.x - this.offsetInParent.x;
163
+ const newY = currentParentLocalPointer.y - this.offsetInParent.y;
164
+
165
+ if (dragProps?.snap()) {
166
+ instance.position.x = snapTo(newX);
167
+ instance.position.y = snapTo(newY);
168
+ } else {
169
+ if (this.axis.x) instance.position.x = newX;
170
+ if (this.axis.y) instance.position.y = newY;
171
+ }
172
+
173
+ // Store the last pointer position for continuous viewport movement
174
+ this.lastPointerPosition.copyFrom(event.global);
175
+
176
+ const { x: xProp, y: yProp } = propObservables as any;
177
+
178
+ const updatePosition = (prop: any, value: number) => {
179
+ if (isComputed(prop)) {
180
+ prop.dependencies.forEach(dependency => {
181
+ dependency.set(value)
182
+ })
183
+ } else if (isSignal(prop)) {
184
+ prop.set(value)
55
185
  }
56
- if (yProp !== undefined && isSignal(yProp)) {
57
- yProp.set(instance.position.y)
186
+ }
187
+
188
+ if (xProp !== undefined) updatePosition(xProp, instance.position.x)
189
+ if (yProp !== undefined) updatePosition(yProp, instance.position.y)
190
+ }
191
+
192
+ /**
193
+ * Moves the viewport if the dragged element is near screen edges
194
+ * @param globalPosition The global pointer position
195
+ */
196
+ private updateViewportPosition(globalPosition: Point) {
197
+ if (!this.viewport || !this.elementRef) return;
198
+
199
+ const dragProps = this.dragProps;
200
+ const edgeThreshold = dragProps?.viewport?.edgeThreshold(); // Distance from edge to trigger viewport movement
201
+ const maxSpeed = dragProps?.viewport?.maxSpeed(); // Maximum speed when element is at the very edge
202
+
203
+ // Calculate screen boundaries
204
+ const screenLeft = 0;
205
+ const screenRight = this.viewport.screenWidth;
206
+ const screenTop = 0;
207
+ const screenBottom = this.viewport.screenHeight;
208
+ const instance = this.elementRef.componentInstance;
209
+
210
+ // Calculate distances from element to screen edges
211
+ const distanceFromLeft = globalPosition.x - screenLeft;
212
+ const distanceFromRight = screenRight - globalPosition.x;
213
+ const distanceFromTop = globalPosition.y - screenTop;
214
+ const distanceFromBottom = screenBottom - globalPosition.y;
215
+
216
+ let moveX = 0;
217
+ let moveY = 0;
218
+
219
+ // Calculate horizontal movement with dynamic velocity
220
+ if (distanceFromLeft < edgeThreshold) {
221
+ // Velocity increases as distance decreases
222
+ // When distance = 0, velocity = maxSpeed
223
+ // When distance = threshold, velocity = 0
224
+ const velocity = maxSpeed * (1 - (distanceFromLeft / edgeThreshold));
225
+ moveX = -velocity;
226
+ } else if (distanceFromRight < edgeThreshold) {
227
+ const velocity = maxSpeed * (1 - (distanceFromRight / edgeThreshold));
228
+ moveX = velocity;
229
+ }
230
+
231
+ // Calculate vertical movement with dynamic velocity
232
+ if (distanceFromTop < edgeThreshold) {
233
+ const velocity = maxSpeed * (1 - (distanceFromTop / edgeThreshold));
234
+ moveY = -velocity;
235
+ } else if (distanceFromBottom < edgeThreshold) {
236
+ const velocity = maxSpeed * (1 - (distanceFromBottom / edgeThreshold));
237
+ moveY = velocity;
238
+ }
239
+
240
+ // Apply movement with velocity-based displacement
241
+ if (moveX !== 0 || moveY !== 0) {
242
+ const lastViewValue = this.viewport.center;
243
+ this.viewport.moveCenter(
244
+ this.viewport.center.x + moveX,
245
+ this.viewport.center.y + moveY
246
+ );
247
+ if (this.axis.x && lastViewValue.x !== this.viewport.center.x) {
248
+ instance.position.x += moveX;
249
+ }
250
+ if (this.axis.y && lastViewValue.y !== this.viewport.center.y) {
251
+ instance.position.y += moveY;
58
252
  }
59
253
  }
254
+ }
255
+
256
+ /**
257
+ * Handles drag end event and stops viewport movement
258
+ */
259
+ private onDragEnd() {
260
+ this.pointerIsDown = false;
261
+
262
+ if (!this.isDragging) return;
60
263
 
61
- const onDragEnd = () => {
62
- drag.end?.()
63
- stage.off('pointermove', onDragMove)
64
- console.log(rootElement.allElements)
65
- }
264
+ const dragProps = this.dragProps;
265
+ this.isDragging = false;
266
+
267
+ dragProps?.end?.();
268
+
269
+ if (this.stageRef) {
270
+ this.stageRef.off('pointermove', this.onDragMoveHandler);
271
+ }
272
+ }
273
+
274
+ onKeyDown(event: KeyboardEvent) {
275
+ this.pressedKeys.add(event.code);
276
+ this.pressedKeys.add(event.key.toLowerCase());
277
+
278
+ if (this.pointerIsDown && !this.isDragging && this.areRequiredKeysPressed()) {
279
+ this.startDrag();
280
+ }
281
+ }
282
+
283
+ onKeyUp(event: KeyboardEvent) {
284
+ this.pressedKeys.delete(event.code);
285
+ this.pressedKeys.delete(event.key.toLowerCase());
286
+ if (this.isDragging && !this.areRequiredKeysPressed()) {
287
+ this.onDragEnd();
288
+ }
289
+ }
66
290
 
67
- instance.on('pointerdown', () => {
68
- drag.start?.()
69
- stage.on('pointermove', onDragMove)
291
+ private areRequiredKeysPressed(): boolean {
292
+ const keyToPress = this.dragProps.keyToPress ? this.dragProps.keyToPress : [];
293
+ if (!keyToPress || keyToPress.length === 0) {
294
+ return true; // No keys required, always return true
295
+ }
296
+
297
+ return keyToPress.some(key => {
298
+ // Check if the key is pressed directly
299
+ if (this.pressedKeys.has(key)) {
300
+ return true;
301
+ }
302
+
303
+ // Check common alternative formats
304
+ // Space key can be "Space", " ", or "space"
305
+ if (key.toLowerCase() === 'space') {
306
+ return this.pressedKeys.has('Space') || this.pressedKeys.has(' ');
307
+ }
308
+
309
+ // Shift key can be "ShiftLeft", "ShiftRight", or "shift"
310
+ if (key.toLowerCase() === 'shift') {
311
+ return this.pressedKeys.has('ShiftLeft') || this.pressedKeys.has('ShiftRight');
312
+ }
313
+
314
+ // Control key can be "ControlLeft", "ControlRight", or "control"
315
+ if (key.toLowerCase() === 'control' || key.toLowerCase() === 'ctrl') {
316
+ return this.pressedKeys.has('ControlLeft') || this.pressedKeys.has('ControlRight');
317
+ }
318
+
319
+ // Alt key can be "AltLeft", "AltRight", or "alt"
320
+ if (key.toLowerCase() === 'alt') {
321
+ return this.pressedKeys.has('AltLeft') || this.pressedKeys.has('AltRight');
322
+ }
323
+
324
+ return false;
70
325
  });
326
+ }
327
+
328
+ private onPointerDown(event: FederatedPointerEvent) {
329
+ if (!this.elementRef?.componentInstance || !this.stageRef || !this.elementRef.componentInstance.parent) return;
330
+
331
+ this.pointerIsDown = true;
332
+
333
+ const instance = this.elementRef.componentInstance;
334
+ const parent = instance.parent;
335
+
336
+ const parentLocalPointer = parent.toLocal(event.global);
71
337
 
72
- stage.on('pointerup', onDragEnd)
73
- stage.on('pointerupoutside', onDragEnd)
338
+ this.offsetInParent.x = parentLocalPointer.x - instance.position.x;
339
+ this.offsetInParent.y = parentLocalPointer.y - instance.position.y;
340
+
341
+ // Store initial pointer position
342
+ this.lastPointerPosition.copyFrom(event.global);
343
+
344
+ if (this.areRequiredKeysPressed()) {
345
+ this.startDrag();
346
+ }
74
347
  }
75
348
 
76
- onUpdate() {}
349
+ private startDrag() {
350
+ if (this.isDragging || !this.stageRef) return;
77
351
 
78
- onDestroy() {
352
+ this.isDragging = true;
353
+ const dragProps = this.dragProps;
354
+ dragProps?.start?.();
355
+ this.stageRef.on('pointermove', this.onDragMoveHandler);
356
+ }
79
357
 
358
+ onUpdate(props) {
359
+ if (props.type && props.type === 'reset') {
360
+ this.onDestroy();
361
+ this.onMount(this.elementRef);
362
+ }
363
+ }
364
+
365
+ onDestroy() {
366
+ this.subscriptions.forEach(subscription => subscription.unsubscribe());
367
+ const instance = this.elementRef?.componentInstance;
368
+ if (instance) {
369
+ instance.off('pointerdown', this.onDragStartHandler);
370
+ }
371
+ if (this.stageRef) {
372
+ this.stageRef.off('pointermove', this.onDragMoveHandler);
373
+ this.stageRef.off('pointerup', this.onDragEndHandler);
374
+ this.stageRef.off('pointerupoutside', this.onDragEndHandler);
375
+ }
376
+
377
+ // Remove keyboard event listeners
378
+ window.removeEventListener('keydown', this.onKeyDownHandler);
379
+ window.removeEventListener('keyup', this.onKeyUpHandler);
380
+
381
+ this.stageRef = null;
382
+ this.viewport = null;
383
+ this.pressedKeys.clear();
384
+ this.pointerIsDown = false;
80
385
  }
81
386
  }
82
387
 
83
- // registerDirective('drag', Drag)
84
- // registerDirective('drop', Drop)
388
+ registerDirective('drag', Drag);
389
+ registerDirective('drop', Drop);
@@ -366,8 +366,10 @@ export class KeyboardControls extends Directive {
366
366
  };
367
367
 
368
368
  onInit(element: Element) {
369
+ const value = element.props.controls.value ?? element.props.controls
370
+ if (!value) return
369
371
  this.setupListeners();
370
- this.setInputs(element.props.controls.value)
372
+ this.setInputs(value)
371
373
  // The processing is outside the rendering loop because if the FPS are lower (or higher) then the sending to the server would be slower or faster. Here it is constant
372
374
  this.interval = setInterval(() => {
373
375
  this.preStep()
@@ -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,53 @@
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
21
+ this.onUpdate(element.props.viewportFollow, element)
22
+ }
23
+ onUpdate(viewportFollow: any, element: Element) {
12
24
  const { viewport } = element.props.context
13
25
  if (!viewport) {
14
26
  throw error('ViewportFollow directive requires a Viewport component to be mounted in the same context')
15
27
  }
16
- viewport.follow(element.componentInstance)
17
- }
18
- onUpdate(props: any) {
19
-
28
+ if (viewportFollow) {
29
+ if (viewportFollow === true) {
30
+ viewport.follow(element.componentInstance)
31
+ } else {
32
+ const options = useProps(viewportFollow, {
33
+ speed: undefined,
34
+ acceleration: undefined,
35
+ radius: undefined
36
+ })
37
+ viewport.follow(element.componentInstance, {
38
+ speed: options.speed(),
39
+ acceleration: options.acceleration(),
40
+ radius: options.radius()
41
+ })
42
+ }
43
+ } else if (viewportFollow === null) {
44
+ viewport.plugins.remove('follow')
45
+ }
20
46
  }
21
- onDestroy() {
22
-
47
+ onDestroy(element: Element) {
48
+ const { viewportFollow } = element.props
49
+ const { viewport } = element.props.context
50
+ if (viewportFollow) viewport.plugins.remove('follow')
23
51
  }
24
52
  }
25
53