canvasengine 2.0.0-beta.38 → 2.0.0-beta.39

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 (32) hide show
  1. package/dist/{DebugRenderer-DxJSMb9B.js → DebugRenderer-Rrw9FlTd.js} +2 -2
  2. package/dist/{DebugRenderer-DxJSMb9B.js.map → DebugRenderer-Rrw9FlTd.js.map} +1 -1
  3. package/dist/components/Button.d.ts +50 -3
  4. package/dist/components/Button.d.ts.map +1 -1
  5. package/dist/components/Joystick.d.ts +36 -0
  6. package/dist/components/Joystick.d.ts.map +1 -0
  7. package/dist/components/index.d.ts +1 -0
  8. package/dist/components/index.d.ts.map +1 -1
  9. package/dist/directives/Controls.d.ts +16 -7
  10. package/dist/directives/Controls.d.ts.map +1 -1
  11. package/dist/directives/GamepadControls.d.ts +3 -1
  12. package/dist/directives/GamepadControls.d.ts.map +1 -1
  13. package/dist/directives/JoystickControls.d.ts +172 -0
  14. package/dist/directives/JoystickControls.d.ts.map +1 -0
  15. package/dist/directives/index.d.ts +1 -0
  16. package/dist/directives/index.d.ts.map +1 -1
  17. package/dist/engine/reactive.d.ts.map +1 -1
  18. package/dist/{index-BgNWflRE.js → index-BQ99FClW.js} +5543 -5141
  19. package/dist/index-BQ99FClW.js.map +1 -0
  20. package/dist/index.global.js +7 -7
  21. package/dist/index.global.js.map +1 -1
  22. package/dist/index.js +59 -57
  23. package/package.json +1 -1
  24. package/src/components/Button.ts +168 -41
  25. package/src/components/Joystick.ts +361 -0
  26. package/src/components/index.ts +2 -1
  27. package/src/directives/Controls.ts +42 -8
  28. package/src/directives/GamepadControls.ts +40 -11
  29. package/src/directives/JoystickControls.ts +396 -0
  30. package/src/directives/index.ts +1 -0
  31. package/src/engine/reactive.ts +41 -14
  32. package/dist/index-BgNWflRE.js.map +0 -1
