@aura3d/animation 1.3.3 → 1.4.1
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/package.json +36 -3
- package/src/AnimationAction.ts +0 -166
- package/src/AnimationClip.ts +0 -63
- package/src/AnimationClipEvents.ts +0 -501
- package/src/AnimationClipRegistry.ts +0 -404
- package/src/AnimationController.ts +0 -1017
- package/src/AnimationEvents.ts +0 -34
- package/src/AnimationLayer.ts +0 -73
- package/src/AnimationMixer.ts +0 -402
- package/src/AnimationStateGraph.ts +0 -105
- package/src/AnimationStateMachine.ts +0 -265
- package/src/AnimationTrack.ts +0 -176
- package/src/BlendTree.ts +0 -89
- package/src/Bone.ts +0 -40
- package/src/CrowdAnimation.ts +0 -25
- package/src/ECSAnimationBridge.ts +0 -30
- package/src/FighterAnimationAdapter.ts +0 -124
- package/src/FootIk.ts +0 -251
- package/src/HumanoidBoneInference.ts +0 -323
- package/src/HumanoidRetargeting.ts +0 -700
- package/src/IK.ts +0 -117
- package/src/Inertialization.ts +0 -269
- package/src/Keyframe.ts +0 -159
- package/src/LocomotionController.ts +0 -155
- package/src/LocomotionKit.ts +0 -89
- package/src/MotionMatchingFixtures.ts +0 -254
- package/src/MotionQuality.ts +0 -182
- package/src/RigQuality.ts +0 -101
- package/src/RootMotion.ts +0 -124
- package/src/SceneAnimationBridge.ts +0 -80
- package/src/SecondaryAnimationFixtures.ts +0 -210
- package/src/Skeleton.ts +0 -78
- package/src/Skinning.ts +0 -18
- package/src/SpringBones.ts +0 -243
- package/src/browser-index.ts +0 -66
- package/src/index.ts +0 -66
- package/src/library/performanceStateGraph.ts +0 -173
- package/src/library/standardHumanoidClips.ts +0 -659
- package/src/threejs-compatibility/AnimationAction.ts +0 -27
- package/src/threejs-compatibility/AnimationClip.ts +0 -16
- package/src/threejs-compatibility/AnimationDiagnostics.ts +0 -42
- package/src/threejs-compatibility/AnimationMixer.ts +0 -24
- package/src/threejs-compatibility/MorphTargetMixer.ts +0 -16
- package/src/threejs-compatibility/Skeleton.ts +0 -9
- package/src/threejs-compatibility/SkinnedMesh.ts +0 -12
- package/src/threejs-compatibility/index.ts +0 -11
|
@@ -1,501 +0,0 @@
|
|
|
1
|
-
export type AnimationPlaybackDirection = 1 | -1;
|
|
2
|
-
|
|
3
|
-
export type AnimationClipEventType =
|
|
4
|
-
| "marker"
|
|
5
|
-
| "hitbox"
|
|
6
|
-
| "hurtbox"
|
|
7
|
-
| "footstep"
|
|
8
|
-
| "sfx"
|
|
9
|
-
| "vfx"
|
|
10
|
-
| "camera"
|
|
11
|
-
| "state"
|
|
12
|
-
| "custom"
|
|
13
|
-
| (string & {});
|
|
14
|
-
|
|
15
|
-
export interface AnimationClipEvent<TName extends string = string, TPayload = unknown> {
|
|
16
|
-
readonly id?: string;
|
|
17
|
-
readonly name: TName;
|
|
18
|
-
readonly type?: AnimationClipEventType;
|
|
19
|
-
readonly time: number;
|
|
20
|
-
readonly duration?: number;
|
|
21
|
-
readonly payload?: TPayload;
|
|
22
|
-
readonly once?: boolean;
|
|
23
|
-
readonly tags?: readonly string[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface AnimationClipEventSource<TEvent extends AnimationClipEvent = AnimationClipEvent> {
|
|
27
|
-
readonly id: string;
|
|
28
|
-
readonly duration: number;
|
|
29
|
-
readonly events?: readonly TEvent[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface AnimationClipEventSamplingOptions {
|
|
33
|
-
readonly from: number;
|
|
34
|
-
readonly to: number;
|
|
35
|
-
readonly duration?: number;
|
|
36
|
-
readonly loop?: boolean;
|
|
37
|
-
readonly direction?: AnimationPlaybackDirection;
|
|
38
|
-
readonly includeStart?: boolean;
|
|
39
|
-
readonly includeEnd?: boolean;
|
|
40
|
-
readonly loopCount?: number;
|
|
41
|
-
readonly playbackTime?: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface AnimationClipEventInvocation<
|
|
45
|
-
TEvent extends AnimationClipEvent = AnimationClipEvent,
|
|
46
|
-
TClipId extends string = string
|
|
47
|
-
> {
|
|
48
|
-
readonly clipId: TClipId;
|
|
49
|
-
readonly event: TEvent;
|
|
50
|
-
readonly time: number;
|
|
51
|
-
readonly previousTime: number;
|
|
52
|
-
readonly localTime: number;
|
|
53
|
-
readonly normalizedTime: number;
|
|
54
|
-
readonly loopCount: number;
|
|
55
|
-
readonly direction: AnimationPlaybackDirection;
|
|
56
|
-
readonly playbackTime?: number;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export type AnimationClipEventListener<TPayload> = (payload: TPayload) => void;
|
|
60
|
-
export type AnimationClipEventUnsubscribe = () => void;
|
|
61
|
-
|
|
62
|
-
export class AnimationEventDispatcher<TEventMap = Record<string, unknown>> {
|
|
63
|
-
private readonly listeners = new Map<string, Set<AnimationClipEventListener<unknown>>>();
|
|
64
|
-
|
|
65
|
-
on<K extends Extract<keyof TEventMap, string>>(
|
|
66
|
-
type: K,
|
|
67
|
-
listener: AnimationClipEventListener<TEventMap[K]>
|
|
68
|
-
): AnimationClipEventUnsubscribe {
|
|
69
|
-
const listeners = this.listeners.get(type) ?? new Set<AnimationClipEventListener<unknown>>();
|
|
70
|
-
listeners.add(listener as AnimationClipEventListener<unknown>);
|
|
71
|
-
this.listeners.set(type, listeners);
|
|
72
|
-
|
|
73
|
-
return () => {
|
|
74
|
-
this.off(type, listener);
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
once<K extends Extract<keyof TEventMap, string>>(
|
|
79
|
-
type: K,
|
|
80
|
-
listener: AnimationClipEventListener<TEventMap[K]>
|
|
81
|
-
): AnimationClipEventUnsubscribe {
|
|
82
|
-
const unsubscribe = this.on(type, (payload) => {
|
|
83
|
-
unsubscribe();
|
|
84
|
-
listener(payload);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
return unsubscribe;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
off<K extends Extract<keyof TEventMap, string>>(
|
|
91
|
-
type: K,
|
|
92
|
-
listener: AnimationClipEventListener<TEventMap[K]>
|
|
93
|
-
): void {
|
|
94
|
-
const listeners = this.listeners.get(type);
|
|
95
|
-
if (!listeners) return;
|
|
96
|
-
|
|
97
|
-
listeners.delete(listener as AnimationClipEventListener<unknown>);
|
|
98
|
-
if (listeners.size === 0) {
|
|
99
|
-
this.listeners.delete(type);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
emit<K extends Extract<keyof TEventMap, string>>(type: K, payload: TEventMap[K]): void {
|
|
104
|
-
const listeners = this.listeners.get(type);
|
|
105
|
-
if (!listeners) return;
|
|
106
|
-
|
|
107
|
-
for (const listener of [...listeners]) {
|
|
108
|
-
listener(payload);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
clear(type?: Extract<keyof TEventMap, string>): void {
|
|
113
|
-
if (type) {
|
|
114
|
-
this.listeners.delete(type);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
this.listeners.clear();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
listenerCount(type?: Extract<keyof TEventMap, string>): number {
|
|
122
|
-
if (type) {
|
|
123
|
-
return this.listeners.get(type)?.size ?? 0;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
let count = 0;
|
|
127
|
-
for (const listeners of this.listeners.values()) {
|
|
128
|
-
count += listeners.size;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return count;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function createAnimationClipEvent<TName extends string, TPayload = unknown>(
|
|
136
|
-
name: TName,
|
|
137
|
-
time: number,
|
|
138
|
-
options: Omit<AnimationClipEvent<TName, TPayload>, "name" | "time"> = {}
|
|
139
|
-
): AnimationClipEvent<TName, TPayload> {
|
|
140
|
-
return {
|
|
141
|
-
...options,
|
|
142
|
-
name,
|
|
143
|
-
time
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function normalizeClipEvents<TEvent extends AnimationClipEvent>(
|
|
148
|
-
events: readonly TEvent[] = []
|
|
149
|
-
): readonly TEvent[] {
|
|
150
|
-
return [...events].sort((a, b) => {
|
|
151
|
-
if (a.time !== b.time) return a.time - b.time;
|
|
152
|
-
return a.name.localeCompare(b.name);
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function animationClipEventKey(
|
|
157
|
-
clipId: string,
|
|
158
|
-
event: AnimationClipEvent
|
|
159
|
-
): string {
|
|
160
|
-
return `${clipId}:${event.id ?? event.name}:${event.type ?? "marker"}:${event.time}`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function sampleClipEvents<TEvent extends AnimationClipEvent, TClipId extends string = string>(
|
|
164
|
-
source: AnimationClipEventSource<TEvent> & { readonly id: TClipId },
|
|
165
|
-
options: AnimationClipEventSamplingOptions
|
|
166
|
-
): AnimationClipEventInvocation<TEvent, TClipId>[] {
|
|
167
|
-
const events = normalizeClipEvents(source.events);
|
|
168
|
-
if (events.length === 0) return [];
|
|
169
|
-
|
|
170
|
-
const duration = sanitizeDuration(options.duration ?? source.duration);
|
|
171
|
-
const direction = options.direction ?? (options.to >= options.from ? 1 : -1);
|
|
172
|
-
const includeStart = options.includeStart ?? false;
|
|
173
|
-
const includeEnd = options.includeEnd ?? true;
|
|
174
|
-
const loopCount = options.loopCount ?? 0;
|
|
175
|
-
const playbackTime = options.playbackTime;
|
|
176
|
-
|
|
177
|
-
if (duration === 0) {
|
|
178
|
-
return events
|
|
179
|
-
.filter((event) => event.time === 0 && includesBoundary(includeStart, includeEnd))
|
|
180
|
-
.map((event) => createInvocation(source.id, event, options.from, 0, duration, loopCount, direction, playbackTime));
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const from = normalizeLocalTime(options.from, duration, options.loop ?? false);
|
|
184
|
-
const to = normalizeLocalTime(options.to, duration, options.loop ?? false);
|
|
185
|
-
const segments = createSamplingSegments(from, to, duration, options.loop ?? false, direction);
|
|
186
|
-
const invocations: AnimationClipEventInvocation<TEvent, TClipId>[] = [];
|
|
187
|
-
|
|
188
|
-
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) {
|
|
189
|
-
const segment = segments[segmentIndex];
|
|
190
|
-
const segmentIncludeStart = segmentIndex === 0 ? includeStart : true;
|
|
191
|
-
const segmentIncludeEnd = segmentIndex === segments.length - 1 ? includeEnd : true;
|
|
192
|
-
const matching = events.filter((event) =>
|
|
193
|
-
eventInSegment(event.time, segment.start, segment.end, segmentIncludeStart, segmentIncludeEnd)
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
if (direction === -1) {
|
|
197
|
-
matching.reverse();
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
for (const event of matching) {
|
|
201
|
-
invocations.push(
|
|
202
|
-
createInvocation(
|
|
203
|
-
source.id,
|
|
204
|
-
event,
|
|
205
|
-
options.from,
|
|
206
|
-
event.time,
|
|
207
|
-
duration,
|
|
208
|
-
loopCount + segment.loopOffset,
|
|
209
|
-
direction,
|
|
210
|
-
playbackTime
|
|
211
|
-
)
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return invocations;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
interface SamplingSegment {
|
|
220
|
-
readonly start: number;
|
|
221
|
-
readonly end: number;
|
|
222
|
-
readonly loopOffset: number;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function createSamplingSegments(
|
|
226
|
-
from: number,
|
|
227
|
-
to: number,
|
|
228
|
-
duration: number,
|
|
229
|
-
loop: boolean,
|
|
230
|
-
direction: AnimationPlaybackDirection
|
|
231
|
-
): SamplingSegment[] {
|
|
232
|
-
if (!loop) {
|
|
233
|
-
return [
|
|
234
|
-
{
|
|
235
|
-
start: Math.min(from, to),
|
|
236
|
-
end: Math.max(from, to),
|
|
237
|
-
loopOffset: 0
|
|
238
|
-
}
|
|
239
|
-
];
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (direction === 1) {
|
|
243
|
-
if (to >= from) {
|
|
244
|
-
return [{ start: from, end: to, loopOffset: 0 }];
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return [
|
|
248
|
-
{ start: from, end: duration, loopOffset: 0 },
|
|
249
|
-
{ start: 0, end: to, loopOffset: 1 }
|
|
250
|
-
];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (to <= from) {
|
|
254
|
-
return [{ start: to, end: from, loopOffset: 0 }];
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return [
|
|
258
|
-
{ start: 0, end: from, loopOffset: 0 },
|
|
259
|
-
{ start: to, end: duration, loopOffset: 1 }
|
|
260
|
-
];
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function createInvocation<TEvent extends AnimationClipEvent, TClipId extends string>(
|
|
264
|
-
clipId: TClipId,
|
|
265
|
-
event: TEvent,
|
|
266
|
-
previousTime: number,
|
|
267
|
-
localTime: number,
|
|
268
|
-
duration: number,
|
|
269
|
-
loopCount: number,
|
|
270
|
-
direction: AnimationPlaybackDirection,
|
|
271
|
-
playbackTime?: number
|
|
272
|
-
): AnimationClipEventInvocation<TEvent, TClipId> {
|
|
273
|
-
return {
|
|
274
|
-
clipId,
|
|
275
|
-
event,
|
|
276
|
-
time: event.time,
|
|
277
|
-
previousTime,
|
|
278
|
-
localTime,
|
|
279
|
-
normalizedTime: duration > 0 ? event.time / duration : 0,
|
|
280
|
-
loopCount,
|
|
281
|
-
direction,
|
|
282
|
-
playbackTime
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function eventInSegment(
|
|
287
|
-
time: number,
|
|
288
|
-
start: number,
|
|
289
|
-
end: number,
|
|
290
|
-
includeStart: boolean,
|
|
291
|
-
includeEnd: boolean
|
|
292
|
-
): boolean {
|
|
293
|
-
const startsAfter = includeStart ? time >= start : time > start;
|
|
294
|
-
const endsBefore = includeEnd ? time <= end : time < end;
|
|
295
|
-
return startsAfter && endsBefore;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function includesBoundary(includeStart: boolean, includeEnd: boolean): boolean {
|
|
299
|
-
return includeStart || includeEnd;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function normalizeLocalTime(time: number, duration: number, loop: boolean): number {
|
|
303
|
-
if (!Number.isFinite(time)) return 0;
|
|
304
|
-
if (!loop) return clamp(time, 0, duration);
|
|
305
|
-
return positiveModulo(time, duration);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function sanitizeDuration(duration: number): number {
|
|
309
|
-
if (!Number.isFinite(duration) || duration <= 0) return 0;
|
|
310
|
-
return duration;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function positiveModulo(value: number, divisor: number): number {
|
|
314
|
-
if (divisor <= 0) return 0;
|
|
315
|
-
return ((value % divisor) + divisor) % divisor;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function clamp(value: number, min: number, max: number): number {
|
|
319
|
-
return Math.min(max, Math.max(min, value));
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// ---- event tracks (authoring container on top of the existing dispatcher/sampler) -------------
|
|
323
|
-
//
|
|
324
|
-
// An event *track* is a named lane of typed markers on a clip's timeline (e.g. a "hitbox" lane, a
|
|
325
|
-
// "footstep" lane, a "vfx" lane). This adds the authoring + active-frame layer the editor and
|
|
326
|
-
// gameplay need WITHOUT rebuilding the dispatcher/sampler above: `toEventSource()` flattens the
|
|
327
|
-
// tracks back into the `AnimationClipEventSource` that `sampleClipEvents` already consumes.
|
|
328
|
-
|
|
329
|
-
export interface AnimationEventTrack<TEvent extends AnimationClipEvent = AnimationClipEvent> {
|
|
330
|
-
readonly name: string;
|
|
331
|
-
readonly type: AnimationClipEventType;
|
|
332
|
-
readonly markers: readonly TEvent[];
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
export interface SerializedAnimationEventTracks<TEvent extends AnimationClipEvent = AnimationClipEvent> {
|
|
336
|
-
readonly schema: "animation-event-tracks/v1";
|
|
337
|
-
readonly clipId: string;
|
|
338
|
-
readonly duration: number;
|
|
339
|
-
readonly tracks: readonly AnimationEventTrack<TEvent>[];
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
export interface ActiveWindow {
|
|
343
|
-
readonly start: number;
|
|
344
|
-
readonly end: number;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export interface AddMarkerOptions<TPayload = unknown> {
|
|
348
|
-
readonly id?: string;
|
|
349
|
-
readonly type?: AnimationClipEventType;
|
|
350
|
-
/** Window length (seconds). >0 makes this an active-frame marker (e.g. hitbox on/off). */
|
|
351
|
-
readonly duration?: number;
|
|
352
|
-
readonly payload?: TPayload;
|
|
353
|
-
readonly once?: boolean;
|
|
354
|
-
readonly tags?: readonly string[];
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Authoring container for clip event tracks: add/move/delete typed markers on named lanes, query
|
|
359
|
-
* active-frame windows, flatten to the existing `AnimationClipEventSource`, and serialize. Pure and
|
|
360
|
-
* deterministic — marker ids are assigned from a monotonic counter (no `Date.now`/`Math.random`).
|
|
361
|
-
*/
|
|
362
|
-
export class AnimationEventTrackContainer {
|
|
363
|
-
readonly clipId: string;
|
|
364
|
-
duration: number;
|
|
365
|
-
private readonly trackOrder: string[] = [];
|
|
366
|
-
private readonly trackMap = new Map<string, { type: AnimationClipEventType; markers: AnimationClipEvent[] }>();
|
|
367
|
-
private idCounter = 0;
|
|
368
|
-
|
|
369
|
-
constructor(clipId: string, duration: number) {
|
|
370
|
-
if (!Number.isFinite(duration) || duration < 0) {
|
|
371
|
-
throw new Error("Event-track clip duration must be a finite, non-negative number.");
|
|
372
|
-
}
|
|
373
|
-
this.clipId = clipId;
|
|
374
|
-
this.duration = duration;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
addTrack(name: string, type: AnimationClipEventType = "marker"): this {
|
|
378
|
-
if (!this.trackMap.has(name)) {
|
|
379
|
-
this.trackMap.set(name, { type, markers: [] });
|
|
380
|
-
this.trackOrder.push(name);
|
|
381
|
-
}
|
|
382
|
-
return this;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
removeTrack(name: string): this {
|
|
386
|
-
if (this.trackMap.delete(name)) {
|
|
387
|
-
this.trackOrder.splice(this.trackOrder.indexOf(name), 1);
|
|
388
|
-
}
|
|
389
|
-
return this;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/** Add a marker to a track (auto-creates the track). Returns the marker id. */
|
|
393
|
-
addMarker(trackName: string, time: number, options: AddMarkerOptions = {}): string {
|
|
394
|
-
if (!Number.isFinite(time) || time < 0) {
|
|
395
|
-
throw new Error("Event marker time must be a finite, non-negative number.");
|
|
396
|
-
}
|
|
397
|
-
this.addTrack(trackName, options.type);
|
|
398
|
-
const track = this.trackMap.get(trackName)!;
|
|
399
|
-
const id = options.id ?? `${trackName}#${this.idCounter++}`;
|
|
400
|
-
const marker: AnimationClipEvent = {
|
|
401
|
-
id,
|
|
402
|
-
name: trackName,
|
|
403
|
-
type: options.type ?? track.type,
|
|
404
|
-
time,
|
|
405
|
-
...(options.duration !== undefined ? { duration: options.duration } : {}),
|
|
406
|
-
...(options.payload !== undefined ? { payload: options.payload } : {}),
|
|
407
|
-
...(options.once !== undefined ? { once: options.once } : {}),
|
|
408
|
-
...(options.tags !== undefined ? { tags: options.tags } : {})
|
|
409
|
-
};
|
|
410
|
-
track.markers.push(marker);
|
|
411
|
-
track.markers.sort((a, b) => a.time - b.time);
|
|
412
|
-
return id;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
moveMarker(trackName: string, markerId: string, time: number): this {
|
|
416
|
-
if (!Number.isFinite(time) || time < 0) {
|
|
417
|
-
throw new Error("Event marker time must be a finite, non-negative number.");
|
|
418
|
-
}
|
|
419
|
-
const track = this.trackMap.get(trackName);
|
|
420
|
-
const index = track?.markers.findIndex((m) => m.id === markerId) ?? -1;
|
|
421
|
-
if (!track || index < 0) throw new Error(`Marker ${markerId} not found on track ${trackName}.`);
|
|
422
|
-
track.markers[index] = { ...track.markers[index]!, time };
|
|
423
|
-
track.markers.sort((a, b) => a.time - b.time);
|
|
424
|
-
return this;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
removeMarker(trackName: string, markerId: string): this {
|
|
428
|
-
const track = this.trackMap.get(trackName);
|
|
429
|
-
if (!track) return this;
|
|
430
|
-
const index = track.markers.findIndex((m) => m.id === markerId);
|
|
431
|
-
if (index >= 0) track.markers.splice(index, 1);
|
|
432
|
-
return this;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
track(name: string): AnimationEventTrack | undefined {
|
|
436
|
-
const track = this.trackMap.get(name);
|
|
437
|
-
return track ? { name, type: track.type, markers: [...track.markers] } : undefined;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
tracks(): readonly AnimationEventTrack[] {
|
|
441
|
-
return this.trackOrder.map((name) => this.track(name)!);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
markerCount(): number {
|
|
445
|
-
let count = 0;
|
|
446
|
-
for (const track of this.trackMap.values()) count += track.markers.length;
|
|
447
|
-
return count;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/** Flatten every track's markers into the source shape `sampleClipEvents` consumes. */
|
|
451
|
-
toEventSource(): AnimationClipEventSource {
|
|
452
|
-
const events: AnimationClipEvent[] = [];
|
|
453
|
-
for (const name of this.trackOrder) events.push(...this.trackMap.get(name)!.markers);
|
|
454
|
-
return { id: this.clipId, duration: this.duration, events: normalizeClipEvents(events) };
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/** Active-frame windows for a track: [time, time+duration] for each marker with a duration. */
|
|
458
|
-
activeWindows(trackName: string): readonly ActiveWindow[] {
|
|
459
|
-
const track = this.trackMap.get(trackName);
|
|
460
|
-
if (!track) return [];
|
|
461
|
-
return track.markers
|
|
462
|
-
.filter((m) => (m.duration ?? 0) > 0)
|
|
463
|
-
.map((m) => ({ start: m.time, end: m.time + (m.duration ?? 0) }));
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/** Whether any active-frame window on a track is open at `time` (hitbox live). */
|
|
467
|
-
isActive(trackName: string, time: number): boolean {
|
|
468
|
-
return this.activeWindows(trackName).some((w) => time >= w.start && time < w.end);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
serialize(): SerializedAnimationEventTracks {
|
|
472
|
-
return {
|
|
473
|
-
schema: "animation-event-tracks/v1",
|
|
474
|
-
clipId: this.clipId,
|
|
475
|
-
duration: this.duration,
|
|
476
|
-
tracks: this.tracks()
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
export function createAnimationEventTracks(clipId: string, duration: number): AnimationEventTrackContainer {
|
|
482
|
-
return new AnimationEventTrackContainer(clipId, duration);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
export function deserializeAnimationEventTracks(data: SerializedAnimationEventTracks): AnimationEventTrackContainer {
|
|
486
|
-
const container = new AnimationEventTrackContainer(data.clipId, data.duration);
|
|
487
|
-
for (const track of data.tracks) {
|
|
488
|
-
container.addTrack(track.name, track.type);
|
|
489
|
-
for (const marker of track.markers) {
|
|
490
|
-
container.addMarker(track.name, marker.time, {
|
|
491
|
-
id: marker.id,
|
|
492
|
-
type: marker.type,
|
|
493
|
-
...(marker.duration !== undefined ? { duration: marker.duration } : {}),
|
|
494
|
-
...(marker.payload !== undefined ? { payload: marker.payload } : {}),
|
|
495
|
-
...(marker.once !== undefined ? { once: marker.once } : {}),
|
|
496
|
-
...(marker.tags !== undefined ? { tags: marker.tags } : {})
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return container;
|
|
501
|
-
}
|