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
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
import { isSignal, Signal } from "@signe/reactive";
|
|
2
|
+
import { Subscription } from "rxjs";
|
|
3
|
+
import { createComponent, Element, registerComponent } from "../engine/reactive";
|
|
4
|
+
import { ComponentFunction } from "../engine/signal";
|
|
5
|
+
import { arrayEquals, fps2ms, isBrowser, isFunction, preciseNow } from "../engine/utils";
|
|
6
|
+
import { DisplayObjectProps } from "./types/DisplayObject";
|
|
7
|
+
import { CanvasDOMElement, DOMElementProps } from "./DOMElement";
|
|
8
|
+
import { OnHook } from "./DisplayObject";
|
|
9
|
+
import { Tick } from "../directives/Scheduler";
|
|
10
|
+
import {
|
|
11
|
+
AnimationFrames,
|
|
12
|
+
FrameOptions,
|
|
13
|
+
SpritesheetOptions,
|
|
14
|
+
TextureOptions,
|
|
15
|
+
} from "./types/Spritesheet";
|
|
16
|
+
|
|
17
|
+
export interface DOMSpriteFrame {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DOMSpriteProps extends DOMElementProps {
|
|
25
|
+
image?: string;
|
|
26
|
+
rectangle?: DOMSpriteFrame | { value?: DOMSpriteFrame };
|
|
27
|
+
frames?: DOMSpriteFrame[];
|
|
28
|
+
frameIndex?: number;
|
|
29
|
+
fps?: number;
|
|
30
|
+
playing?: boolean;
|
|
31
|
+
loop?: boolean;
|
|
32
|
+
sheet?: {
|
|
33
|
+
definition?:
|
|
34
|
+
| DOMSpriteSheetDefinition
|
|
35
|
+
| { value?: DOMSpriteSheetDefinition }
|
|
36
|
+
| Signal<DOMSpriteSheetDefinition | undefined>
|
|
37
|
+
| Promise<DOMSpriteSheetDefinition>;
|
|
38
|
+
playing?: string;
|
|
39
|
+
params?: any;
|
|
40
|
+
onFinish?: () => void;
|
|
41
|
+
};
|
|
42
|
+
element?: "div" | "img";
|
|
43
|
+
attrs?: Record<string, any> & {
|
|
44
|
+
class?:
|
|
45
|
+
| string
|
|
46
|
+
| string[]
|
|
47
|
+
| Record<string, boolean>
|
|
48
|
+
| { items?: string[] }
|
|
49
|
+
| { value?: string | string[] | Record<string, boolean> };
|
|
50
|
+
style?:
|
|
51
|
+
| string
|
|
52
|
+
| Record<string, string | number>
|
|
53
|
+
| { value?: string | Record<string, string | number> };
|
|
54
|
+
};
|
|
55
|
+
onBeforeDestroy?: OnHook;
|
|
56
|
+
context?: {
|
|
57
|
+
tick?: Signal<Tick | null>;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const EVENTS = [
|
|
62
|
+
"click",
|
|
63
|
+
"mouseover",
|
|
64
|
+
"mouseout",
|
|
65
|
+
"mouseenter",
|
|
66
|
+
"mouseleave",
|
|
67
|
+
"mousemove",
|
|
68
|
+
"mouseup",
|
|
69
|
+
"mousedown",
|
|
70
|
+
"touchstart",
|
|
71
|
+
"touchend",
|
|
72
|
+
"touchmove",
|
|
73
|
+
"touchcancel",
|
|
74
|
+
"wheel",
|
|
75
|
+
"scroll",
|
|
76
|
+
"resize",
|
|
77
|
+
"focus",
|
|
78
|
+
"blur",
|
|
79
|
+
"change",
|
|
80
|
+
"input",
|
|
81
|
+
"submit",
|
|
82
|
+
"reset",
|
|
83
|
+
"keydown",
|
|
84
|
+
"keyup",
|
|
85
|
+
"keypress",
|
|
86
|
+
"contextmenu",
|
|
87
|
+
"drag",
|
|
88
|
+
"dragend",
|
|
89
|
+
"dragenter",
|
|
90
|
+
"dragleave",
|
|
91
|
+
"dragover",
|
|
92
|
+
"drop",
|
|
93
|
+
"dragstart",
|
|
94
|
+
"select",
|
|
95
|
+
"selectstart",
|
|
96
|
+
"selectend",
|
|
97
|
+
"selectall",
|
|
98
|
+
"selectnone",
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
type DOMSpriteTextureOptionsMerging = TextureOptions & {
|
|
102
|
+
spriteWidth: number;
|
|
103
|
+
spriteHeight: number;
|
|
104
|
+
image?: string;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
type DOMSpriteFrameOptions = FrameOptions & DOMSpriteFrame;
|
|
108
|
+
|
|
109
|
+
type DOMSpriteAnimationData = {
|
|
110
|
+
frames: DOMSpriteFrameOptions[];
|
|
111
|
+
animations: AnimationFrames;
|
|
112
|
+
params: any[];
|
|
113
|
+
data: DOMSpriteTextureOptionsMerging;
|
|
114
|
+
name: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
type DOMSpriteSheetDefinition = SpritesheetOptions & {
|
|
118
|
+
image?: string;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export class CanvasDOMSprite extends CanvasDOMElement {
|
|
122
|
+
private frameIndex = 0;
|
|
123
|
+
private frames: DOMSpriteFrame[] = [];
|
|
124
|
+
private rectangle?: DOMSpriteFrame;
|
|
125
|
+
private image?: string;
|
|
126
|
+
private fps = 120;
|
|
127
|
+
private loop = true;
|
|
128
|
+
private playing = true;
|
|
129
|
+
private hasExternalFrameIndex = false;
|
|
130
|
+
private elapsed = 0;
|
|
131
|
+
private tickSignal?: Signal<Tick | null>;
|
|
132
|
+
private tickSubscription?: Subscription;
|
|
133
|
+
private sheetSubscriptions: Subscription[] = [];
|
|
134
|
+
private sheetDefinition?: DOMSpriteSheetDefinition;
|
|
135
|
+
private sheetAnimations: Map<string, DOMSpriteAnimationData> = new Map();
|
|
136
|
+
private sheetCurrentAnimation?: DOMSpriteAnimationData;
|
|
137
|
+
private sheetCurrentName?: string;
|
|
138
|
+
private sheetParams: any = {};
|
|
139
|
+
private sheetTime = 0;
|
|
140
|
+
private sheetFrameIndex = 0;
|
|
141
|
+
private sheetFinished = false;
|
|
142
|
+
private sheetLoadToken = 0;
|
|
143
|
+
private sheetOnFinish?: () => void;
|
|
144
|
+
private rafId?: number;
|
|
145
|
+
private lastRafTimestamp?: number;
|
|
146
|
+
private lastTickTimestamp?: number;
|
|
147
|
+
private elementType: "div" | "img" = "div";
|
|
148
|
+
private isAnimating = false;
|
|
149
|
+
private playingSubscription?: Subscription;
|
|
150
|
+
private playingSignal?: Signal<boolean>;
|
|
151
|
+
|
|
152
|
+
onInit(props: DOMElementProps) {
|
|
153
|
+
const spriteProps = props as DOMSpriteProps;
|
|
154
|
+
this.elementType = spriteProps.element ?? "div";
|
|
155
|
+
const nextProps = this.mergeEventAttrs({ ...spriteProps, element: this.elementType });
|
|
156
|
+
this.tickSignal = nextProps.context?.tick;
|
|
157
|
+
this.applyProps(nextProps);
|
|
158
|
+
super.onInit(nextProps as any);
|
|
159
|
+
this.render();
|
|
160
|
+
this.updateAnimationLoop();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
onMount(context: Element<CanvasDOMElement>) {
|
|
164
|
+
this.tickSignal = context.props.context?.tick;
|
|
165
|
+
super.onMount(context);
|
|
166
|
+
this.bindPlayingSignal(context);
|
|
167
|
+
this.bindSheetParams(context);
|
|
168
|
+
this.updateAnimationLoop();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
onUpdate(props: DOMElementProps) {
|
|
172
|
+
const nextProps = this.mergeEventAttrs(props as DOMSpriteProps);
|
|
173
|
+
super.onUpdate(nextProps as any);
|
|
174
|
+
this.applyProps(nextProps);
|
|
175
|
+
this.render();
|
|
176
|
+
this.updateAnimationLoop();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async onDestroy(
|
|
180
|
+
parent: Element<CanvasDOMElement>,
|
|
181
|
+
afterDestroy: () => void
|
|
182
|
+
): Promise<void> {
|
|
183
|
+
if (this.playingSubscription) {
|
|
184
|
+
this.playingSubscription.unsubscribe();
|
|
185
|
+
this.playingSubscription = undefined;
|
|
186
|
+
}
|
|
187
|
+
if (this.sheetSubscriptions.length > 0) {
|
|
188
|
+
this.sheetSubscriptions.forEach((sub) => sub.unsubscribe());
|
|
189
|
+
this.sheetSubscriptions = [];
|
|
190
|
+
}
|
|
191
|
+
this.stopAnimationLoop();
|
|
192
|
+
await super.onDestroy(parent, afterDestroy);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private resolveRectangle(
|
|
196
|
+
rectangle?: DOMSpriteFrame | { value?: DOMSpriteFrame } | Signal<DOMSpriteFrame | undefined>
|
|
197
|
+
): DOMSpriteFrame | undefined {
|
|
198
|
+
if (!rectangle) return undefined;
|
|
199
|
+
const signalResolved = isSignal(rectangle as any)
|
|
200
|
+
? (rectangle as Signal<DOMSpriteFrame | undefined>)()
|
|
201
|
+
: rectangle;
|
|
202
|
+
if (!signalResolved) return undefined;
|
|
203
|
+
const resolved = (signalResolved as any).value ?? signalResolved;
|
|
204
|
+
if (!resolved) return undefined;
|
|
205
|
+
return resolved as DOMSpriteFrame;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private resolveSheetDefinition(
|
|
209
|
+
definition?:
|
|
210
|
+
| DOMSpriteSheetDefinition
|
|
211
|
+
| { value?: DOMSpriteSheetDefinition }
|
|
212
|
+
| Signal<DOMSpriteSheetDefinition | undefined>
|
|
213
|
+
| Promise<DOMSpriteSheetDefinition>
|
|
214
|
+
): DOMSpriteSheetDefinition | Promise<DOMSpriteSheetDefinition | undefined> | undefined {
|
|
215
|
+
if (!definition) return undefined;
|
|
216
|
+
const signalResolved = isSignal(definition as any)
|
|
217
|
+
? (definition as Signal<DOMSpriteSheetDefinition | undefined>)()
|
|
218
|
+
: definition;
|
|
219
|
+
if (!signalResolved) return undefined;
|
|
220
|
+
return (signalResolved as any).value ?? signalResolved;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async detectImageDimensions(
|
|
224
|
+
imagePath: string
|
|
225
|
+
): Promise<{ width: number; height: number }> {
|
|
226
|
+
if (!isBrowser() || !imagePath) {
|
|
227
|
+
return { width: 0, height: 0 };
|
|
228
|
+
}
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
const img = new Image();
|
|
231
|
+
img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
232
|
+
img.onerror = () => reject(new Error(`Failed to load image: ${imagePath}`));
|
|
233
|
+
img.src = imagePath;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private async createSheetAnimations(definition: DOMSpriteSheetDefinition) {
|
|
238
|
+
this.sheetAnimations.clear();
|
|
239
|
+
const { textures } = definition;
|
|
240
|
+
if (!textures) return;
|
|
241
|
+
|
|
242
|
+
const parentProps: (keyof TextureOptions)[] = [
|
|
243
|
+
"width",
|
|
244
|
+
"height",
|
|
245
|
+
"framesHeight",
|
|
246
|
+
"framesWidth",
|
|
247
|
+
"rectWidth",
|
|
248
|
+
"rectHeight",
|
|
249
|
+
"offset",
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
for (const animationName in textures) {
|
|
253
|
+
const baseOptions = parentProps.reduce(
|
|
254
|
+
(prev, val) => ({ ...prev, [val]: (definition as any)[val] }),
|
|
255
|
+
{}
|
|
256
|
+
);
|
|
257
|
+
const optionsTextures: DOMSpriteTextureOptionsMerging = {
|
|
258
|
+
...baseOptions,
|
|
259
|
+
...textures[animationName],
|
|
260
|
+
spriteWidth: 0,
|
|
261
|
+
spriteHeight: 0,
|
|
262
|
+
};
|
|
263
|
+
optionsTextures.image =
|
|
264
|
+
(textures[animationName] as any).image ?? definition.image;
|
|
265
|
+
|
|
266
|
+
const {
|
|
267
|
+
rectWidth,
|
|
268
|
+
rectHeight,
|
|
269
|
+
framesWidth = 1,
|
|
270
|
+
framesHeight = 1,
|
|
271
|
+
image,
|
|
272
|
+
} = optionsTextures;
|
|
273
|
+
|
|
274
|
+
let width = optionsTextures.width || 0;
|
|
275
|
+
let height = optionsTextures.height || 0;
|
|
276
|
+
|
|
277
|
+
if (image && ((!width || width <= 0) || (!height || height <= 0))) {
|
|
278
|
+
const dimensions = await this.detectImageDimensions(image);
|
|
279
|
+
if (!width || width <= 0) {
|
|
280
|
+
width = dimensions.width;
|
|
281
|
+
}
|
|
282
|
+
if (!height || height <= 0) {
|
|
283
|
+
height = dimensions.height;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!width || !height || !framesWidth || !framesHeight) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
optionsTextures.width = width;
|
|
292
|
+
optionsTextures.height = height;
|
|
293
|
+
optionsTextures.spriteWidth = rectWidth ? rectWidth : width / framesWidth;
|
|
294
|
+
optionsTextures.spriteHeight = rectHeight ? rectHeight : height / framesHeight;
|
|
295
|
+
|
|
296
|
+
this.sheetAnimations.set(animationName, {
|
|
297
|
+
frames: [],
|
|
298
|
+
name: animationName,
|
|
299
|
+
animations: textures[animationName].animations,
|
|
300
|
+
params: [],
|
|
301
|
+
data: optionsTextures,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private async setSheetDefinition(
|
|
307
|
+
definition?: DOMSpriteSheetDefinition | Promise<DOMSpriteSheetDefinition>
|
|
308
|
+
) {
|
|
309
|
+
const token = ++this.sheetLoadToken;
|
|
310
|
+
if (!definition) {
|
|
311
|
+
this.sheetDefinition = undefined;
|
|
312
|
+
this.sheetAnimations.clear();
|
|
313
|
+
this.sheetCurrentAnimation = undefined;
|
|
314
|
+
this.sheetCurrentName = undefined;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const resolved = await definition;
|
|
319
|
+
if (token !== this.sheetLoadToken) return;
|
|
320
|
+
if (!resolved) {
|
|
321
|
+
this.sheetDefinition = undefined;
|
|
322
|
+
this.sheetAnimations.clear();
|
|
323
|
+
this.sheetCurrentAnimation = undefined;
|
|
324
|
+
this.sheetCurrentName = undefined;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
this.sheetDefinition = resolved;
|
|
328
|
+
if (resolved.image) {
|
|
329
|
+
this.image = resolved.image;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
await this.createSheetAnimations(resolved);
|
|
333
|
+
|
|
334
|
+
const textureKeys = resolved.textures ? Object.keys(resolved.textures) : [];
|
|
335
|
+
const fallbackName =
|
|
336
|
+
this.sheetCurrentName ||
|
|
337
|
+
(textureKeys.includes("stand") ? "stand" : textureKeys[0]) ||
|
|
338
|
+
undefined;
|
|
339
|
+
if (fallbackName) {
|
|
340
|
+
this.playSheet(fallbackName, [this.sheetParams]);
|
|
341
|
+
}
|
|
342
|
+
this.render();
|
|
343
|
+
this.updateAnimationLoop();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private playSheet(name: string, params: any[] = []) {
|
|
347
|
+
const animParams = this.sheetCurrentAnimation?.params;
|
|
348
|
+
if (this.sheetCurrentAnimation && this.sheetCurrentAnimation.name === name) {
|
|
349
|
+
if (arrayEquals(params, animParams || [])) return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const animation = this.sheetAnimations.get(name);
|
|
353
|
+
if (!animation) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`Impossible to play the ${name} animation because it doesn't exist on the spritesheet`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const cloneParams = structuredClone(params);
|
|
360
|
+
let animations: any = animation.animations;
|
|
361
|
+
animations = isFunction(animations) ? (animations as Function)(...cloneParams) : animations;
|
|
362
|
+
|
|
363
|
+
animation.frames = [];
|
|
364
|
+
animation.params = cloneParams;
|
|
365
|
+
this.sheetCurrentAnimation = animation;
|
|
366
|
+
this.sheetCurrentName = name;
|
|
367
|
+
this.sheetTime = 0;
|
|
368
|
+
this.sheetFrameIndex = 0;
|
|
369
|
+
this.sheetFinished = false;
|
|
370
|
+
|
|
371
|
+
const data = animation.data;
|
|
372
|
+
const spriteWidth = data.spriteWidth;
|
|
373
|
+
const spriteHeight = data.spriteHeight;
|
|
374
|
+
const offsetX = data.offset?.x ?? 0;
|
|
375
|
+
const offsetY = data.offset?.y ?? 0;
|
|
376
|
+
|
|
377
|
+
for (const container of animations as FrameOptions[][]) {
|
|
378
|
+
for (const frame of container) {
|
|
379
|
+
const frameX = frame.frameX ?? 0;
|
|
380
|
+
const frameY = frame.frameY ?? 0;
|
|
381
|
+
animation.frames.push({
|
|
382
|
+
...frame,
|
|
383
|
+
x: frameX * spriteWidth + offsetX,
|
|
384
|
+
y: frameY * spriteHeight + offsetY,
|
|
385
|
+
width: spriteWidth,
|
|
386
|
+
height: spriteHeight,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.render();
|
|
392
|
+
this.updateAnimationLoop();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private getCurrentSheetFrame(): DOMSpriteFrame | null {
|
|
396
|
+
if (!this.sheetCurrentAnimation || this.sheetCurrentAnimation.frames.length === 0) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
return this.sheetCurrentAnimation.frames[this.sheetFrameIndex] ?? null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private advanceSheet(tick: Tick) {
|
|
403
|
+
if (!this.playing || !this.sheetCurrentAnimation) return;
|
|
404
|
+
|
|
405
|
+
const frames = this.sheetCurrentAnimation.frames;
|
|
406
|
+
if (frames.length <= 1) return;
|
|
407
|
+
|
|
408
|
+
const baseFps = this.fps > 0 ? this.fps : 60;
|
|
409
|
+
let deltaRatio = 0;
|
|
410
|
+
if (tick.deltaTime) {
|
|
411
|
+
deltaRatio = tick.deltaTime / fps2ms(baseFps);
|
|
412
|
+
} else if (typeof tick.deltaRatio === "number") {
|
|
413
|
+
deltaRatio = tick.deltaRatio * (baseFps / 60);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const nextFrame = frames[this.sheetFrameIndex + 1];
|
|
417
|
+
if (!nextFrame) {
|
|
418
|
+
if (this.loop) {
|
|
419
|
+
this.sheetTime = 0;
|
|
420
|
+
this.sheetFrameIndex = 0;
|
|
421
|
+
this.applyFrame(frames[0]);
|
|
422
|
+
return;
|
|
423
|
+
} else if (!this.sheetFinished) {
|
|
424
|
+
this.sheetFinished = true;
|
|
425
|
+
if (this.sheetOnFinish) this.sheetOnFinish();
|
|
426
|
+
}
|
|
427
|
+
this.applyFrame(frames[frames.length - 1]);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
this.sheetTime += deltaRatio || 0;
|
|
432
|
+
if (this.sheetTime >= nextFrame.time) {
|
|
433
|
+
this.sheetFrameIndex += 1;
|
|
434
|
+
}
|
|
435
|
+
this.applyFrame(frames[this.sheetFrameIndex]);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private mergeEventAttrs(props: DOMSpriteProps): DOMSpriteProps {
|
|
439
|
+
let merged = props.attrs ? { ...props.attrs } : undefined;
|
|
440
|
+
for (const event of EVENTS) {
|
|
441
|
+
const handler = (props as any)[event];
|
|
442
|
+
if (handler && !merged?.[event]) {
|
|
443
|
+
if (!merged) merged = {};
|
|
444
|
+
merged[event] = handler;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (!merged) return props;
|
|
448
|
+
return { ...props, attrs: merged };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private applyProps(props: DOMSpriteProps) {
|
|
452
|
+
if (props.image !== undefined) {
|
|
453
|
+
this.image = isSignal(props.image as any) ? (props.image as any)() : props.image;
|
|
454
|
+
}
|
|
455
|
+
if (props.rectangle !== undefined) {
|
|
456
|
+
this.rectangle = this.resolveRectangle(props.rectangle);
|
|
457
|
+
}
|
|
458
|
+
if (props.frames !== undefined) {
|
|
459
|
+
const resolvedFrames = isSignal(props.frames as any)
|
|
460
|
+
? (props.frames as any)()
|
|
461
|
+
: props.frames;
|
|
462
|
+
this.frames = resolvedFrames ?? [];
|
|
463
|
+
}
|
|
464
|
+
if (props.sheet !== undefined) {
|
|
465
|
+
const resolvedSheet = isSignal(props.sheet as any)
|
|
466
|
+
? (props.sheet as any)()
|
|
467
|
+
: props.sheet;
|
|
468
|
+
if (resolvedSheet?.definition !== undefined) {
|
|
469
|
+
const resolvedDefinition = this.resolveSheetDefinition(resolvedSheet.definition);
|
|
470
|
+
if (resolvedDefinition instanceof Promise) {
|
|
471
|
+
void this.setSheetDefinition(resolvedDefinition);
|
|
472
|
+
} else if (resolvedDefinition && resolvedDefinition !== this.sheetDefinition) {
|
|
473
|
+
void this.setSheetDefinition(resolvedDefinition);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (resolvedSheet?.params !== undefined) {
|
|
477
|
+
this.sheetParams = { ...this.sheetParams, ...resolvedSheet.params };
|
|
478
|
+
}
|
|
479
|
+
if (resolvedSheet?.playing !== undefined) {
|
|
480
|
+
this.sheetCurrentName = resolvedSheet.playing;
|
|
481
|
+
}
|
|
482
|
+
if (resolvedSheet?.onFinish !== undefined) {
|
|
483
|
+
this.sheetOnFinish = resolvedSheet.onFinish;
|
|
484
|
+
}
|
|
485
|
+
if (this.sheetAnimations.size > 0 && this.sheetCurrentName) {
|
|
486
|
+
this.playSheet(this.sheetCurrentName, [this.sheetParams]);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (props.frameIndex !== undefined) {
|
|
490
|
+
const isFrameIndexSignal = isSignal(props.frameIndex as any);
|
|
491
|
+
const resolvedIndex = isFrameIndexSignal
|
|
492
|
+
? (props.frameIndex as any)()
|
|
493
|
+
: props.frameIndex;
|
|
494
|
+
if (resolvedIndex !== undefined) {
|
|
495
|
+
this.frameIndex = resolvedIndex;
|
|
496
|
+
}
|
|
497
|
+
this.hasExternalFrameIndex = isFrameIndexSignal;
|
|
498
|
+
}
|
|
499
|
+
if (props.fps !== undefined) {
|
|
500
|
+
const resolvedFps = isSignal(props.fps as any) ? (props.fps as any)() : props.fps;
|
|
501
|
+
if (resolvedFps !== undefined) {
|
|
502
|
+
this.fps = resolvedFps;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (props.playing !== undefined) {
|
|
506
|
+
const resolvedPlaying = isSignal(props.playing as any)
|
|
507
|
+
? (props.playing as any)()
|
|
508
|
+
: props.playing;
|
|
509
|
+
this.playing = resolvedPlaying === true;
|
|
510
|
+
if (!this.playing) {
|
|
511
|
+
this.elapsed = 0;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (props.loop !== undefined) {
|
|
515
|
+
const resolvedLoop = isSignal(props.loop as any) ? (props.loop as any)() : props.loop;
|
|
516
|
+
if (resolvedLoop !== undefined) {
|
|
517
|
+
this.loop = resolvedLoop;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private bindPlayingSignal(context: Element<CanvasDOMElement>) {
|
|
523
|
+
const playingValue = context.propObservables?.playing as any;
|
|
524
|
+
if (!playingValue || !isSignal(playingValue)) return;
|
|
525
|
+
if (this.playingSignal === playingValue) return;
|
|
526
|
+
|
|
527
|
+
if (this.playingSubscription) {
|
|
528
|
+
this.playingSubscription.unsubscribe();
|
|
529
|
+
this.playingSubscription = undefined;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
this.playingSignal = playingValue;
|
|
533
|
+
this.playing = playingValue();
|
|
534
|
+
this.playingSubscription = playingValue.observable.subscribe((value) => {
|
|
535
|
+
this.playing = value === true;
|
|
536
|
+
if (!this.playing) {
|
|
537
|
+
this.elapsed = 0;
|
|
538
|
+
}
|
|
539
|
+
this.updateAnimationLoop();
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private bindSheetParams(context: Element<CanvasDOMElement>) {
|
|
544
|
+
const sheetProps = context.propObservables?.sheet as any;
|
|
545
|
+
const params = sheetProps?.params as any;
|
|
546
|
+
if (!params || typeof params !== "object") return;
|
|
547
|
+
for (const key in params) {
|
|
548
|
+
const value = params[key];
|
|
549
|
+
if (!isSignal(value)) continue;
|
|
550
|
+
this.sheetSubscriptions.push(
|
|
551
|
+
value.observable.subscribe((nextValue) => {
|
|
552
|
+
this.sheetParams = { ...this.sheetParams, [key]: nextValue };
|
|
553
|
+
if (this.sheetCurrentName) {
|
|
554
|
+
this.playSheet(this.sheetCurrentName, [this.sheetParams]);
|
|
555
|
+
}
|
|
556
|
+
})
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private getFrames(): DOMSpriteFrame[] {
|
|
562
|
+
if (this.frames && this.frames.length > 0) return this.frames;
|
|
563
|
+
if (this.rectangle) return [this.rectangle];
|
|
564
|
+
return [];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private normalizeIndex(index: number, length: number) {
|
|
568
|
+
if (length <= 0) return 0;
|
|
569
|
+
if (this.loop) {
|
|
570
|
+
const mod = index % length;
|
|
571
|
+
return mod < 0 ? mod + length : mod;
|
|
572
|
+
}
|
|
573
|
+
if (index < 0) return 0;
|
|
574
|
+
if (index >= length) return length - 1;
|
|
575
|
+
return index;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private render() {
|
|
579
|
+
if (!this.element) return;
|
|
580
|
+
const sheetFrame = this.getCurrentSheetFrame();
|
|
581
|
+
if (sheetFrame) {
|
|
582
|
+
this.applyFrame(sheetFrame);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const frames = this.getFrames();
|
|
586
|
+
if (frames.length === 0) {
|
|
587
|
+
if (this.elementType === "img" && this.image) {
|
|
588
|
+
(this.element as HTMLImageElement).src = this.image;
|
|
589
|
+
} else if (this.image) {
|
|
590
|
+
this.element.style.backgroundImage = `url("${this.image}")`;
|
|
591
|
+
}
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const normalizedIndex = this.normalizeIndex(this.frameIndex, frames.length);
|
|
596
|
+
if (normalizedIndex !== this.frameIndex && !this.loop) {
|
|
597
|
+
this.frameIndex = normalizedIndex;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const frame = frames[normalizedIndex];
|
|
601
|
+
this.applyFrame(frame);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private applyFrame(frame: DOMSpriteFrame) {
|
|
605
|
+
if (!this.element) return;
|
|
606
|
+
this.element.style.width = `${frame.width}px`;
|
|
607
|
+
this.element.style.height = `${frame.height}px`;
|
|
608
|
+
|
|
609
|
+
const x = frame.x ?? 0;
|
|
610
|
+
const y = frame.y ?? 0;
|
|
611
|
+
|
|
612
|
+
if (this.elementType === "img") {
|
|
613
|
+
const img = this.element as HTMLImageElement;
|
|
614
|
+
if (this.image) {
|
|
615
|
+
img.src = this.image;
|
|
616
|
+
}
|
|
617
|
+
img.style.objectFit = "none";
|
|
618
|
+
img.style.objectPosition = `-${x}px -${y}px`;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (this.image) {
|
|
623
|
+
this.element.style.backgroundImage = `url("${this.image}")`;
|
|
624
|
+
}
|
|
625
|
+
this.element.style.backgroundRepeat = "no-repeat";
|
|
626
|
+
this.element.style.backgroundPosition = `-${x}px -${y}px`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private updateAnimationLoop() {
|
|
630
|
+
const sheetFrames = this.sheetCurrentAnimation?.frames ?? [];
|
|
631
|
+
const shouldAnimate = this.sheetCurrentAnimation
|
|
632
|
+
? this.playing && sheetFrames.length > 1
|
|
633
|
+
: this.playing &&
|
|
634
|
+
!this.hasExternalFrameIndex &&
|
|
635
|
+
this.getFrames().length > 1 &&
|
|
636
|
+
this.fps > 0;
|
|
637
|
+
|
|
638
|
+
if (shouldAnimate) {
|
|
639
|
+
this.startAnimationLoop();
|
|
640
|
+
} else {
|
|
641
|
+
this.stopAnimationLoop();
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
private startAnimationLoop() {
|
|
646
|
+
if (this.tickSubscription || this.rafId !== undefined) {
|
|
647
|
+
this.stopAnimationLoop();
|
|
648
|
+
}
|
|
649
|
+
this.isAnimating = true;
|
|
650
|
+
this.elapsed = 0;
|
|
651
|
+
this.lastTickTimestamp = undefined;
|
|
652
|
+
|
|
653
|
+
if (this.tickSignal?.observable) {
|
|
654
|
+
this.tickSubscription = (this.tickSignal.observable as any).subscribe((result: any) => {
|
|
655
|
+
const tick = result?.value ?? result;
|
|
656
|
+
if (!tick) return;
|
|
657
|
+
if (this.sheetCurrentAnimation) {
|
|
658
|
+
this.advanceSheet(tick);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
let deltaTime = tick.deltaTime || 0;
|
|
662
|
+
if (deltaTime <= 0) {
|
|
663
|
+
const now = preciseNow();
|
|
664
|
+
if (this.lastTickTimestamp === undefined) {
|
|
665
|
+
this.lastTickTimestamp = now;
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
deltaTime = now - this.lastTickTimestamp;
|
|
669
|
+
this.lastTickTimestamp = now;
|
|
670
|
+
}
|
|
671
|
+
if (deltaTime > 0) {
|
|
672
|
+
this.advance(deltaTime);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const step = (timestamp: number) => {
|
|
679
|
+
if (!this.playing || this.hasExternalFrameIndex) {
|
|
680
|
+
this.stopAnimationLoop();
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (this.lastRafTimestamp === undefined) {
|
|
684
|
+
this.lastRafTimestamp = timestamp;
|
|
685
|
+
}
|
|
686
|
+
const delta = timestamp - this.lastRafTimestamp;
|
|
687
|
+
this.lastRafTimestamp = timestamp;
|
|
688
|
+
if (this.sheetCurrentAnimation) {
|
|
689
|
+
this.advanceSheet({
|
|
690
|
+
timestamp,
|
|
691
|
+
deltaTime: delta,
|
|
692
|
+
frame: 0,
|
|
693
|
+
deltaRatio: delta / fps2ms(this.fps),
|
|
694
|
+
});
|
|
695
|
+
} else {
|
|
696
|
+
this.advance(delta);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (isBrowser()) {
|
|
700
|
+
this.rafId = window.requestAnimationFrame(step);
|
|
701
|
+
} else {
|
|
702
|
+
this.rafId = setTimeout(() => {
|
|
703
|
+
step(preciseNow());
|
|
704
|
+
}, fps2ms(this.fps)) as unknown as number;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
if (isBrowser()) {
|
|
709
|
+
this.rafId = window.requestAnimationFrame(step);
|
|
710
|
+
} else {
|
|
711
|
+
this.rafId = setTimeout(() => {
|
|
712
|
+
step(preciseNow());
|
|
713
|
+
}, fps2ms(this.fps)) as unknown as number;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private stopAnimationLoop() {
|
|
718
|
+
this.isAnimating = false;
|
|
719
|
+
if (this.tickSubscription) {
|
|
720
|
+
this.tickSubscription.unsubscribe();
|
|
721
|
+
this.tickSubscription = undefined;
|
|
722
|
+
}
|
|
723
|
+
if (this.rafId !== undefined) {
|
|
724
|
+
if (isBrowser()) {
|
|
725
|
+
window.cancelAnimationFrame(this.rafId);
|
|
726
|
+
} else {
|
|
727
|
+
clearTimeout(this.rafId);
|
|
728
|
+
}
|
|
729
|
+
this.rafId = undefined;
|
|
730
|
+
this.lastRafTimestamp = undefined;
|
|
731
|
+
}
|
|
732
|
+
this.lastTickTimestamp = undefined;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
private advance(deltaTime: number) {
|
|
736
|
+
const frames = this.getFrames();
|
|
737
|
+
if (!this.playing || frames.length <= 1 || this.fps <= 0) return;
|
|
738
|
+
|
|
739
|
+
this.elapsed += deltaTime;
|
|
740
|
+
const frameDuration = fps2ms(this.fps);
|
|
741
|
+
|
|
742
|
+
while (this.elapsed >= frameDuration) {
|
|
743
|
+
this.elapsed -= frameDuration;
|
|
744
|
+
this.frameIndex += 1;
|
|
745
|
+
if (!this.loop && this.frameIndex >= frames.length) {
|
|
746
|
+
this.frameIndex = frames.length - 1;
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
this.render();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
registerComponent("DOMSprite", CanvasDOMSprite);
|
|
756
|
+
|
|
757
|
+
export const DOMSprite: ComponentFunction<DOMSpriteProps> = (props) => {
|
|
758
|
+
return createComponent("DOMSprite", props);
|
|
759
|
+
};
|