@@ -0,0 +1,396 @@
1
+ import { ControlsBase, Controls } from "./ControlsBase";
2
+
3
+ /**
4
+ * Joystick directions reported by the Joystick component
5
+ */
6
+ export type JoystickDirection =
7
+ | 'left'
8
+ | 'right'
9
+ | 'top'
10
+ | 'bottom'
11
+ | 'top_left'
12
+ | 'top_right'
13
+ | 'bottom_left'
14
+ | 'bottom_right';
15
+
16
+ /**
17
+ * Joystick change event payload
18
+ */
19
+ export interface JoystickChangeEvent {
20
+ angle: number;
21
+ direction: JoystickDirection;
22
+ power: number;
23
+ }
24
+
25
+ /**
26
+ * Joystick configuration interface
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const joystickConfig: JoystickConfig = {
31
+ * enabled: true,
32
+ * directionMapping: {
33
+ * 'top': 'up',
34
+ * 'bottom': 'down',
35
+ * 'left': 'left',
36
+ * 'right': 'right',
37
+ * 'top_left': ['up', 'left'],
38
+ * 'top_right': ['up', 'right'],
39
+ * 'bottom_left': ['down', 'left'],
40
+ * 'bottom_right': ['down', 'right']
41
+ * },
42
+ * moveInterval: 50,
43
+ * threshold: 0.1
44
+ * };
45
+ * ```
46
+ */
47
+ export interface JoystickConfig {
48
+ /** Whether joystick is enabled (default: true) */
49
+ enabled?: boolean;
50
+ /** Mapping of joystick direction names to control names (can be single string or array for diagonals) */
51
+ directionMapping?: {
52
+ [joystickDirection: string]: string | string[]; // e.g., 'top' -> 'up', 'top_left' -> ['up', 'left']
53
+ };
54
+ /** Interval in milliseconds for repeating movement actions (default: 50) */
55
+ moveInterval?: number;
56
+ /** Threshold for power value to trigger movement (default: 0.1) */
57
+ threshold?: number;
58
+ }
59
+
60
+ /**
61
+ * Default direction mapping
62
+ */
63
+ const DEFAULT_DIRECTION_MAPPING: { [direction: string]: string | string[] } = {
64
+ 'top': 'up',
65
+ 'bottom': 'down',
66
+ 'left': 'left',
67
+ 'right': 'right',
68
+ 'top_left': ['up', 'left'],
69
+ 'top_right': ['up', 'right'],
70
+ 'bottom_left': ['down', 'left'],
71
+ 'bottom_right': ['down', 'right']
72
+ };
73
+
74
+ /**
75
+ * Joystick input controls implementation
76
+ *
77
+ * Handles joystick input events from the Joystick component and maps them to control actions.
78
+ * Supports directional movement with configurable mappings, including diagonal directions.
79
+ *
80
+ * The joystick controls work by receiving change events from a Joystick component instance.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * const joystickControls = new JoystickControls();
85
+ * joystickControls.setInputs({
86
+ * up: {
87
+ * repeat: true,
88
+ * bind: 'up',
89
+ * keyDown() {
90
+ * console.log('Up pressed');
91
+ * }
92
+ * }
93
+ * });
94
+ * joystickControls.updateJoystickConfig({
95
+ * enabled: true,
96
+ * directionMapping: {
97
+ * 'top': 'up'
98
+ * }
99
+ * });
100
+ * joystickControls.start();
101
+ *
102
+ * // Later, when joystick changes:
103
+ * joystickControls.handleJoystickChange({ angle: 90, direction: Direction.TOP, power: 0.8 });
104
+ * ```
105
+ */
106
+ export class JoystickControls extends ControlsBase {
107
+ private joystickEnabled: boolean = true;
108
+ private joystickConfig: JoystickConfig = {
109
+ enabled: true,
110
+ directionMapping: DEFAULT_DIRECTION_MAPPING,
111
+ moveInterval: 50,
112
+ threshold: 0.1
113
+ };
114
+ private joystickMoving: boolean = false;
115
+ private joystickDirections: { [direction: string]: boolean } = {};
116
+ private joystickLastUpdate: number = 0;
117
+ private joystickMoveInterval: any = null;
118
+ private currentPower: number = 0;
119
+
120
+ /**
121
+ * Setup joystick event listeners
122
+ * Note: Joystick events are handled via handleJoystickChange() method
123
+ */
124
+ protected setupListeners(): void {
125
+ // Joystick events are handled externally via handleJoystickChange()
126
+ // This method is kept for consistency with ControlsBase interface
127
+ }
128
+
129
+ /**
130
+ * Cleanup joystick intervals
131
+ */
132
+ protected cleanup(): void {
133
+ if (this.joystickMoveInterval) {
134
+ clearInterval(this.joystickMoveInterval);
135
+ this.joystickMoveInterval = null;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Process joystick inputs each step
141
+ * Continuous actions are handled by the interval; no inactivity timeout here.
142
+ */
143
+ protected preStep(): void {
144
+ if (this.stop) return;
145
+ // No-op: continuous movement is driven by processJoystickMovement interval
146
+ }
147
+
148
+ /**
149
+ * Handle joystick change event
150
+ * Called by the Joystick component when its position changes
151
+ *
152
+ * @param event - Joystick change event containing angle, direction, and power
153
+ */
154
+ handleJoystickChange(event: JoystickChangeEvent): void {
155
+ if (!this.joystickEnabled || !this.joystickConfig.enabled) return;
156
+ if (this.stop) return;
157
+
158
+ this.joystickLastUpdate = Date.now();
159
+ this.currentPower = event.power;
160
+
161
+ // Check threshold
162
+ if (event.power < (this.joystickConfig.threshold || 0.1)) {
163
+ // Power too low, stop all movements
164
+ this.stopAllMovements();
165
+ return;
166
+ }
167
+
168
+ const directionMapping = this.joystickConfig.directionMapping || DEFAULT_DIRECTION_MAPPING;
169
+ const directionKey = event.direction;
170
+ const mappedControls = directionMapping[directionKey];
171
+
172
+ if (!mappedControls) {
173
+ // No mapping for this direction, stop all movements
174
+ this.stopAllMovements();
175
+ return;
176
+ }
177
+
178
+ // Convert to array if single string
179
+ const controlNames = Array.isArray(mappedControls) ? mappedControls : [mappedControls];
180
+
181
+ // Determine which directions to activate and deactivate
182
+ const previousDirections = this.joystickDirections;
183
+ const newDirections: { [dir: string]: boolean } = {};
184
+ controlNames.forEach(controlName => {
185
+ newDirections[controlName] = true;
186
+ });
187
+
188
+ // Deactivate directions that are no longer active
189
+ const allDirections = new Set([
190
+ ...Object.keys(this.joystickDirections),
191
+ ...Object.keys(newDirections)
192
+ ]);
193
+
194
+ for (const dir of allDirections) {
195
+ const wasActive = this.joystickDirections[dir];
196
+ const shouldBeActive = newDirections[dir] || false;
197
+
198
+ if (wasActive && !shouldBeActive) {
199
+ // Deactivate this direction
200
+ this.applyControl(dir, false).catch(() => {
201
+ // Ignore errors
202
+ });
203
+ }
204
+ }
205
+
206
+ // Update active directions
207
+ this.joystickDirections = { ...newDirections };
208
+ this.joystickMoving = true;
209
+
210
+ // Activate new directions
211
+ const directionsToActivate = controlNames.filter((name) => !previousDirections[name]);
212
+ for (const controlName of directionsToActivate) {
213
+ this.applyControl(controlName, true, { power: event.power }).catch(() => {
214
+ // Ignore errors
215
+ });
216
+ }
217
+
218
+ // Start movement interval if not already running
219
+ if (!this.joystickMoveInterval) {
220
+ this.joystickMoveInterval = setInterval(() => {
221
+ this.processJoystickMovement();
222
+ }, this.joystickConfig.moveInterval || 50);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Handle joystick start event
228
+ * Called when user starts interacting with the joystick
229
+ */
230
+ handleJoystickStart(): void {
231
+ if (!this.joystickEnabled || !this.joystickConfig.enabled) return;
232
+ if (this.stop) return;
233
+ // Start event doesn't need special handling, change event will handle activation
234
+ }
235
+
236
+ /**
237
+ * Handle joystick end event
238
+ * Called when user stops interacting with the joystick
239
+ */
240
+ handleJoystickEnd(): void {
241
+ if (!this.joystickEnabled || !this.joystickConfig.enabled) return;
242
+ this.stopAllMovements();
243
+ }
244
+
245
+ /**
246
+ * Stop all active joystick movements
247
+ */
248
+ private stopAllMovements(): void {
249
+ if (this.joystickMoveInterval) {
250
+ clearInterval(this.joystickMoveInterval);
251
+ this.joystickMoveInterval = null;
252
+ }
253
+
254
+ const allDirections = Object.keys(this.joystickDirections);
255
+ for (const dir of allDirections) {
256
+ this.applyControl(dir, false).catch(() => {
257
+ // Ignore errors
258
+ });
259
+ }
260
+
261
+ this.joystickDirections = {};
262
+ this.joystickMoving = false;
263
+ this.currentPower = 0;
264
+ }
265
+
266
+ /**
267
+ * Process continuous joystick movement
268
+ * Called at intervals to repeat movement actions while joystick is active
269
+ */
270
+ private processJoystickMovement(): void {
271
+ if (!this.joystickMoving) return;
272
+ if (this.stop) return;
273
+
274
+ for (const direction in this.joystickDirections) {
275
+ if (this.joystickDirections[direction]) {
276
+ this.applyControl(direction, true, { power: this.currentPower }).catch(() => {
277
+ // Ignore errors
278
+ });
279
+ }
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Update joystick configuration
285
+ * Merges provided config with defaults
286
+ *
287
+ * @param config - Partial joystick configuration
288
+ */
289
+ updateJoystickConfig(config: Partial<JoystickConfig>): void {
290
+ this.joystickConfig = {
291
+ enabled: config.enabled !== undefined ? config.enabled : true,
292
+ directionMapping: config.directionMapping || DEFAULT_DIRECTION_MAPPING,
293
+ moveInterval: config.moveInterval || 50,
294
+ threshold: config.threshold || 0.1
295
+ };
296
+ }
297
+
298
+ /**
299
+ * Extract joystick config from controls configuration and update
300
+ *
301
+ * @param inputs - Controls configuration that may contain a 'joystick' property
302
+ */
303
+ extractJoystickConfig(inputs: Controls & { joystick?: JoystickConfig }): void {
304
+ if (inputs.joystick) {
305
+ this.updateJoystickConfig(inputs.joystick);
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Get the current joystick configuration
311
+ *
312
+ * @returns The joystick configuration object
313
+ */
314
+ getJoystickConfig(): JoystickConfig {
315
+ return this.joystickConfig;
316
+ }
317
+
318
+ /**
319
+ * Apply a control action programmatically
320
+ * Uses the bound controls to trigger actions
321
+ *
322
+ * @param controlName - Name of the control
323
+ * @param isDown - Whether the control is pressed (true) or released (false)
324
+ * @returns Promise that resolves when the action is complete
325
+ */
326
+ async applyControl(controlName: string | number, isDown?: boolean, payload?: any): Promise<void> {
327
+ const control = this._controlsOptions[controlName];
328
+ if (!control) return;
329
+
330
+ // Find the bound key for this control
331
+ const boundKeys = Object.keys(this.boundKeys);
332
+ for (const keyName of boundKeys) {
333
+ const boundKey = this.boundKeys[keyName];
334
+ if (boundKey.actionName === String(controlName)) {
335
+ // Execute the control callback
336
+ if (isDown === undefined) {
337
+ // Press and release (simulate button press)
338
+ if (boundKey.options.keyDown) {
339
+ let parameters = payload ?? boundKey.parameters;
340
+ if (typeof parameters === "function") {
341
+ parameters = parameters();
342
+ }
343
+ boundKey.options.keyDown(boundKey, parameters);
344
+ }
345
+ // Release after a short delay (similar to keyboard)
346
+ return new Promise((resolve) => {
347
+ setTimeout(() => {
348
+ if (boundKey.options.keyUp) {
349
+ let parameters = payload ?? boundKey.parameters;
350
+ if (typeof parameters === "function") {
351
+ parameters = parameters();
352
+ }
353
+ boundKey.options.keyUp(boundKey, parameters);
354
+ }
355
+ resolve();
356
+ }, 200);
357
+ });
358
+ } else if (isDown) {
359
+ if (boundKey.options.keyDown) {
360
+ let parameters = payload ?? boundKey.parameters;
361
+ if (typeof parameters === "function") {
362
+ parameters = parameters();
363
+ }
364
+ boundKey.options.keyDown(boundKey, parameters);
365
+ }
366
+ } else {
367
+ if (boundKey.options.keyUp) {
368
+ let parameters = payload ?? boundKey.parameters;
369
+ if (typeof parameters === "function") {
370
+ parameters = parameters();
371
+ }
372
+ boundKey.options.keyUp(boundKey, parameters);
373
+ }
374
+ }
375
+ break;
376
+ }
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Override setInputs to extract joystick config
382
+ */
383
+ setInputs(inputs: Controls & { joystick?: JoystickConfig }): void {
384
+ super.setInputs(inputs);
385
+ this.extractJoystickConfig(inputs);
386
+ }
387
+
388
+ /**
389
+ * Check if joystick is currently active
390
+ *
391
+ * @returns true if joystick is moving, false otherwise
392
+ */
393
+ isActive(): boolean {
394
+ return this.joystickMoving;
395
+ }
396
+ }
@@ -1,6 +1,7 @@
1
1
  export * from './ControlsBase'
2
2
  export * from './KeyboardControls'
3
3
  export * from './GamepadControls'
4
+ export * from './JoystickControls'
4
5
  export * from './Controls'
5
6
  export * from './Scheduler'
6
7
  export * from './ViewportFollow'
@@ -301,8 +301,13 @@ export function createComponent(tag: string, props?: Props): Element {
301
301
  }
302
302
 
303
303
  async function onMount(parent: Element, element: Element, index?: number) {
304
- element.props.context = parent.props.context;
305
- element.parent = parent;
304
+ let actualParent = parent;
305
+ while (actualParent?.tag === 'fragment') {
306
+ actualParent = actualParent.parent;
307
+ }
308
+
309
+ element.props.context = actualParent.props.context;
310
+ element.parent = actualParent;
306
311
 
307
312
  // Check dependencies before mounting
308
313
  if (element.props.dependencies && Array.isArray(element.props.dependencies)) {
@@ -310,12 +315,12 @@ export function createComponent(tag: string, props?: Props): Element {
310
315
  const ready = await checkDependencies(deps);
311
316
  if (!ready) {
312
317
  // Set up subscriptions for reactive signals to trigger mount later
313
- setupDependencySubscriptions(parent, element, deps, index);
318
+ setupDependencySubscriptions(actualParent, element, deps, index);
314
319
  return;
315
320
  }
316
321
  }
317
322
 
318
- performMount(parent, element, index);
323
+ performMount(actualParent, element, index);
319
324
  };
320
325
 
321
326
  async function propagateContext(element) {
@@ -494,6 +499,25 @@ export function loop<T>(
494
499
  let elementMap = new Map<string | number, Element>();
495
500
  let isFirstSubscription = true;
496
501
 
502
+ const ensureElement = (itemResult: any): Element | null => {
503
+ if (!itemResult) return null;
504
+ if (isElement(itemResult)) return itemResult;
505
+ return {
506
+ tag: 'fragment',
507
+ props: { children: Array.isArray(itemResult) ? itemResult : [itemResult] },
508
+ componentInstance: {} as any,
509
+ propSubscriptions: [],
510
+ effectSubscriptions: [],
511
+ effectMounts: [],
512
+ effectUnmounts: [],
513
+ propObservables: {},
514
+ parent: null,
515
+ directives: {},
516
+ destroy() { destroyElement(this) },
517
+ allElements: new Subject()
518
+ };
519
+ }
520
+
497
521
  const isArraySignal = (signal: any): signal is WritableArraySignal<T[]> =>
498
522
  Array.isArray(signal());
499
523
 
@@ -509,7 +533,7 @@ export function loop<T>(
509
533
  const items = itemsSubject();
510
534
  if (items) {
511
535
  items.forEach((item, index) => {
512
- const element = createElementFn(item, index);
536
+ const element = ensureElement(createElementFn(item, index));
513
537
  if (element) {
514
538
  elements.push(element);
515
539
  elementMap.set(index, element);
@@ -534,7 +558,7 @@ export function loop<T>(
534
558
  const items = itemsSubject();
535
559
  if (items) {
536
560
  items.forEach((item, index) => {
537
- const element = createElementFn(item, index);
561
+ const element = ensureElement(createElementFn(item, index));
538
562
  if (element) {
539
563
  elements.push(element);
540
564
  elementMap.set(index, element);
@@ -543,7 +567,7 @@ export function loop<T>(
543
567
  }
544
568
  } else if (change.type === 'add' && change.index !== undefined) {
545
569
  const newElements = change.items.map((item, i) => {
546
- const element = createElementFn(item as T, change.index! + i);
570
+ const element = ensureElement(createElementFn(item as T, change.index! + i));
547
571
  if (element) {
548
572
  elementMap.set(change.index! + i, element);
549
573
  }
@@ -564,7 +588,7 @@ export function loop<T>(
564
588
  // Check if the previous item at this index was effectively undefined or non-existent
565
589
  if (index >= elements.length || elements[index] === undefined || !elementMap.has(index)) {
566
590
  // Treat as add operation
567
- const newElement = createElementFn(newItem as T, index);
591
+ const newElement = ensureElement(createElementFn(newItem as T, index));
568
592
  if (newElement) {
569
593
  elements.splice(index, 0, newElement); // Insert at the correct index
570
594
  elementMap.set(index, newElement);
@@ -577,7 +601,7 @@ export function loop<T>(
577
601
  // Treat as a standard update operation
578
602
  const oldElement = elements[index];
579
603
  destroyElement(oldElement)
580
- const newElement = createElementFn(newItem as T, index);
604
+ const newElement = ensureElement(createElementFn(newItem as T, index));
581
605
  if (newElement) {
582
606
  elements[index] = newElement;
583
607
  elementMap.set(index, newElement);
@@ -604,7 +628,7 @@ export function loop<T>(
604
628
  const items = (itemsSubject as WritableObjectSignal<T>)();
605
629
  if (items) {
606
630
  Object.entries(items).forEach(([key, value]) => {
607
- const element = createElementFn(value, key);
631
+ const element = ensureElement(createElementFn(value, key));
608
632
  if (element) {
609
633
  elements.push(element);
610
634
  elementMap.set(key, element);
@@ -625,7 +649,7 @@ export function loop<T>(
625
649
  const items = (itemsSubject as WritableObjectSignal<T>)();
626
650
  if (items) {
627
651
  Object.entries(items).forEach(([key, value]) => {
628
- const element = createElementFn(value, key);
652
+ const element = ensureElement(createElementFn(value, key));
629
653
  if (element) {
630
654
  elements.push(element);
631
655
  elementMap.set(key, element);
@@ -633,7 +657,7 @@ export function loop<T>(
633
657
  });
634
658
  }
635
659
  } else if (change.type === 'add' && change.key && change.value !== undefined) {
636
- const element = createElementFn(change.value as T, key);
660
+ const element = ensureElement(createElementFn(change.value as T, key));
637
661
  if (element) {
638
662
  elements.push(element);
639
663
  elementMap.set(key, element);
@@ -650,7 +674,7 @@ export function loop<T>(
650
674
  if (index !== -1) {
651
675
  const oldElement = elements[index];
652
676
  destroyElement(oldElement)
653
- const newElement = createElementFn(change.value as T, key);
677
+ const newElement = ensureElement(createElementFn(change.value as T, key));
654
678
  if (newElement) {
655
679
  elements[index] = newElement;
656
680
  elementMap.set(key, newElement);
@@ -663,7 +687,10 @@ export function loop<T>(
663
687
  });
664
688
  });
665
689
 
666
- return subscription;
690
+ return () => {
691
+ subscription.unsubscribe();
692
+ elements.forEach(el => destroyElement(el));
693
+ };
667
694
  });
668
695
  });
669
696
  }