canvasengine 2.0.0-beta.2 → 2.0.0-beta.21
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/dist/index.d.ts +1289 -0
- package/dist/index.js +4248 -0
- package/dist/index.js.map +1 -0
- package/index.d.ts +4 -0
- package/package.json +5 -12
- package/src/components/Canvas.ts +53 -45
- package/src/components/Container.ts +2 -2
- package/src/components/DOMContainer.ts +123 -0
- package/src/components/DOMElement.ts +421 -0
- package/src/components/DisplayObject.ts +263 -189
- package/src/components/Graphic.ts +213 -36
- package/src/components/Mesh.ts +222 -0
- package/src/components/NineSliceSprite.ts +4 -1
- package/src/components/ParticleEmitter.ts +12 -8
- package/src/components/Sprite.ts +77 -14
- package/src/components/Text.ts +34 -14
- package/src/components/Video.ts +110 -0
- package/src/components/Viewport.ts +59 -43
- package/src/components/index.ts +6 -4
- package/src/components/types/DisplayObject.ts +30 -0
- package/src/directives/Drag.ts +357 -52
- package/src/directives/KeyboardControls.ts +3 -1
- package/src/directives/Sound.ts +94 -31
- package/src/directives/ViewportFollow.ts +35 -7
- package/src/engine/animation.ts +41 -5
- package/src/engine/bootstrap.ts +22 -3
- package/src/engine/directive.ts +2 -2
- package/src/engine/reactive.ts +337 -168
- package/src/engine/trigger.ts +65 -9
- package/src/engine/utils.ts +97 -9
- package/src/hooks/useProps.ts +1 -1
- package/src/index.ts +5 -1
- package/src/utils/RadialGradient.ts +29 -0
- package/src/utils/functions.ts +7 -0
- package/testing/index.ts +12 -0
- package/src/components/DrawMap/index.ts +0 -65
- package/src/components/Tilemap/Tile.ts +0 -79
- package/src/components/Tilemap/TileGroup.ts +0 -207
- package/src/components/Tilemap/TileLayer.ts +0 -163
- package/src/components/Tilemap/TileSet.ts +0 -41
- package/src/components/Tilemap/index.ts +0 -80
package/src/directives/Drag.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
349
|
+
private startDrag() {
|
|
350
|
+
if (this.isDragging || !this.stageRef) return;
|
|
77
351
|
|
|
78
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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(
|
|
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()
|
package/src/directives/Sound.ts
CHANGED
|
@@ -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
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
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
|
-
|
|
72
|
-
this.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|