canvasengine 2.0.0-beta.42 → 2.0.0-beta.44
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/{DebugRenderer-BosXI2Pd.js → DebugRenderer-P5sZ-0Tq.js} +2 -2
- package/dist/{DebugRenderer-BosXI2Pd.js.map → DebugRenderer-P5sZ-0Tq.js.map} +1 -1
- package/dist/components/Button.d.ts +3 -1
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Canvas.d.ts +0 -1
- package/dist/components/DOMElement.d.ts +0 -1
- package/dist/components/DOMElement.d.ts.map +1 -1
- package/dist/components/Graphic.d.ts +1 -2
- package/dist/components/Graphic.d.ts.map +1 -1
- package/dist/components/NineSliceSprite.d.ts +0 -1
- package/dist/components/ParticleEmitter.d.ts +0 -1
- package/dist/components/Text.d.ts +0 -1
- package/dist/components/TilingSprite.d.ts +0 -1
- package/dist/components/Video.d.ts +0 -1
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/types/DisplayObject.d.ts +12 -16
- package/dist/components/types/DisplayObject.d.ts.map +1 -1
- package/dist/components/types/index.d.ts +0 -1
- package/dist/directives/Controls.d.ts +0 -1
- package/dist/directives/Drag.d.ts +0 -1
- package/dist/directives/Flash.d.ts +0 -1
- package/dist/directives/FocusNavigation.d.ts +70 -0
- package/dist/directives/FocusNavigation.d.ts.map +1 -0
- package/dist/directives/GamepadControls.d.ts +0 -1
- package/dist/directives/JoystickControls.d.ts +0 -1
- package/dist/directives/KeyboardControls.d.ts +0 -1
- package/dist/directives/KeyboardControls.d.ts.map +1 -1
- package/dist/directives/Scheduler.d.ts +0 -1
- package/dist/directives/Shake.d.ts +0 -1
- package/dist/directives/Sound.d.ts +0 -1
- package/dist/directives/Transition.d.ts +0 -1
- package/dist/directives/ViewportCull.d.ts +0 -1
- package/dist/directives/ViewportFollow.d.ts +0 -1
- package/dist/directives/ViewportFollow.d.ts.map +1 -1
- package/dist/engine/FocusManager.d.ts +173 -0
- package/dist/engine/FocusManager.d.ts.map +1 -0
- package/dist/engine/animation.d.ts +0 -1
- package/dist/engine/bootstrap.d.ts +0 -1
- package/dist/engine/directive.d.ts +0 -1
- package/dist/engine/reactive.d.ts +0 -1
- package/dist/engine/reactive.d.ts.map +1 -1
- package/dist/engine/signal.d.ts +0 -1
- package/dist/engine/utils.d.ts +0 -1
- package/dist/hooks/useFocus.d.ts +60 -0
- package/dist/hooks/useFocus.d.ts.map +1 -0
- package/dist/hooks/useRef.d.ts +0 -1
- package/dist/{index-DNwqVzaq.js → index-VPoz4ufu.js} +6792 -6117
- package/dist/index-VPoz4ufu.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.js +4 -28
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +67 -61
- package/dist/utils/RadialGradient.d.ts +0 -1
- package/package.json +4 -4
- package/src/components/Button.ts +7 -4
- package/src/components/Canvas.ts +1 -1
- package/src/components/DOMContainer.ts +27 -2
- package/src/components/DOMElement.ts +37 -29
- package/src/components/DisplayObject.ts +15 -3
- package/src/components/FocusContainer.ts +372 -0
- package/src/components/Graphic.ts +43 -48
- package/src/components/Sprite.ts +4 -2
- package/src/components/Viewport.ts +65 -26
- package/src/components/index.ts +2 -1
- package/src/components/types/DisplayObject.ts +7 -4
- package/src/directives/Controls.ts +1 -1
- package/src/directives/ControlsBase.ts +1 -1
- package/src/directives/FocusNavigation.ts +251 -0
- package/src/directives/KeyboardControls.ts +12 -8
- package/src/directives/ViewportFollow.ts +8 -5
- package/src/engine/FocusManager.ts +495 -0
- package/src/engine/reactive.ts +20 -19
- package/src/hooks/useFocus.ts +94 -0
- package/src/index.ts +2 -0
- package/dist/index-DNwqVzaq.js.map +0 -1
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { Container as PixiContainer } from "pixi.js";
|
|
2
|
+
import { createComponent, registerComponent, type Element } from "../engine/reactive";
|
|
3
|
+
import { applyDirective } from "../engine/directive";
|
|
4
|
+
import { ComponentInstance, DisplayObject } from "./DisplayObject";
|
|
5
|
+
import { ComponentFunction, h } from "../engine/signal";
|
|
6
|
+
import { DisplayObjectProps } from "./types/DisplayObject";
|
|
7
|
+
import { Container } from "./Container";
|
|
8
|
+
import { focusManager, ScrollOptions } from "../engine/FocusManager";
|
|
9
|
+
import { signal, Signal, isSignal } from "@signe/reactive";
|
|
10
|
+
import { CanvasViewport } from "./Viewport";
|
|
11
|
+
import { Controls } from "../directives/ControlsBase";
|
|
12
|
+
// Import FocusNavigation directive to ensure it's registered
|
|
13
|
+
import "../directives/FocusNavigation";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Properties for FocusContainer component
|
|
17
|
+
*
|
|
18
|
+
* @property tabindex - Focus index for the container (default: 0 if present)
|
|
19
|
+
* @property controls - Controls configuration for automatic navigation
|
|
20
|
+
* @property onFocusChange - Callback when focus changes
|
|
21
|
+
* @property autoScroll - Enable automatic scrolling to focused element (default: false)
|
|
22
|
+
* @property viewport - Viewport instance to use for scrolling (optional, uses context viewport by default)
|
|
23
|
+
*/
|
|
24
|
+
export interface FocusContainerProps extends DisplayObjectProps {
|
|
25
|
+
tabindex?: number;
|
|
26
|
+
controls?: Controls | Signal<Controls>;
|
|
27
|
+
onFocusChange?: (index: number, element: Element | null) => void;
|
|
28
|
+
autoScroll?: boolean | ScrollOptions;
|
|
29
|
+
viewport?: CanvasViewport;
|
|
30
|
+
throttle?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* FocusContainer component for managing focus navigation
|
|
35
|
+
*
|
|
36
|
+
* This component provides a container that manages focus navigation between
|
|
37
|
+
* focusable child elements. It supports automatic navigation via Controls
|
|
38
|
+
* (keyboard/gamepad) and automatic scrolling with Viewport.
|
|
39
|
+
*
|
|
40
|
+
* ## Features
|
|
41
|
+
*
|
|
42
|
+
* - **Focus Management**: Automatically registers focusable children
|
|
43
|
+
* - **Navigation**: Supports keyboard/gamepad navigation via Controls
|
|
44
|
+
* - **Auto-scroll**: Automatically scrolls viewport to show focused element
|
|
45
|
+
* - **Hooks**: Provides reactive signals for focus state
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* // Basic usage
|
|
50
|
+
* <FocusContainer tabindex={0}>
|
|
51
|
+
* <Button tabindex={0} text="Button 1" />
|
|
52
|
+
* <Button tabindex={1} text="Button 2" />
|
|
53
|
+
* </FocusContainer>
|
|
54
|
+
*
|
|
55
|
+
* // With Controls
|
|
56
|
+
* <FocusContainer tabindex={0} controls={controlsConfig}>
|
|
57
|
+
* <Button tabindex={0} text="Button 1" />
|
|
58
|
+
* <Button tabindex={1} text="Button 2" />
|
|
59
|
+
* </FocusContainer>
|
|
60
|
+
*
|
|
61
|
+
* // With auto-scroll
|
|
62
|
+
* <Viewport worldWidth={2000} worldHeight={5000}>
|
|
63
|
+
* <FocusContainer tabindex={0} autoScroll={true}>
|
|
64
|
+
* <Button tabindex={0} y={0} text="Item 1" />
|
|
65
|
+
* <Button tabindex={1} y={100} text="Item 2" />
|
|
66
|
+
* </FocusContainer>
|
|
67
|
+
* </Viewport>
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export class CanvasFocusContainer extends DisplayObject(PixiContainer) {
|
|
71
|
+
private containerId: string = '';
|
|
72
|
+
private currentIndexSignal: Signal<number | null> | null = null;
|
|
73
|
+
private focusedElementSignal: Signal<Element | null> | null = null;
|
|
74
|
+
private registeredFocusables: Set<number> = new Set();
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Initialize the focus container
|
|
78
|
+
*
|
|
79
|
+
* @param props - Component properties
|
|
80
|
+
*/
|
|
81
|
+
onInit(props: FocusContainerProps) {
|
|
82
|
+
super.onInit(props);
|
|
83
|
+
|
|
84
|
+
// Generate unique container ID
|
|
85
|
+
this.containerId = `focus-container-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
86
|
+
|
|
87
|
+
// Create signals for current index and focused element
|
|
88
|
+
const currentIndex = signal<number | null>(null);
|
|
89
|
+
const focusedElement = signal<Element | null>(null);
|
|
90
|
+
|
|
91
|
+
this.currentIndexSignal = currentIndex;
|
|
92
|
+
this.focusedElementSignal = focusedElement;
|
|
93
|
+
|
|
94
|
+
// Get viewport from context or props
|
|
95
|
+
const viewport = props.viewport || (props.context?.viewport as CanvasViewport | undefined);
|
|
96
|
+
|
|
97
|
+
// Register container with FocusManager
|
|
98
|
+
focusManager.registerContainer(this.containerId, {
|
|
99
|
+
focusables: new Map(),
|
|
100
|
+
currentIndex,
|
|
101
|
+
focusedElement,
|
|
102
|
+
onFocusChange: props.onFocusChange,
|
|
103
|
+
autoScroll: props.autoScroll,
|
|
104
|
+
viewport,
|
|
105
|
+
throttle: props.throttle ?? 150
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Mount hook - register focusable children
|
|
111
|
+
*
|
|
112
|
+
* @param element - The element being mounted
|
|
113
|
+
*/
|
|
114
|
+
async onMount(element: Element<CanvasFocusContainer>): Promise<void> {
|
|
115
|
+
await super.onMount(element, undefined);
|
|
116
|
+
|
|
117
|
+
// Update container with element reference for freeze checking
|
|
118
|
+
focusManager.updateContainer(this.containerId, { element });
|
|
119
|
+
|
|
120
|
+
// Apply focusNavigation directive if controls are provided
|
|
121
|
+
if (element.props.controls) {
|
|
122
|
+
const focusNavDirective = applyDirective(element, 'focusNavigation');
|
|
123
|
+
if (focusNavDirective && !element.directives) {
|
|
124
|
+
element.directives = {};
|
|
125
|
+
}
|
|
126
|
+
if (focusNavDirective) {
|
|
127
|
+
element.directives.focusNavigation = focusNavDirective;
|
|
128
|
+
// Initialize the directive
|
|
129
|
+
focusNavDirective.onInit(element);
|
|
130
|
+
focusNavDirective.onMount(element);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Subscribe to allElements to detect when children are mounted
|
|
135
|
+
if (element.allElements) {
|
|
136
|
+
const subscription = element.allElements.subscribe(() => {
|
|
137
|
+
// Register children when they are mounted
|
|
138
|
+
this.registerChildren(element);
|
|
139
|
+
});
|
|
140
|
+
// Store subscription for cleanup
|
|
141
|
+
if (!element.effectSubscriptions) {
|
|
142
|
+
element.effectSubscriptions = [];
|
|
143
|
+
}
|
|
144
|
+
element.effectSubscriptions.push(subscription);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// if (element.propObservables.tabindex) {
|
|
148
|
+
// const subscription = element.propObservables.tabindex.observable.subscribe((value: any) => {
|
|
149
|
+
// console.log("tabindex changed", value);
|
|
150
|
+
// if (value !== null) {
|
|
151
|
+
// // focusManager.setIndex(this.containerId, value);
|
|
152
|
+
// }
|
|
153
|
+
// });
|
|
154
|
+
// element.effectSubscriptions.push(subscription);
|
|
155
|
+
// }
|
|
156
|
+
|
|
157
|
+
focusManager.setTabindex(this.containerId, element.propObservables.tabindex);
|
|
158
|
+
|
|
159
|
+
// Register all focusable children initially
|
|
160
|
+
// Use setTimeout to ensure children are mounted
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
this.registerChildren(element);
|
|
163
|
+
}, 0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Update hook - handle prop changes
|
|
168
|
+
*
|
|
169
|
+
* @param props - Updated properties
|
|
170
|
+
*/
|
|
171
|
+
onUpdate(props: FocusContainerProps) {
|
|
172
|
+
super.onUpdate(props);
|
|
173
|
+
|
|
174
|
+
// Update viewport if changed
|
|
175
|
+
const viewport = props.viewport || (props.context?.viewport as CanvasViewport | undefined);
|
|
176
|
+
focusManager.updateContainer(this.containerId, {
|
|
177
|
+
viewport,
|
|
178
|
+
autoScroll: props.autoScroll,
|
|
179
|
+
onFocusChange: props.onFocusChange,
|
|
180
|
+
throttle: props.throttle ?? 150
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Destroy hook - cleanup
|
|
186
|
+
*
|
|
187
|
+
* @param parent - Parent element
|
|
188
|
+
* @param afterDestroy - Callback after destruction
|
|
189
|
+
*/
|
|
190
|
+
async onDestroy(parent: Element<any>, afterDestroy?: () => void): Promise<void> {
|
|
191
|
+
// Unregister all focusables
|
|
192
|
+
for (const index of this.registeredFocusables) {
|
|
193
|
+
focusManager.unregisterFocusable(this.containerId, index);
|
|
194
|
+
}
|
|
195
|
+
this.registeredFocusables.clear();
|
|
196
|
+
|
|
197
|
+
// Unregister container
|
|
198
|
+
focusManager.unregisterContainer(this.containerId);
|
|
199
|
+
|
|
200
|
+
await super.onDestroy(parent, afterDestroy);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Register focusable children from element
|
|
205
|
+
*
|
|
206
|
+
* @param element - Container element
|
|
207
|
+
*/
|
|
208
|
+
private registerChildren(element: Element<CanvasFocusContainer>) {
|
|
209
|
+
if (!element.props.children) return;
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
let registeredCount = 0;
|
|
213
|
+
const processChildren = (children: any[]) => {
|
|
214
|
+
for (const child of children) {
|
|
215
|
+
if (!child) continue;
|
|
216
|
+
|
|
217
|
+
// Handle signals/observables
|
|
218
|
+
if (isSignal(child) || (child && typeof child.subscribe === 'function')) {
|
|
219
|
+
|
|
220
|
+
// Subscribe to changes
|
|
221
|
+
const subscription = (isSignal(child) ? child.observable : child).subscribe((value: any) => {
|
|
222
|
+
// Handle FlowObservable result (from loop, cond, etc.) - has 'elements' property
|
|
223
|
+
if (value && typeof value === 'object' && 'elements' in value) {
|
|
224
|
+
const elements = value.elements || [];
|
|
225
|
+
if (Array.isArray(elements)) {
|
|
226
|
+
processChildren(elements);
|
|
227
|
+
}
|
|
228
|
+
} else if (Array.isArray(value)) {
|
|
229
|
+
processChildren(value);
|
|
230
|
+
} else if (value) {
|
|
231
|
+
processChild(value);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
// Note: We should track subscriptions for cleanup, but for now this works
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Handle arrays
|
|
239
|
+
if (Array.isArray(child)) {
|
|
240
|
+
processChildren(child);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Handle single element
|
|
245
|
+
processChild(child);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const processChild = (child: Element) => {
|
|
250
|
+
if (!child || !child.componentInstance) return;
|
|
251
|
+
|
|
252
|
+
// Check for tabindex in props
|
|
253
|
+
let tabindex: number | undefined = undefined;
|
|
254
|
+
|
|
255
|
+
// For DOMElement/DOMContainer, check attrs.tabindex
|
|
256
|
+
if (child.props?.attrs?.tabindex !== undefined) {
|
|
257
|
+
const tabindexValue = child.props.attrs.tabindex;
|
|
258
|
+
tabindex = isSignal(tabindexValue) ? tabindexValue() : tabindexValue;
|
|
259
|
+
}
|
|
260
|
+
// For other components, check tabindex prop directly
|
|
261
|
+
else if (child.props?.tabindex !== undefined) {
|
|
262
|
+
const tabindexValue = child.props.tabindex;
|
|
263
|
+
tabindex = isSignal(tabindexValue) ? tabindexValue() : tabindexValue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Register if tabindex >= 0
|
|
267
|
+
if (tabindex !== undefined && tabindex >= 0) {
|
|
268
|
+
if (!this.registeredFocusables.has(tabindex)) {
|
|
269
|
+
focusManager.registerFocusable(this.containerId, child, tabindex);
|
|
270
|
+
this.registeredFocusables.add(tabindex);
|
|
271
|
+
registeredCount++;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Recursively process children
|
|
276
|
+
if (child.props && child.props.children) {
|
|
277
|
+
if (Array.isArray(child.props.children)) {
|
|
278
|
+
processChildren(child.props.children);
|
|
279
|
+
} else if (isSignal(child.props.children) || (child.props.children && typeof child.props.children.subscribe === 'function')) {
|
|
280
|
+
const subscription = (isSignal(child.props.children) ? child.props.children.observable : child.props.children).subscribe((value: any) => {
|
|
281
|
+
// Handle FlowObservable result (from loop, cond, etc.) - has 'elements' property
|
|
282
|
+
if (value && typeof value === 'object' && 'elements' in value) {
|
|
283
|
+
const elements = value.elements || [];
|
|
284
|
+
if (Array.isArray(elements)) {
|
|
285
|
+
processChildren(elements);
|
|
286
|
+
}
|
|
287
|
+
} else if (Array.isArray(value)) {
|
|
288
|
+
processChildren(value);
|
|
289
|
+
} else if (value) {
|
|
290
|
+
processChild(value);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
// Store subscription for cleanup if child has effectSubscriptions
|
|
294
|
+
if (child.effectSubscriptions) {
|
|
295
|
+
child.effectSubscriptions.push(subscription);
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
processChild(child.props.children as any);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (Array.isArray(element.props.children)) {
|
|
304
|
+
processChildren(element.props.children);
|
|
305
|
+
} else if (element.props.children) {
|
|
306
|
+
if (isSignal(element.props.children) || (element.props.children && typeof element.props.children.subscribe === 'function')) {
|
|
307
|
+
const subscription = (isSignal(element.props.children) ? element.props.children.observable : element.props.children).subscribe((value: any) => {
|
|
308
|
+
// Handle FlowObservable result (from loop, cond, etc.) - has 'elements' property
|
|
309
|
+
if (value && typeof value === 'object' && 'elements' in value) {
|
|
310
|
+
const elements = value.elements || [];
|
|
311
|
+
if (Array.isArray(elements)) {
|
|
312
|
+
processChildren(elements);
|
|
313
|
+
}
|
|
314
|
+
} else if (Array.isArray(value)) {
|
|
315
|
+
processChildren(value);
|
|
316
|
+
} else if (value) {
|
|
317
|
+
processChild(value);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
// Store subscription for cleanup
|
|
321
|
+
if (!element.effectSubscriptions) {
|
|
322
|
+
element.effectSubscriptions = [];
|
|
323
|
+
}
|
|
324
|
+
element.effectSubscriptions.push(subscription);
|
|
325
|
+
} else {
|
|
326
|
+
processChild(element.props.children as any);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Get the container ID
|
|
333
|
+
*
|
|
334
|
+
* @returns Container identifier
|
|
335
|
+
*/
|
|
336
|
+
getContainerId(): string {
|
|
337
|
+
return this.containerId;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get current index signal
|
|
342
|
+
*
|
|
343
|
+
* @returns Signal for current focus index
|
|
344
|
+
*/
|
|
345
|
+
getCurrentIndexSignal(): Signal<number | null> | null {
|
|
346
|
+
return this.currentIndexSignal;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get focused element signal
|
|
351
|
+
*
|
|
352
|
+
* @returns Signal for current focused element
|
|
353
|
+
*/
|
|
354
|
+
getFocusedElementSignal(): Signal<Element | null> | null {
|
|
355
|
+
return this.focusedElementSignal;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export interface CanvasFocusContainer extends DisplayObjectProps { }
|
|
360
|
+
|
|
361
|
+
registerComponent("FocusContainer", CanvasFocusContainer);
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* FocusContainer component function
|
|
365
|
+
*
|
|
366
|
+
* @param props - Component properties
|
|
367
|
+
* @returns FocusContainer element
|
|
368
|
+
*/
|
|
369
|
+
export const FocusContainer: ComponentFunction<FocusContainerProps> = (props) => {
|
|
370
|
+
return createComponent("FocusContainer", props);
|
|
371
|
+
};
|
|
372
|
+
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { Effect, effect, isSignal, signal, Signal, WritableSignal } from "@signe/reactive";
|
|
2
|
-
import { Assets, Graphics as PixiGraphics } from "pixi.js";
|
|
2
|
+
import { Assets, ObservablePoint, Graphics as PixiGraphics } from "pixi.js";
|
|
3
3
|
import { createComponent, Element, registerComponent } from "../engine/reactive";
|
|
4
4
|
import { ComponentInstance, DisplayObject } from "./DisplayObject";
|
|
5
5
|
import { DisplayObjectProps } from "./types/DisplayObject";
|
|
6
6
|
import { useProps } from "../hooks/useProps";
|
|
7
7
|
import { SignalOrPrimitive } from "./types";
|
|
8
8
|
import { isPercent } from "../utils/functions";
|
|
9
|
+
import { setObservablePoint } from "../engine/utils";
|
|
9
10
|
|
|
10
11
|
interface GraphicsProps extends DisplayObjectProps {
|
|
11
|
-
draw?: (graphics: PixiGraphics, width: number, height: number) => void;
|
|
12
|
+
draw?: (graphics: PixiGraphics, width: number, height: number, anchor?: [number, number]) => void;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
interface RectProps extends DisplayObjectProps {
|
|
@@ -42,6 +43,8 @@ class CanvasGraphics extends DisplayObject(PixiGraphics) {
|
|
|
42
43
|
clearEffect: Effect;
|
|
43
44
|
_width: WritableSignal<number>;
|
|
44
45
|
_height: WritableSignal<number>;
|
|
46
|
+
|
|
47
|
+
isCustomAnchor = true;
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
50
|
* Initializes the graphics component with reactive width and height handling.
|
|
@@ -74,6 +77,7 @@ class CanvasGraphics extends DisplayObject(PixiGraphics) {
|
|
|
74
77
|
*/
|
|
75
78
|
async onInit(props) {
|
|
76
79
|
await super.onInit(props);
|
|
80
|
+
this.setObjectFit('none');
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
/**
|
|
@@ -89,6 +93,7 @@ class CanvasGraphics extends DisplayObject(PixiGraphics) {
|
|
|
89
93
|
// Use original signals from propObservables if available, otherwise create new ones
|
|
90
94
|
const width = (isSignal(propObservables?.width) ? propObservables.width : signal(props.width || 0)) as WritableSignal<number>;
|
|
91
95
|
const height = (isSignal(propObservables?.height) ? propObservables.height : signal(props.height || 0)) as WritableSignal<number>;
|
|
96
|
+
const anchor = (isSignal(propObservables?.anchor) ? propObservables.anchor : signal(props.anchor || [0, 0])) as WritableSignal<[number, number]>;
|
|
92
97
|
|
|
93
98
|
// Store as class properties for access in other methods
|
|
94
99
|
this._width = width;
|
|
@@ -102,11 +107,12 @@ class CanvasGraphics extends DisplayObject(PixiGraphics) {
|
|
|
102
107
|
this.clearEffect = effect(() => {
|
|
103
108
|
const w = width();
|
|
104
109
|
const h = height();
|
|
110
|
+
const a = anchor();
|
|
105
111
|
if (typeof w == 'string' || typeof h == 'string') {
|
|
106
112
|
return
|
|
107
113
|
}
|
|
108
114
|
this.clear();
|
|
109
|
-
props.draw?.(this, w, h);
|
|
115
|
+
props.draw?.(this, w, h, a);
|
|
110
116
|
this.subjectInit.next(this)
|
|
111
117
|
});
|
|
112
118
|
}
|
|
@@ -169,6 +175,15 @@ export function Graphics(props: GraphicsProps) {
|
|
|
169
175
|
return createComponent("Graphics", props);
|
|
170
176
|
}
|
|
171
177
|
|
|
178
|
+
const graphicsAnchor = (anchor, width, height) => {
|
|
179
|
+
const observableAnchor = new ObservablePoint({ _onUpdate: () => {} }, 0, 0);
|
|
180
|
+
setObservablePoint(observableAnchor, anchor);
|
|
181
|
+
const ax = observableAnchor.x;
|
|
182
|
+
const ay = observableAnchor.y;
|
|
183
|
+
|
|
184
|
+
return { x: -ax * width, y: -ay * height };
|
|
185
|
+
}
|
|
186
|
+
|
|
172
187
|
export function Rect(props: RectProps) {
|
|
173
188
|
const { color, borderRadius, border } = useProps(props, {
|
|
174
189
|
borderRadius: null,
|
|
@@ -176,11 +191,12 @@ export function Rect(props: RectProps) {
|
|
|
176
191
|
})
|
|
177
192
|
|
|
178
193
|
return Graphics({
|
|
179
|
-
draw: (g, width, height) => {
|
|
194
|
+
draw: (g, width, height, anchor) => {
|
|
195
|
+
const { x, y } = graphicsAnchor(anchor, width, height);
|
|
180
196
|
if (borderRadius()) {
|
|
181
|
-
g.roundRect(
|
|
197
|
+
g.roundRect(x, y, width, height, borderRadius());
|
|
182
198
|
} else {
|
|
183
|
-
g.rect(
|
|
199
|
+
g.rect(x, y, width, height);
|
|
184
200
|
}
|
|
185
201
|
if (border) {
|
|
186
202
|
g.stroke(border);
|
|
@@ -191,45 +207,19 @@ export function Rect(props: RectProps) {
|
|
|
191
207
|
})
|
|
192
208
|
}
|
|
193
209
|
|
|
194
|
-
function drawShape(g: PixiGraphics, shape: 'circle' | 'ellipse', props: {
|
|
195
|
-
radius: Signal<number>;
|
|
196
|
-
color: Signal<string>;
|
|
197
|
-
border: Signal<number>;
|
|
198
|
-
} | {
|
|
199
|
-
width: Signal<number>;
|
|
200
|
-
height: Signal<number>;
|
|
201
|
-
color: Signal<string>;
|
|
202
|
-
border: Signal<number>;
|
|
203
|
-
}) {
|
|
204
|
-
const { color, border } = props;
|
|
205
|
-
if ('radius' in props) {
|
|
206
|
-
g.circle(0, 0, props.radius());
|
|
207
|
-
} else {
|
|
208
|
-
g.ellipse(0, 0, props.width() / 2, props.height() / 2);
|
|
209
|
-
}
|
|
210
|
-
if (border()) {
|
|
211
|
-
g.stroke(border());
|
|
212
|
-
}
|
|
213
|
-
g.fill(color());
|
|
214
|
-
}
|
|
215
|
-
|
|
216
210
|
export function Circle(props: CircleProps) {
|
|
217
|
-
const {
|
|
218
|
-
border: null
|
|
219
|
-
|
|
220
|
-
return Graphics({
|
|
221
|
-
draw: (g) => drawShape(g, 'circle', { radius, color, border }),
|
|
222
|
-
...props
|
|
223
|
-
})
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export function Ellipse(props: EllipseProps) {
|
|
227
|
-
const { width, height, color, border } = useProps(props, {
|
|
228
|
-
border: null
|
|
211
|
+
const { color, border, radius } = useProps(props, {
|
|
212
|
+
border: null,
|
|
213
|
+
radius: null
|
|
229
214
|
})
|
|
230
215
|
return Graphics({
|
|
231
|
-
draw: (g,
|
|
232
|
-
|
|
216
|
+
draw: (g, width, height, anchor) => {
|
|
217
|
+
const { x, y } = graphicsAnchor(anchor, width, height);
|
|
218
|
+
if (width == height || height == 0) {
|
|
219
|
+
g.circle(x, y, radius() || width);
|
|
220
|
+
} else {
|
|
221
|
+
g.ellipse(x, y, width, height);
|
|
222
|
+
}
|
|
233
223
|
if (border()) {
|
|
234
224
|
g.stroke(border());
|
|
235
225
|
}
|
|
@@ -239,17 +229,22 @@ export function Ellipse(props: EllipseProps) {
|
|
|
239
229
|
})
|
|
240
230
|
}
|
|
241
231
|
|
|
232
|
+
export function Ellipse(props: EllipseProps) {
|
|
233
|
+
return Circle(props as CircleProps);
|
|
234
|
+
}
|
|
235
|
+
|
|
242
236
|
export function Triangle(props: TriangleProps) {
|
|
243
|
-
const {
|
|
237
|
+
const { color, border } = useProps(props, {
|
|
244
238
|
border: null,
|
|
245
239
|
color: '#000'
|
|
246
240
|
})
|
|
247
241
|
return Graphics({
|
|
248
|
-
draw: (g, gWidth, gHeight) => {
|
|
249
|
-
|
|
250
|
-
g.
|
|
251
|
-
g.lineTo(gWidth,
|
|
252
|
-
g.lineTo(
|
|
242
|
+
draw: (g, gWidth, gHeight, anchor) => {
|
|
243
|
+
const { x, y } = graphicsAnchor(anchor, gWidth, gHeight);
|
|
244
|
+
g.moveTo(x, y + gHeight);
|
|
245
|
+
g.lineTo(x + gWidth / 2, y);
|
|
246
|
+
g.lineTo(x + gWidth, y + gHeight);
|
|
247
|
+
g.lineTo(x, y + gHeight);
|
|
253
248
|
g.fill(color());
|
|
254
249
|
if (border) {
|
|
255
250
|
g.stroke(border);
|
package/src/components/Sprite.ts
CHANGED
|
@@ -319,7 +319,8 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
|
|
|
319
319
|
this.update(value);
|
|
320
320
|
});
|
|
321
321
|
if (definition) {
|
|
322
|
-
|
|
322
|
+
const resolvedDefinition = definition instanceof Promise ? await definition : definition;
|
|
323
|
+
this.spritesheet = resolvedDefinition.value ?? resolvedDefinition;
|
|
323
324
|
await this.createAnimations();
|
|
324
325
|
}
|
|
325
326
|
if (sheet.params) {
|
|
@@ -420,7 +421,8 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
|
|
|
420
421
|
const definition = props.sheet?.definition ?? {};
|
|
421
422
|
|
|
422
423
|
if (definition?.type === 'reset') {
|
|
423
|
-
|
|
424
|
+
const resolvedValue = definition.value instanceof Promise ? await definition.value : definition.value;
|
|
425
|
+
this.spritesheet = resolvedValue ?? definition;
|
|
424
426
|
await this.resetAnimations();
|
|
425
427
|
}
|
|
426
428
|
|