canvasengine 2.0.0-beta.45 → 2.0.0-beta.46
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/components/Container.d.ts +86 -0
- package/dist/components/Container.d.ts.map +1 -0
- package/dist/components/DOMContainer.d.ts +98 -0
- package/dist/components/DOMContainer.d.ts.map +1 -0
- package/dist/components/DOMElement.d.ts +16 -5
- package/dist/components/DOMElement.d.ts.map +1 -1
- package/dist/components/DOMSprite.d.ts +108 -0
- package/dist/components/DOMSprite.d.ts.map +1 -0
- package/dist/components/DisplayObject.d.ts +94 -0
- package/dist/components/DisplayObject.d.ts.map +1 -0
- package/dist/components/FocusContainer.d.ts +129 -0
- package/dist/components/FocusContainer.d.ts.map +1 -0
- package/dist/components/Mesh.d.ts +208 -0
- package/dist/components/Mesh.d.ts.map +1 -0
- package/dist/components/Sprite.d.ts +242 -0
- package/dist/components/Sprite.d.ts.map +1 -0
- package/dist/components/Viewport.d.ts +121 -0
- package/dist/components/Viewport.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/directives/Controls.d.ts +4 -4
- package/dist/directives/Controls.d.ts.map +1 -1
- package/dist/directives/ControlsBase.d.ts +1 -0
- package/dist/directives/ControlsBase.d.ts.map +1 -1
- package/dist/directives/FocusNavigation.d.ts +4 -22
- package/dist/directives/FocusNavigation.d.ts.map +1 -1
- package/dist/directives/KeyboardControls.d.ts +1 -0
- package/dist/directives/KeyboardControls.d.ts.map +1 -1
- package/dist/directives/Scheduler.d.ts.map +1 -1
- package/dist/directives/Shake.d.ts +1 -0
- package/dist/directives/Shake.d.ts.map +1 -1
- package/dist/engine/FocusManager.d.ts +10 -9
- package/dist/engine/FocusManager.d.ts.map +1 -1
- package/dist/engine/bootstrap.d.ts +1 -0
- package/dist/engine/bootstrap.d.ts.map +1 -1
- package/dist/engine/directive.d.ts +1 -1
- package/dist/engine/directive.d.ts.map +1 -1
- package/dist/engine/reactive.d.ts.map +1 -1
- package/dist/hooks/useFocus.d.ts.map +1 -1
- package/dist/index-DaGekQUW.js +2218 -0
- package/dist/index-DaGekQUW.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.js +3 -3
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +11868 -88
- package/dist/index.js.map +1 -1
- package/dist/utils/tabindex.d.ts +16 -0
- package/dist/utils/tabindex.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/DOMContainer.ts +186 -1
- package/src/components/DOMElement.ts +164 -37
- package/src/components/DOMSprite.ts +759 -0
- package/src/components/DisplayObject.ts +33 -7
- package/src/components/FocusContainer.ts +22 -26
- package/src/components/Sprite.ts +12 -3
- package/src/components/Text.ts +1 -1
- package/src/components/Viewport.ts +5 -5
- package/src/components/index.ts +2 -1
- package/src/directives/Controls.ts +5 -5
- package/src/directives/ControlsBase.ts +1 -0
- package/src/directives/FocusNavigation.ts +8 -146
- package/src/directives/KeyboardControls.ts +11 -2
- package/src/directives/Scheduler.ts +12 -4
- package/src/directives/Shake.ts +9 -6
- package/src/engine/FocusManager.ts +44 -29
- package/src/engine/bootstrap.ts +5 -2
- package/src/engine/directive.ts +2 -2
- package/src/engine/reactive.ts +84 -12
- package/src/hooks/useFocus.ts +2 -5
- package/src/index.ts +2 -1
- package/src/types/pixi-cull.d.ts +7 -0
- package/src/utils/tabindex.ts +70 -0
- package/testing/index.ts +31 -3
- package/tsconfig.json +3 -2
- package/dist/DebugRenderer-CSxse9YI.js +0 -172
- package/dist/DebugRenderer-CSxse9YI.js.map +0 -1
- package/dist/index-DH2ZMhYm.js +0 -13276
- package/dist/index-DH2ZMhYm.js.map +0 -1
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
TransformOrigin,
|
|
10
10
|
} from "./types/DisplayObject";
|
|
11
11
|
import { signal } from "@signe/reactive";
|
|
12
|
-
import { BlurFilter, ObservablePoint } from "pixi.js";
|
|
12
|
+
import { BlurFilter, ObservablePoint, type Point, type Rectangle } from "pixi.js";
|
|
13
13
|
import * as FILTERS from "pixi-filters";
|
|
14
14
|
import { isPercent } from "../utils/functions";
|
|
15
15
|
import { BehaviorSubject, filter, Subject } from "rxjs";
|
|
@@ -20,9 +20,11 @@ export interface ComponentInstance extends PixiMixins.ContainerOptions {
|
|
|
20
20
|
onInit?(props: Props): void;
|
|
21
21
|
onUpdate?(props: Props): void;
|
|
22
22
|
onDestroy?(parent: Element, afterDestroy: () => void): void;
|
|
23
|
-
onMount?(context: Element
|
|
23
|
+
onMount?(context: Element<any>, index?: number): void;
|
|
24
24
|
setWidth(width: number): void;
|
|
25
25
|
setHeight(height: number): void;
|
|
26
|
+
getLocalBounds?(): Rectangle;
|
|
27
|
+
getGlobalPosition?(): Point;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export const EVENTS = [
|
|
@@ -121,16 +123,18 @@ export function DisplayObject(extendClass) {
|
|
|
121
123
|
// Store computed layout box dimensions
|
|
122
124
|
#computedLayoutBox: { width?: number; height?: number } | null = null;
|
|
123
125
|
// Store reference to element for freeze checking
|
|
124
|
-
#element: Element<
|
|
126
|
+
#element: Element<any> | null = null;
|
|
125
127
|
|
|
126
128
|
/**
|
|
127
129
|
* Get the element reference for freeze checking
|
|
128
130
|
* @returns The element reference or null
|
|
129
131
|
*/
|
|
130
|
-
|
|
132
|
+
getElement(): Element<any> | null {
|
|
131
133
|
return this.#element;
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
onLayoutComputed(_event: any) {}
|
|
137
|
+
|
|
134
138
|
get deltaRatio() {
|
|
135
139
|
return this.#canvasContext?.scheduler?.tick.value.deltaRatio;
|
|
136
140
|
}
|
|
@@ -141,6 +145,10 @@ export function DisplayObject(extendClass) {
|
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
onInit(props: Props) {
|
|
148
|
+
// Ensure layout setter from @pixi/layout is used when available.
|
|
149
|
+
if (Object.prototype.hasOwnProperty.call(this, "layout")) {
|
|
150
|
+
delete (this as any).layout;
|
|
151
|
+
}
|
|
144
152
|
this._id = props.id;
|
|
145
153
|
for (let event of EVENTS) {
|
|
146
154
|
if (props[event] && !this.overrideProps.includes(event)) {
|
|
@@ -190,12 +198,29 @@ export function DisplayObject(extendClass) {
|
|
|
190
198
|
this.subjectInit.next(this);
|
|
191
199
|
}
|
|
192
200
|
|
|
193
|
-
async onMount(element: Element<
|
|
201
|
+
async onMount(element: Element<any>, index?: number) {
|
|
194
202
|
if (this.destroyed) return
|
|
195
203
|
this.#element = element;
|
|
196
204
|
this.#canvasContext = element.props.context;
|
|
197
205
|
if (element.parent) {
|
|
198
|
-
|
|
206
|
+
let parentElement = element.parent;
|
|
207
|
+
let instance = parentElement.componentInstance as DisplayObject;
|
|
208
|
+
if (typeof (instance as any)?.addChild !== "function") {
|
|
209
|
+
let search = parentElement.parent;
|
|
210
|
+
while (search && typeof (search.componentInstance as any)?.addChild !== "function") {
|
|
211
|
+
search = search.parent;
|
|
212
|
+
}
|
|
213
|
+
if (search && typeof (search.componentInstance as any)?.addChild === "function") {
|
|
214
|
+
parentElement = search;
|
|
215
|
+
instance = parentElement.componentInstance as DisplayObject;
|
|
216
|
+
} else {
|
|
217
|
+
console.warn("DisplayObject mount skipped: parent has no addChild", {
|
|
218
|
+
child: element.tag,
|
|
219
|
+
parent: element.parent?.tag,
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
199
224
|
if (instance.isFlex && !this.layout && !this.disableLayout) {
|
|
200
225
|
try {
|
|
201
226
|
this.layout = {};
|
|
@@ -203,7 +228,7 @@ export function DisplayObject(extendClass) {
|
|
|
203
228
|
console.warn('Failed to set layout:', error);
|
|
204
229
|
}
|
|
205
230
|
}
|
|
206
|
-
if (index === undefined) {
|
|
231
|
+
if (index === undefined || parentElement !== element.parent || typeof (instance as any)?.addChildAt !== "function") {
|
|
207
232
|
instance.addChild(this);
|
|
208
233
|
} else {
|
|
209
234
|
instance.addChildAt(this, index);
|
|
@@ -219,6 +244,7 @@ export function DisplayObject(extendClass) {
|
|
|
219
244
|
height: event.computedLayout.height,
|
|
220
245
|
};
|
|
221
246
|
}
|
|
247
|
+
this.onLayoutComputed(event);
|
|
222
248
|
};
|
|
223
249
|
this.on('layout', layoutHandler);
|
|
224
250
|
this.#registeredEvents.set('layout', layoutHandler);
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { Container as PixiContainer } from "pixi.js";
|
|
2
1
|
import { createComponent, registerComponent, type Element } from "../engine/reactive";
|
|
3
2
|
import { applyDirective } from "../engine/directive";
|
|
4
|
-
import {
|
|
5
|
-
import { ComponentFunction, h } from "../engine/signal";
|
|
3
|
+
import { ComponentFunction } from "../engine/signal";
|
|
6
4
|
import { DisplayObjectProps } from "./types/DisplayObject";
|
|
7
|
-
import { Container } from "./Container";
|
|
8
5
|
import { focusManager, ScrollOptions } from "../engine/FocusManager";
|
|
9
|
-
import { signal, Signal, isSignal } from "@signe/reactive";
|
|
6
|
+
import { signal, Signal, WritableSignal, WritableObjectSignal, isSignal } from "@signe/reactive";
|
|
10
7
|
import { CanvasViewport } from "./Viewport";
|
|
11
8
|
import { Controls } from "../directives/ControlsBase";
|
|
12
9
|
// Import FocusNavigation directive to ensure it's registered
|
|
@@ -27,7 +24,9 @@ export interface FocusContainerProps extends DisplayObjectProps {
|
|
|
27
24
|
onFocusChange?: (index: number, element: Element | null) => void;
|
|
28
25
|
autoScroll?: boolean | ScrollOptions;
|
|
29
26
|
viewport?: CanvasViewport;
|
|
30
|
-
|
|
27
|
+
context?: {
|
|
28
|
+
viewport?: CanvasViewport;
|
|
29
|
+
};
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
/**
|
|
@@ -67,10 +66,10 @@ export interface FocusContainerProps extends DisplayObjectProps {
|
|
|
67
66
|
* </Viewport>
|
|
68
67
|
* ```
|
|
69
68
|
*/
|
|
70
|
-
export class CanvasFocusContainer
|
|
69
|
+
export class CanvasFocusContainer {
|
|
71
70
|
private containerId: string = '';
|
|
72
|
-
private currentIndexSignal:
|
|
73
|
-
private focusedElementSignal:
|
|
71
|
+
private currentIndexSignal: WritableSignal<number | null> | null = null;
|
|
72
|
+
private focusedElementSignal: WritableSignal<Element | null> | WritableObjectSignal<Element | null> | null = null;
|
|
74
73
|
private registeredFocusables: Set<number> = new Set();
|
|
75
74
|
|
|
76
75
|
/**
|
|
@@ -79,14 +78,12 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
|
|
|
79
78
|
* @param props - Component properties
|
|
80
79
|
*/
|
|
81
80
|
onInit(props: FocusContainerProps) {
|
|
82
|
-
super.onInit(props);
|
|
83
|
-
|
|
84
81
|
// Generate unique container ID
|
|
85
82
|
this.containerId = `focus-container-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
86
83
|
|
|
87
84
|
// Create signals for current index and focused element
|
|
88
85
|
const currentIndex = signal<number | null>(null);
|
|
89
|
-
const focusedElement = signal<Element | null>(null)
|
|
86
|
+
const focusedElement = signal<Element | null>(null) as WritableSignal<Element | null> | WritableObjectSignal<Element | null>;
|
|
90
87
|
|
|
91
88
|
this.currentIndexSignal = currentIndex;
|
|
92
89
|
this.focusedElementSignal = focusedElement;
|
|
@@ -101,8 +98,7 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
|
|
|
101
98
|
focusedElement,
|
|
102
99
|
onFocusChange: props.onFocusChange,
|
|
103
100
|
autoScroll: props.autoScroll,
|
|
104
|
-
viewport
|
|
105
|
-
throttle: props.throttle ?? 150
|
|
101
|
+
viewport
|
|
106
102
|
});
|
|
107
103
|
}
|
|
108
104
|
|
|
@@ -112,8 +108,6 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
|
|
|
112
108
|
* @param element - The element being mounted
|
|
113
109
|
*/
|
|
114
110
|
async onMount(element: Element<CanvasFocusContainer>): Promise<void> {
|
|
115
|
-
await super.onMount(element, undefined);
|
|
116
|
-
|
|
117
111
|
// Update container with element reference for freeze checking
|
|
118
112
|
focusManager.updateContainer(this.containerId, { element });
|
|
119
113
|
|
|
@@ -154,7 +148,7 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
|
|
|
154
148
|
// element.effectSubscriptions.push(subscription);
|
|
155
149
|
// }
|
|
156
150
|
|
|
157
|
-
focusManager.setTabindex(this.containerId, element.propObservables
|
|
151
|
+
focusManager.setTabindex(this.containerId, element.propObservables?.tabindex as any);
|
|
158
152
|
|
|
159
153
|
// Register all focusable children initially
|
|
160
154
|
// Use setTimeout to ensure children are mounted
|
|
@@ -169,15 +163,12 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
|
|
|
169
163
|
* @param props - Updated properties
|
|
170
164
|
*/
|
|
171
165
|
onUpdate(props: FocusContainerProps) {
|
|
172
|
-
super.onUpdate(props);
|
|
173
|
-
|
|
174
166
|
// Update viewport if changed
|
|
175
167
|
const viewport = props.viewport || (props.context?.viewport as CanvasViewport | undefined);
|
|
176
168
|
focusManager.updateContainer(this.containerId, {
|
|
177
169
|
viewport,
|
|
178
170
|
autoScroll: props.autoScroll,
|
|
179
|
-
onFocusChange: props.onFocusChange
|
|
180
|
-
throttle: props.throttle ?? 150
|
|
171
|
+
onFocusChange: props.onFocusChange
|
|
181
172
|
});
|
|
182
173
|
}
|
|
183
174
|
|
|
@@ -196,8 +187,9 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
|
|
|
196
187
|
|
|
197
188
|
// Unregister container
|
|
198
189
|
focusManager.unregisterContainer(this.containerId);
|
|
199
|
-
|
|
200
|
-
|
|
190
|
+
if (afterDestroy) {
|
|
191
|
+
afterDestroy();
|
|
192
|
+
}
|
|
201
193
|
}
|
|
202
194
|
|
|
203
195
|
/**
|
|
@@ -248,6 +240,9 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
|
|
|
248
240
|
|
|
249
241
|
const processChild = (child: Element) => {
|
|
250
242
|
if (!child || !child.componentInstance) return;
|
|
243
|
+
if ((child.tag === "Navigation" || child.tag === "FocusContainer") && child !== (element as any)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
251
246
|
|
|
252
247
|
// Check for tabindex in props
|
|
253
248
|
let tabindex: number | undefined = undefined;
|
|
@@ -272,7 +267,7 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
|
|
|
272
267
|
}
|
|
273
268
|
}
|
|
274
269
|
|
|
275
|
-
// Recursively process children
|
|
270
|
+
// Recursively process children unless we hit another FocusContainer
|
|
276
271
|
if (child.props && child.props.children) {
|
|
277
272
|
if (Array.isArray(child.props.children)) {
|
|
278
273
|
processChildren(child.props.children);
|
|
@@ -358,7 +353,7 @@ export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
|
|
|
358
353
|
|
|
359
354
|
export interface CanvasFocusContainer extends DisplayObjectProps { }
|
|
360
355
|
|
|
361
|
-
registerComponent("
|
|
356
|
+
registerComponent("Navigation", CanvasFocusContainer);
|
|
362
357
|
|
|
363
358
|
/**
|
|
364
359
|
* FocusContainer component function
|
|
@@ -367,6 +362,7 @@ registerComponent("FocusContainer", CanvasFocusContainer);
|
|
|
367
362
|
* @returns FocusContainer element
|
|
368
363
|
*/
|
|
369
364
|
export const FocusContainer: ComponentFunction<FocusContainerProps> = (props) => {
|
|
370
|
-
return createComponent("
|
|
365
|
+
return createComponent("Navigation", props);
|
|
371
366
|
};
|
|
372
367
|
|
|
368
|
+
export const Navigation = FocusContainer;
|
package/src/components/Sprite.ts
CHANGED
|
@@ -299,7 +299,7 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
async onMount(params: Element<
|
|
302
|
+
async onMount(params: Element<any>) {
|
|
303
303
|
// Set #element manually for freeze checking before calling super.onMount
|
|
304
304
|
// We need to set it early so update() can check freeze state
|
|
305
305
|
(this as any)['#element'] = params;
|
|
@@ -323,6 +323,13 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
|
|
|
323
323
|
this.spritesheet = resolvedDefinition.value ?? resolvedDefinition;
|
|
324
324
|
await this.createAnimations();
|
|
325
325
|
}
|
|
326
|
+
if (sheet?.params) {
|
|
327
|
+
this.sheetParams = sheet.params;
|
|
328
|
+
}
|
|
329
|
+
if (sheet?.playing && this.has(sheet.playing)) {
|
|
330
|
+
this.sheetCurrentAnimation = sheet.playing;
|
|
331
|
+
this.play(this.sheetCurrentAnimation, [this.sheetParams]);
|
|
332
|
+
}
|
|
326
333
|
if (sheet.params) {
|
|
327
334
|
for (let key in propObservables?.sheet["params"]) {
|
|
328
335
|
const value = propObservables?.sheet["params"][key] as Signal;
|
|
@@ -330,11 +337,13 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
|
|
|
330
337
|
this.subscriptionSheet.push(
|
|
331
338
|
value.observable.subscribe((value) => {
|
|
332
339
|
if (this.animations.size == 0) return;
|
|
333
|
-
this.
|
|
340
|
+
if (!this.has(this.sheetCurrentAnimation)) return;
|
|
341
|
+
this.play(this.sheetCurrentAnimation, [{ ...this.sheetParams, [key]: value }]);
|
|
334
342
|
})
|
|
335
343
|
);
|
|
336
344
|
} else {
|
|
337
|
-
this.
|
|
345
|
+
if (!this.has(this.sheetCurrentAnimation)) continue;
|
|
346
|
+
this.play(this.sheetCurrentAnimation, [{ ...this.sheetParams, [key]: value }]);
|
|
338
347
|
}
|
|
339
348
|
}
|
|
340
349
|
}
|
package/src/components/Text.ts
CHANGED
|
@@ -48,7 +48,7 @@ class CanvasText extends DisplayObject(PixiText) {
|
|
|
48
48
|
* @param {Element<CanvasText>} element - The element being mounted with parent and props.
|
|
49
49
|
* @param {number} [index] - The index of the component among its siblings.
|
|
50
50
|
*/
|
|
51
|
-
async onMount(element: Element<
|
|
51
|
+
async onMount(element: Element<any>, index?: number): Promise<void> {
|
|
52
52
|
const { props } = element;
|
|
53
53
|
await super.onMount(element, index);
|
|
54
54
|
const tick: Signal = props.context.tick;
|
|
@@ -3,7 +3,7 @@ import { Subscription } from 'rxjs';
|
|
|
3
3
|
import { createComponent, registerComponent, Element, Props } from '../engine/reactive';
|
|
4
4
|
import { DisplayObject, ComponentInstance } from './DisplayObject';
|
|
5
5
|
import { effect, Signal } from '@signe/reactive';
|
|
6
|
-
import { Graphics, Container } from 'pixi.js';
|
|
6
|
+
import { Graphics, Container, ContainerChild, IRenderLayer } from 'pixi.js';
|
|
7
7
|
|
|
8
8
|
const EVENTS = [
|
|
9
9
|
'bounce-x-end',
|
|
@@ -73,8 +73,8 @@ export class CanvasViewport extends DisplayObject(Container) {
|
|
|
73
73
|
return this.viewport.addChild(...children)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
addChildAt<
|
|
77
|
-
return this.viewport.addChildAt(child, index)
|
|
76
|
+
addChildAt<T extends ContainerChild | IRenderLayer>(child: T, index: number): T {
|
|
77
|
+
return this.viewport.addChildAt(child, index) as T
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
onInit(props) {
|
|
@@ -90,7 +90,7 @@ export class CanvasViewport extends DisplayObject(Container) {
|
|
|
90
90
|
* @param {Element<CanvasViewport>} element - The element being mounted. Its `props` property (of type ViewportProps) contains component properties and context.
|
|
91
91
|
* @param {number} [index] - The index of the component among its siblings.
|
|
92
92
|
*/
|
|
93
|
-
async onMount(element: Element<
|
|
93
|
+
async onMount(element: Element<any>, index?: number): Promise<void> {
|
|
94
94
|
element.props.context.viewport = this.viewport
|
|
95
95
|
await super.onMount(element, index);
|
|
96
96
|
const { props } = element;
|
|
@@ -211,4 +211,4 @@ registerComponent('Viewport', CanvasViewport)
|
|
|
211
211
|
|
|
212
212
|
export function Viewport(props: ViewportProps) {
|
|
213
213
|
return createComponent('Viewport', props);
|
|
214
|
-
}
|
|
214
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { NineSliceSprite } from './NineSliceSprite'
|
|
|
13
13
|
export { type ComponentInstance } from './DisplayObject'
|
|
14
14
|
export { DOMContainer } from './DOMContainer'
|
|
15
15
|
export { DOMElement } from './DOMElement'
|
|
16
|
+
export { DOMSprite } from './DOMSprite'
|
|
16
17
|
export { Button, ButtonState, type ButtonProps, type ButtonStyle } from './Button'
|
|
17
18
|
export { Joystick, type JoystickSettings } from './Joystick'
|
|
18
|
-
export { FocusContainer, type FocusContainerProps } from './FocusContainer'
|
|
19
|
+
export { FocusContainer, Navigation, type FocusContainerProps } from './FocusContainer'
|
|
@@ -36,7 +36,7 @@ export class ControlsDirective extends Directive {
|
|
|
36
36
|
* Initialize the controls directive
|
|
37
37
|
* Sets up keyboard, gamepad, and joystick controls if available
|
|
38
38
|
*/
|
|
39
|
-
onInit(element: Element) {
|
|
39
|
+
onInit(element: Element<any>) {
|
|
40
40
|
this.element = element;
|
|
41
41
|
const value = element.props.controls?.value ?? element.props.controls;
|
|
42
42
|
if (!value) return;
|
|
@@ -71,7 +71,7 @@ export class ControlsDirective extends Directive {
|
|
|
71
71
|
// Subscribe to freeze prop if it's a signal
|
|
72
72
|
const freezeProp = element.propObservables?.freeze ?? element.props?.freeze;
|
|
73
73
|
if (isSignal(freezeProp)) {
|
|
74
|
-
this.freezeSubscription = (freezeProp as Signal<boolean>).observable.subscribe((isFrozen) => {
|
|
74
|
+
this.freezeSubscription = ((freezeProp as Signal<boolean>).observable as any).subscribe((isFrozen) => {
|
|
75
75
|
if (isFrozen) {
|
|
76
76
|
this.stopInputs();
|
|
77
77
|
} else {
|
|
@@ -84,13 +84,13 @@ export class ControlsDirective extends Directive {
|
|
|
84
84
|
/**
|
|
85
85
|
* Mount hook (no specific action needed)
|
|
86
86
|
*/
|
|
87
|
-
onMount(element: Element) { }
|
|
87
|
+
onMount(element: Element<any>) { }
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* Update controls configuration
|
|
91
91
|
* Updates both keyboard and gamepad controls
|
|
92
92
|
*/
|
|
93
|
-
onUpdate(props: any, element: Element) {
|
|
93
|
+
onUpdate(props: any, element: Element<any>) {
|
|
94
94
|
const value = props.controls?.value ?? props.controls;
|
|
95
95
|
if (value) {
|
|
96
96
|
if (this.keyboardControls) {
|
|
@@ -115,7 +115,7 @@ export class ControlsDirective extends Directive {
|
|
|
115
115
|
/**
|
|
116
116
|
* Cleanup and destroy all control systems
|
|
117
117
|
*/
|
|
118
|
-
onDestroy(element: Element) {
|
|
118
|
+
onDestroy(element: Element<any>) {
|
|
119
119
|
if (this.freezeSubscription) {
|
|
120
120
|
this.freezeSubscription.unsubscribe();
|
|
121
121
|
this.freezeSubscription = null;
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { Directive, registerDirective, applyDirective } from "../engine/directive";
|
|
2
2
|
import { type Element } from "../engine/reactive";
|
|
3
|
-
import { focusManager } from "../engine/FocusManager";
|
|
4
3
|
import { ControlsDirective } from "./Controls";
|
|
5
4
|
import { Controls } from "./ControlsBase";
|
|
6
5
|
import { isSignal, Signal } from "@signe/reactive";
|
|
7
6
|
import { CanvasFocusContainer } from "../components/FocusContainer";
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
|
-
* FocusNavigation directive for
|
|
9
|
+
* FocusNavigation directive for wiring Controls with FocusContainer
|
|
11
10
|
*
|
|
12
|
-
* This directive integrates with the Controls system
|
|
13
|
-
*
|
|
11
|
+
* This directive integrates with the Controls system and lets external
|
|
12
|
+
* control handlers update the FocusContainer tabindex signal.
|
|
14
13
|
*
|
|
15
14
|
* The directive is automatically applied when a FocusContainer has a `controls` prop.
|
|
16
|
-
* It
|
|
15
|
+
* It keeps the Controls directive in sync with the provided controls config.
|
|
17
16
|
*
|
|
18
17
|
* @example
|
|
19
18
|
* ```typescript
|
|
@@ -27,9 +26,7 @@ import { CanvasFocusContainer } from "../components/FocusContainer";
|
|
|
27
26
|
export class FocusNavigationDirective extends Directive {
|
|
28
27
|
private element: Element<CanvasFocusContainer> | null = null;
|
|
29
28
|
private controlsDirective: ControlsDirective | null = null;
|
|
30
|
-
private containerId: string = '';
|
|
31
29
|
private controlsSubscription: any = null;
|
|
32
|
-
private originalControls: Controls | null = null;
|
|
33
30
|
|
|
34
31
|
/**
|
|
35
32
|
* Initialize the focus navigation directive
|
|
@@ -38,130 +35,6 @@ export class FocusNavigationDirective extends Directive {
|
|
|
38
35
|
*/
|
|
39
36
|
onInit(element: Element<CanvasFocusContainer>) {
|
|
40
37
|
this.element = element;
|
|
41
|
-
|
|
42
|
-
// Get container ID from component instance
|
|
43
|
-
const instance = element.componentInstance as CanvasFocusContainer;
|
|
44
|
-
if (instance && typeof instance.getContainerId === 'function') {
|
|
45
|
-
this.containerId = instance.getContainerId();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Get controls from props
|
|
49
|
-
const controlsProp = element.props.controls;
|
|
50
|
-
if (!controlsProp) return;
|
|
51
|
-
|
|
52
|
-
// Get controls value (handle signals)
|
|
53
|
-
const controlsValue = isSignal(controlsProp) ? controlsProp() : controlsProp;
|
|
54
|
-
if (!controlsValue) return;
|
|
55
|
-
|
|
56
|
-
this.originalControls = controlsValue;
|
|
57
|
-
|
|
58
|
-
// Note: Controls directive will be created/initialized in onMount
|
|
59
|
-
// We'll set up navigation controls there
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Set up navigation controls
|
|
64
|
-
*
|
|
65
|
-
* @param controls - Controls configuration
|
|
66
|
-
*/
|
|
67
|
-
private setupNavigationControls(controls: Controls) {
|
|
68
|
-
if (!this.controlsDirective) {
|
|
69
|
-
console.warn('FocusNavigation: Controls directive not found, cannot set up navigation');
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
controls = (controls.value ?? controls) as Controls;
|
|
73
|
-
// Create navigation controls by wrapping existing ones
|
|
74
|
-
const navigationControls: Controls = {
|
|
75
|
-
...controls,
|
|
76
|
-
// Override or add navigation controls
|
|
77
|
-
up: {
|
|
78
|
-
...controls.up,
|
|
79
|
-
repeat: controls.up?.repeat ?? true,
|
|
80
|
-
bind: controls.up?.bind ?? ['up', 'top_left', 'top_right'],
|
|
81
|
-
keyDown: (boundKey?: any) => {
|
|
82
|
-
// Navigate up/previous first
|
|
83
|
-
this.navigate('previous');
|
|
84
|
-
// Call original handler if exists
|
|
85
|
-
controls.up?.keyDown?.(boundKey);
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
down: {
|
|
89
|
-
...controls.down,
|
|
90
|
-
repeat: controls.down?.repeat ?? true,
|
|
91
|
-
bind: controls.down?.bind ?? ['down', 'bottom_left', 'bottom_right'],
|
|
92
|
-
keyDown: (boundKey?: any) => {
|
|
93
|
-
// Navigate down/next first
|
|
94
|
-
this.navigate('next');
|
|
95
|
-
// Call original handler if exists
|
|
96
|
-
controls.down?.keyDown?.(boundKey);
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
left: {
|
|
100
|
-
...controls.left,
|
|
101
|
-
repeat: controls.left?.repeat ?? true,
|
|
102
|
-
bind: controls.left?.bind ?? 'left',
|
|
103
|
-
keyDown: (boundKey?: any) => {
|
|
104
|
-
// Navigate previous (for horizontal lists)
|
|
105
|
-
this.navigate('previous');
|
|
106
|
-
// Call original handler if exists
|
|
107
|
-
controls.left?.keyDown?.(boundKey);
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
right: {
|
|
111
|
-
...controls.right,
|
|
112
|
-
repeat: controls.right?.repeat ?? true,
|
|
113
|
-
bind: controls.right?.bind ?? 'right',
|
|
114
|
-
keyDown: (boundKey?: any) => {
|
|
115
|
-
// Navigate next (for horizontal lists)
|
|
116
|
-
this.navigate('next');
|
|
117
|
-
// Call original handler if exists
|
|
118
|
-
controls.right?.keyDown?.(boundKey);
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
action: {
|
|
122
|
-
...controls.action,
|
|
123
|
-
bind: controls.action?.bind ?? ['space', 'enter'],
|
|
124
|
-
keyDown: (boundKey?: any) => {
|
|
125
|
-
// Trigger action on focused element (e.g., click)
|
|
126
|
-
this.triggerAction();
|
|
127
|
-
// Call original handler if exists
|
|
128
|
-
controls.action?.keyDown?.(boundKey);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
// Update controls directive with navigation controls
|
|
134
|
-
this.controlsDirective.onUpdate({ controls: navigationControls }, this.element!);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Navigate to next or previous focusable element
|
|
139
|
-
*
|
|
140
|
-
* @param direction - Navigation direction
|
|
141
|
-
*/
|
|
142
|
-
private navigate(direction: 'next' | 'previous') {
|
|
143
|
-
if (!this.containerId) return;
|
|
144
|
-
focusManager.navigate(this.containerId, direction);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Trigger action on currently focused element
|
|
149
|
-
*/
|
|
150
|
-
private triggerAction() {
|
|
151
|
-
if (!this.containerId) return;
|
|
152
|
-
|
|
153
|
-
const focusedElementSignal = focusManager.getFocusedElementSignal(this.containerId);
|
|
154
|
-
if (!focusedElementSignal) return;
|
|
155
|
-
|
|
156
|
-
const focusedElement = focusedElementSignal();
|
|
157
|
-
if (!focusedElement) return;
|
|
158
|
-
|
|
159
|
-
// Try to trigger click/pointertap event on focused element
|
|
160
|
-
const instance = focusedElement.componentInstance;
|
|
161
|
-
if (instance && typeof instance.emit === 'function') {
|
|
162
|
-
// Emit pointertap event (equivalent to click)
|
|
163
|
-
instance.emit('pointertap', { target: instance });
|
|
164
|
-
}
|
|
165
38
|
}
|
|
166
39
|
|
|
167
40
|
/**
|
|
@@ -170,12 +43,6 @@ export class FocusNavigationDirective extends Directive {
|
|
|
170
43
|
* @param element - FocusContainer element
|
|
171
44
|
*/
|
|
172
45
|
onMount(element: Element<CanvasFocusContainer>) {
|
|
173
|
-
// Get container ID again (should be available now)
|
|
174
|
-
const instance = element.componentInstance as CanvasFocusContainer;
|
|
175
|
-
if (instance && typeof instance.getContainerId === 'function') {
|
|
176
|
-
this.containerId = instance.getContainerId();
|
|
177
|
-
}
|
|
178
|
-
|
|
179
46
|
// Get or create Controls directive
|
|
180
47
|
this.controlsDirective = element.directives?.controls as ControlsDirective;
|
|
181
48
|
|
|
@@ -196,17 +63,15 @@ export class FocusNavigationDirective extends Directive {
|
|
|
196
63
|
if (controlsProp) {
|
|
197
64
|
const controlsValue = isSignal(controlsProp) ? controlsProp() : controlsProp;
|
|
198
65
|
if (controlsValue) {
|
|
199
|
-
this.
|
|
200
|
-
this.setupNavigationControls(controlsValue);
|
|
66
|
+
this.controlsDirective?.onUpdate({ controls: controlsValue }, element);
|
|
201
67
|
}
|
|
202
68
|
}
|
|
203
69
|
|
|
204
70
|
// Handle controls prop updates if it's a signal
|
|
205
71
|
if (isSignal(controlsProp)) {
|
|
206
|
-
this.controlsSubscription = (controlsProp as Signal<Controls>).observable.subscribe((controls) => {
|
|
72
|
+
this.controlsSubscription = ((controlsProp as Signal<Controls>).observable as any).subscribe((controls) => {
|
|
207
73
|
if (controls) {
|
|
208
|
-
this.
|
|
209
|
-
this.setupNavigationControls(controls);
|
|
74
|
+
this.controlsDirective?.onUpdate({ controls }, element);
|
|
210
75
|
}
|
|
211
76
|
});
|
|
212
77
|
}
|
|
@@ -223,8 +88,7 @@ export class FocusNavigationDirective extends Directive {
|
|
|
223
88
|
if (props.controls !== undefined) {
|
|
224
89
|
const controlsValue = isSignal(props.controls) ? props.controls() : props.controls;
|
|
225
90
|
if (controlsValue) {
|
|
226
|
-
this.
|
|
227
|
-
this.setupNavigationControls(controlsValue);
|
|
91
|
+
this.controlsDirective?.onUpdate({ controls: controlsValue }, element);
|
|
228
92
|
}
|
|
229
93
|
}
|
|
230
94
|
}
|
|
@@ -243,9 +107,7 @@ export class FocusNavigationDirective extends Directive {
|
|
|
243
107
|
|
|
244
108
|
this.element = null;
|
|
245
109
|
this.controlsDirective = null;
|
|
246
|
-
this.originalControls = null;
|
|
247
110
|
}
|
|
248
111
|
}
|
|
249
112
|
|
|
250
113
|
registerDirective('focusNavigation', FocusNavigationDirective);
|
|
251
|
-
|
|
@@ -351,6 +351,7 @@ export class KeyboardControls extends ControlsBase {
|
|
|
351
351
|
} | null
|
|
352
352
|
} = {}
|
|
353
353
|
private lastKeyPressed: number | null = null
|
|
354
|
+
private lastActionTimes: Record<string, number> = {}
|
|
354
355
|
private directionState: {
|
|
355
356
|
up: boolean,
|
|
356
357
|
down: boolean,
|
|
@@ -419,8 +420,16 @@ export class KeyboardControls extends ControlsBase {
|
|
|
419
420
|
if (!boundKey) {
|
|
420
421
|
return;
|
|
421
422
|
}
|
|
422
|
-
const { repeat, keyDown } = boundKey.options;
|
|
423
|
+
const { repeat, keyDown, throttle } = boundKey.options;
|
|
423
424
|
if ((repeat || count == 0)) {
|
|
425
|
+
if (typeof throttle === "number") {
|
|
426
|
+
const now = Date.now();
|
|
427
|
+
const lastTime = this.lastActionTimes[boundKey.actionName] ?? 0;
|
|
428
|
+
if (now - lastTime < throttle) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
this.lastActionTimes[boundKey.actionName] = now;
|
|
432
|
+
}
|
|
424
433
|
let parameters = boundKey.parameters;
|
|
425
434
|
if (typeof parameters === "function") {
|
|
426
435
|
parameters = parameters();
|
|
@@ -565,4 +574,4 @@ export class KeyboardControls extends ControlsBase {
|
|
|
565
574
|
this.keyState = {}
|
|
566
575
|
}
|
|
567
576
|
|
|
568
|
-
}
|
|
577
|
+
}
|
|
@@ -30,9 +30,17 @@ export class Scheduler extends Directive {
|
|
|
30
30
|
onUpdate(props: any) { }
|
|
31
31
|
|
|
32
32
|
nextTick(timestamp: number) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const now = (typeof timestamp === "number" && timestamp > 0)
|
|
34
|
+
? timestamp
|
|
35
|
+
: Utils.preciseNow()
|
|
36
|
+
if (this.lastTimestamp === 0) {
|
|
37
|
+
this.lastTimestamp = now
|
|
38
|
+
this.deltaTime = 0
|
|
39
|
+
} else {
|
|
40
|
+
this.deltaTime = now - this.lastTimestamp
|
|
41
|
+
this.lastTimestamp = now
|
|
42
|
+
}
|
|
43
|
+
this.timestamp = now
|
|
36
44
|
this.tick.set({
|
|
37
45
|
timestamp: this.timestamp,
|
|
38
46
|
deltaTime: this.deltaTime,
|
|
@@ -98,4 +106,4 @@ export class Scheduler extends Directive {
|
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
registerDirective('tick', Scheduler)
|
|
109
|
+
registerDirective('tick', Scheduler)
|