@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.
Files changed (46) hide show
  1. package/package.json +36 -3
  2. package/src/AnimationAction.ts +0 -166
  3. package/src/AnimationClip.ts +0 -63
  4. package/src/AnimationClipEvents.ts +0 -501
  5. package/src/AnimationClipRegistry.ts +0 -404
  6. package/src/AnimationController.ts +0 -1017
  7. package/src/AnimationEvents.ts +0 -34
  8. package/src/AnimationLayer.ts +0 -73
  9. package/src/AnimationMixer.ts +0 -402
  10. package/src/AnimationStateGraph.ts +0 -105
  11. package/src/AnimationStateMachine.ts +0 -265
  12. package/src/AnimationTrack.ts +0 -176
  13. package/src/BlendTree.ts +0 -89
  14. package/src/Bone.ts +0 -40
  15. package/src/CrowdAnimation.ts +0 -25
  16. package/src/ECSAnimationBridge.ts +0 -30
  17. package/src/FighterAnimationAdapter.ts +0 -124
  18. package/src/FootIk.ts +0 -251
  19. package/src/HumanoidBoneInference.ts +0 -323
  20. package/src/HumanoidRetargeting.ts +0 -700
  21. package/src/IK.ts +0 -117
  22. package/src/Inertialization.ts +0 -269
  23. package/src/Keyframe.ts +0 -159
  24. package/src/LocomotionController.ts +0 -155
  25. package/src/LocomotionKit.ts +0 -89
  26. package/src/MotionMatchingFixtures.ts +0 -254
  27. package/src/MotionQuality.ts +0 -182
  28. package/src/RigQuality.ts +0 -101
  29. package/src/RootMotion.ts +0 -124
  30. package/src/SceneAnimationBridge.ts +0 -80
  31. package/src/SecondaryAnimationFixtures.ts +0 -210
  32. package/src/Skeleton.ts +0 -78
  33. package/src/Skinning.ts +0 -18
  34. package/src/SpringBones.ts +0 -243
  35. package/src/browser-index.ts +0 -66
  36. package/src/index.ts +0 -66
  37. package/src/library/performanceStateGraph.ts +0 -173
  38. package/src/library/standardHumanoidClips.ts +0 -659
  39. package/src/threejs-compatibility/AnimationAction.ts +0 -27
  40. package/src/threejs-compatibility/AnimationClip.ts +0 -16
  41. package/src/threejs-compatibility/AnimationDiagnostics.ts +0 -42
  42. package/src/threejs-compatibility/AnimationMixer.ts +0 -24
  43. package/src/threejs-compatibility/MorphTargetMixer.ts +0 -16
  44. package/src/threejs-compatibility/Skeleton.ts +0 -9
  45. package/src/threejs-compatibility/SkinnedMesh.ts +0 -12
  46. package/src/threejs-compatibility/index.ts +0 -11
@@ -1,1017 +0,0 @@
1
- import {
2
- animationClipEventKey,
3
- AnimationEventDispatcher,
4
- sampleClipEvents,
5
- type AnimationClipEvent,
6
- type AnimationClipEventInvocation,
7
- type AnimationClipEventUnsubscribe,
8
- type AnimationPlaybackDirection
9
- } from "./AnimationClipEvents.js";
10
- import {
11
- AnimationClipRegistry,
12
- type AnimationClipDefinition,
13
- type RegisteredAnimationClip
14
- } from "./AnimationClipRegistry.js";
15
-
16
- export type AnimationLoopMode = "once" | "loop" | "pingpong";
17
- export type AnimationPlaybackStatus = "idle" | "playing" | "paused" | "stopped" | "completed";
18
-
19
- export interface AnimationVector3 {
20
- readonly x: number;
21
- readonly y: number;
22
- readonly z: number;
23
- }
24
-
25
- export interface AnimationQuaternion {
26
- readonly x: number;
27
- readonly y: number;
28
- readonly z: number;
29
- readonly w: number;
30
- }
31
-
32
- export interface AnimationPoseTransform {
33
- readonly position?: AnimationVector3;
34
- readonly rotation?: AnimationQuaternion;
35
- readonly scale?: AnimationVector3;
36
- }
37
-
38
- export interface AnimationRootMotion {
39
- readonly translation?: AnimationVector3;
40
- readonly rotation?: AnimationQuaternion;
41
- }
42
-
43
- export interface AnimationPose {
44
- readonly bones: Record<string, AnimationPoseTransform>;
45
- readonly morphTargets?: Record<string, number>;
46
- readonly rootMotion?: AnimationRootMotion;
47
- readonly metadata?: Record<string, unknown>;
48
- }
49
-
50
- export interface AnimationClipPlaybackState<TClipId extends string = string> {
51
- readonly id: string;
52
- readonly clipId: TClipId;
53
- readonly status: AnimationPlaybackStatus;
54
- readonly localTime: number;
55
- readonly previousLocalTime: number;
56
- readonly normalizedTime: number;
57
- readonly duration: number;
58
- readonly speed: number;
59
- readonly weight: number;
60
- readonly targetWeight: number;
61
- readonly loopMode: AnimationLoopMode;
62
- readonly loopCount: number;
63
- readonly direction: AnimationPlaybackDirection;
64
- readonly completed: boolean;
65
- readonly fade?: AnimationFadeState;
66
- }
67
-
68
- export interface AnimationFadeState {
69
- readonly kind: "in" | "out";
70
- readonly elapsed: number;
71
- readonly duration: number;
72
- readonly fromWeight: number;
73
- readonly toWeight: number;
74
- }
75
-
76
- export interface AnimationPlayOptions<TClipId extends string = string> {
77
- readonly id?: string;
78
- readonly restart?: boolean;
79
- readonly exclusive?: boolean;
80
- readonly loop?: AnimationLoopMode | boolean;
81
- readonly speed?: number;
82
- readonly weight?: number;
83
- readonly startTime?: number;
84
- readonly fadeIn?: number;
85
- readonly paused?: boolean;
86
- readonly direction?: AnimationPlaybackDirection;
87
- readonly metadata?: Record<string, unknown>;
88
- readonly fallbackClipId?: TClipId;
89
- }
90
-
91
- export interface AnimationStopOptions {
92
- readonly fadeOut?: number;
93
- }
94
-
95
- export interface AnimationCrossfadeOptions<TClipId extends string = string> extends AnimationPlayOptions<TClipId> {
96
- readonly fromClipId?: TClipId;
97
- }
98
-
99
- export type AnimationCrossFadeOptions<TClipId extends string = string> = AnimationCrossfadeOptions<TClipId>;
100
-
101
- export interface AnimationScrubOptions<TClipId extends string = string> {
102
- readonly clipId?: TClipId;
103
- readonly emitEvents?: boolean;
104
- readonly play?: boolean;
105
- readonly createIfMissing?: boolean;
106
- }
107
-
108
- export interface AnimationPoseCaptureOptions<TClipId extends string = string> {
109
- readonly clipId?: TClipId;
110
- readonly time?: number;
111
- readonly emitEvent?: boolean;
112
- }
113
-
114
- export interface AnimationPoseSnapshot<TClipId extends string = string> {
115
- readonly time: number;
116
- readonly pose: AnimationPose;
117
- readonly clips: readonly AnimationClipPlaybackState<TClipId>[];
118
- }
119
-
120
- export interface AnimationControllerSnapshot<TClipId extends string = string> {
121
- readonly time: number;
122
- readonly activeClipId?: TClipId;
123
- readonly clips: readonly AnimationClipPlaybackState<TClipId>[];
124
- }
125
-
126
- export type AnimationAnimationAction = "speak" | "listen" | "gesture" | "walk" | "action";
127
-
128
- export interface AnimationAnimationTimelineBinding<TClipId extends string = string> {
129
- readonly action: AnimationAnimationAction;
130
- readonly clipId: TClipId;
131
- readonly loop: AnimationLoopMode | boolean;
132
- readonly restartOnEnter: boolean;
133
- }
134
-
135
- export interface AnimationAnimationTimelineSample<TClipId extends string = string> {
136
- readonly time: number;
137
- readonly action: AnimationAnimationAction;
138
- readonly clipId: TClipId;
139
- readonly playback: AnimationClipPlaybackState<TClipId>;
140
- }
141
-
142
- export interface AnimationClipLoopEvent<TClipId extends string = string> {
143
- readonly clipId: TClipId;
144
- readonly playbackId: string;
145
- readonly loopCount: number;
146
- readonly loopsPassed: number;
147
- }
148
-
149
- export interface AnimationCrossfadeEvent<TClipId extends string = string> {
150
- readonly fromClipIds: readonly TClipId[];
151
- readonly toClipId: TClipId;
152
- readonly duration: number;
153
- }
154
-
155
- export type AnimationCrossFadeEvent<TClipId extends string = string> = AnimationCrossfadeEvent<TClipId>;
156
-
157
- export interface AnimationScrubEvent<TClipId extends string = string> {
158
- readonly clipId: TClipId;
159
- readonly playbackId: string;
160
- readonly fromTime: number;
161
- readonly toTime: number;
162
- }
163
-
164
- export interface AnimationControllerEventMap<
165
- TClipId extends string = string,
166
- TEvent extends AnimationClipEvent = AnimationClipEvent
167
- > {
168
- readonly start: AnimationClipPlaybackState<TClipId>;
169
- readonly end: AnimationClipPlaybackState<TClipId>;
170
- readonly loop: AnimationClipLoopEvent<TClipId>;
171
- readonly event: AnimationClipEventInvocation<TEvent, TClipId>;
172
- readonly crossFadeStart: AnimationCrossFadeEvent<TClipId>;
173
- readonly crossFadeEnd: AnimationCrossFadeEvent<TClipId>;
174
- readonly crossfadeStart: AnimationCrossfadeEvent<TClipId>;
175
- readonly crossfadeEnd: AnimationCrossfadeEvent<TClipId>;
176
- readonly scrub: AnimationScrubEvent<TClipId>;
177
- readonly poseCaptured: AnimationPoseSnapshot<TClipId>;
178
- readonly stateChanged: AnimationControllerSnapshot<TClipId>;
179
- }
180
-
181
- interface InternalPlaybackState<TClipId extends string, TEvent extends AnimationClipEvent>
182
- extends AnimationClipPlaybackState<TClipId> {
183
- clip: RegisteredAnimationClip<TClipId, TEvent, AnimationPose>;
184
- metadata?: Record<string, unknown>;
185
- onceEvents: Set<string>;
186
- playhead: number;
187
- inputDirection: AnimationPlaybackDirection;
188
- status: AnimationPlaybackStatus;
189
- localTime: number;
190
- previousLocalTime: number;
191
- normalizedTime: number;
192
- speed: number;
193
- weight: number;
194
- targetWeight: number;
195
- loopCount: number;
196
- direction: AnimationPlaybackDirection;
197
- completed: boolean;
198
- fade?: AnimationFadeState;
199
- }
200
-
201
- export class AnimationController<
202
- TClipId extends string = string,
203
- TEvent extends AnimationClipEvent = AnimationClipEvent
204
- > {
205
- readonly registry: AnimationClipRegistry<TClipId, TEvent, AnimationPose>;
206
-
207
- private readonly dispatcher = new AnimationEventDispatcher<AnimationControllerEventMap<TClipId, TEvent>>();
208
- private readonly states = new Map<string, InternalPlaybackState<TClipId, TEvent>>();
209
- private clockTime = 0;
210
- private nextPlaybackIndex = 0;
211
- private pendingCrossfade?: AnimationCrossfadeEvent<TClipId>;
212
-
213
- constructor(registry: AnimationClipRegistry<TClipId, TEvent, AnimationPose> = new AnimationClipRegistry()) {
214
- this.registry = registry;
215
- }
216
-
217
- on<K extends Extract<keyof AnimationControllerEventMap<TClipId, TEvent>, string>>(
218
- type: K,
219
- listener: (payload: AnimationControllerEventMap<TClipId, TEvent>[K]) => void
220
- ): AnimationClipEventUnsubscribe {
221
- return this.dispatcher.on(type, listener);
222
- }
223
-
224
- once<K extends Extract<keyof AnimationControllerEventMap<TClipId, TEvent>, string>>(
225
- type: K,
226
- listener: (payload: AnimationControllerEventMap<TClipId, TEvent>[K]) => void
227
- ): AnimationClipEventUnsubscribe {
228
- return this.dispatcher.once(type, listener);
229
- }
230
-
231
- registerClip(
232
- definition: AnimationClipDefinition<TClipId, TEvent, AnimationPose>
233
- ): RegisteredAnimationClip<TClipId, TEvent, AnimationPose> {
234
- return this.registry.register(definition);
235
- }
236
-
237
- play(clipId: TClipId, options: AnimationPlayOptions<TClipId> = {}): AnimationClipPlaybackState<TClipId> {
238
- const resolvedClipId = this.registry.has(clipId) ? clipId : options.fallbackClipId;
239
- if (!resolvedClipId) {
240
- throw new Error(`Animation clip "${clipId}" is not registered.`);
241
- }
242
-
243
- const existing = this.findStateByClip(resolvedClipId);
244
- if (existing && !options.restart) {
245
- existing.status = options.paused ? "paused" : "playing";
246
- existing.speed = sanitizeSpeed(options.speed ?? existing.speed);
247
- existing.targetWeight = sanitizeWeight(options.weight ?? existing.targetWeight);
248
- existing.inputDirection = options.direction ?? existing.inputDirection;
249
- this.emitStateChanged();
250
- return cloneState(existing);
251
- }
252
-
253
- if (options.exclusive ?? true) {
254
- this.states.clear();
255
- }
256
-
257
- const clip = this.registry.require(resolvedClipId);
258
- const playback = this.createPlaybackState(clip, options);
259
- this.states.set(playback.id, playback);
260
- this.dispatcher.emit("start", cloneState(playback));
261
- this.emitStateChanged();
262
- return cloneState(playback);
263
- }
264
-
265
- playOnce(clipId: TClipId, options: Omit<AnimationPlayOptions<TClipId>, "loop"> = {}): AnimationClipPlaybackState<TClipId> {
266
- return this.play(clipId, {
267
- ...options,
268
- loop: "once",
269
- restart: options.restart ?? true
270
- });
271
- }
272
-
273
- bindAnimationTimelineAction(
274
- action: AnimationAnimationAction,
275
- bindings: readonly AnimationAnimationTimelineBinding<TClipId>[],
276
- time: number,
277
- options: Omit<AnimationPlayOptions<TClipId>, "loop" | "restart"> = {}
278
- ): AnimationAnimationTimelineSample<TClipId> {
279
- const binding = bindings.find((candidate) => candidate.action === action);
280
- if (!binding) throw new Error(`No animation animation binding registered for action "${action}".`);
281
- const playback = this.play(binding.clipId, {
282
- ...options,
283
- loop: binding.loop,
284
- restart: binding.restartOnEnter,
285
- metadata: {
286
- ...(options.metadata ?? {}),
287
- animationAction: action,
288
- timelineTime: time
289
- }
290
- });
291
- return {
292
- time,
293
- action,
294
- clipId: binding.clipId,
295
- playback
296
- };
297
- }
298
-
299
- pause(clipId?: TClipId): void {
300
- for (const state of this.selectStates(clipId)) {
301
- if (state.status === "playing") {
302
- state.status = "paused";
303
- }
304
- }
305
-
306
- this.emitStateChanged();
307
- }
308
-
309
- resume(clipId?: TClipId): void {
310
- for (const state of this.selectStates(clipId)) {
311
- if (state.status === "paused") {
312
- state.status = "playing";
313
- }
314
- }
315
-
316
- this.emitStateChanged();
317
- }
318
-
319
- restart(clipId?: TClipId, options: AnimationPlayOptions<TClipId> = {}): AnimationClipPlaybackState<TClipId> {
320
- const resolvedClipId = clipId ?? this.activeClipId();
321
- if (!resolvedClipId) {
322
- throw new Error("Cannot restart animation without an active clip or clipId.");
323
- }
324
-
325
- return this.play(resolvedClipId, {
326
- ...options,
327
- restart: true
328
- });
329
- }
330
-
331
- stop(clipId?: TClipId, options: AnimationStopOptions = {}): void {
332
- const targets = this.selectStates(clipId);
333
- const fadeOut = sanitizeDuration(options.fadeOut ?? 0);
334
-
335
- for (const state of targets) {
336
- if (fadeOut > 0) {
337
- state.fade = {
338
- kind: "out",
339
- elapsed: 0,
340
- duration: fadeOut,
341
- fromWeight: state.weight,
342
- toWeight: 0
343
- };
344
- state.targetWeight = 0;
345
- } else {
346
- this.states.delete(state.id);
347
- state.status = "stopped";
348
- this.dispatcher.emit("end", cloneState(state));
349
- }
350
- }
351
-
352
- this.emitStateChanged();
353
- }
354
-
355
- crossFade(
356
- toClipId: TClipId,
357
- duration: number,
358
- options: AnimationCrossFadeOptions<TClipId> = {}
359
- ): AnimationClipPlaybackState<TClipId> {
360
- return this.crossfade(toClipId, duration, options);
361
- }
362
-
363
- crossfade(
364
- toClipId: TClipId,
365
- duration: number,
366
- options: AnimationCrossfadeOptions<TClipId> = {}
367
- ): AnimationClipPlaybackState<TClipId> {
368
- const fadeDuration = sanitizeDuration(duration);
369
- const fromStates = options.fromClipId ? this.selectStates(options.fromClipId) : this.getInternalStates();
370
- const fromClipIds = fromStates.map((state) => state.clipId);
371
- const event: AnimationCrossfadeEvent<TClipId> = {
372
- fromClipIds,
373
- toClipId,
374
- duration: fadeDuration
375
- };
376
-
377
- this.dispatcher.emit("crossFadeStart", event);
378
- this.dispatcher.emit("crossfadeStart", event);
379
- this.pendingCrossfade = event;
380
-
381
- for (const state of fromStates) {
382
- state.fade = {
383
- kind: "out",
384
- elapsed: 0,
385
- duration: fadeDuration,
386
- fromWeight: state.weight,
387
- toWeight: 0
388
- };
389
- state.targetWeight = 0;
390
- }
391
-
392
- const next = this.play(toClipId, {
393
- ...options,
394
- exclusive: false,
395
- restart: true,
396
- fadeIn: fadeDuration,
397
- weight: options.weight ?? 1
398
- });
399
-
400
- if (fadeDuration === 0) {
401
- this.finishCrossfade();
402
- }
403
-
404
- return next;
405
- }
406
-
407
- update(dt: number): AnimationControllerSnapshot<TClipId> {
408
- if (!Number.isFinite(dt) || dt === 0) {
409
- return this.snapshot();
410
- }
411
-
412
- this.clockTime += dt;
413
- const endedStates: InternalPlaybackState<TClipId, TEvent>[] = [];
414
-
415
- for (const state of this.getInternalStates()) {
416
- this.updateFade(state, Math.abs(dt));
417
-
418
- if (state.status !== "playing") {
419
- continue;
420
- }
421
-
422
- const advance = advanceState(state, dt);
423
- this.emitSampledEvents(state);
424
-
425
- if (advance.loopsPassed > 0) {
426
- this.dispatcher.emit("loop", {
427
- clipId: state.clipId,
428
- playbackId: state.id,
429
- loopCount: state.loopCount,
430
- loopsPassed: advance.loopsPassed
431
- });
432
- }
433
-
434
- if (advance.completed) {
435
- state.status = "completed";
436
- state.completed = true;
437
- endedStates.push(state);
438
- }
439
- }
440
-
441
- for (const state of endedStates) {
442
- this.dispatcher.emit("end", cloneState(state));
443
- }
444
-
445
- this.removeFinishedFadeOuts();
446
- this.finishCrossfadeIfReady();
447
- this.emitStateChanged();
448
- return this.snapshot();
449
- }
450
-
451
- scrub(time: number, options?: AnimationScrubOptions<TClipId>): AnimationPoseSnapshot<TClipId>;
452
- scrub(clipId: TClipId, time: number, options?: AnimationScrubOptions<TClipId>): AnimationPoseSnapshot<TClipId>;
453
- scrub(
454
- clipOrTime: TClipId | number,
455
- timeOrOptions: number | AnimationScrubOptions<TClipId> = {},
456
- maybeOptions: AnimationScrubOptions<TClipId> = {}
457
- ): AnimationPoseSnapshot<TClipId> {
458
- const hasClipId = typeof clipOrTime === "string";
459
- const time = hasClipId ? Number(timeOrOptions) : clipOrTime;
460
- const options = hasClipId ? maybeOptions : (timeOrOptions as AnimationScrubOptions<TClipId>);
461
- const clipId = hasClipId ? clipOrTime : options.clipId ?? this.activeClipId();
462
-
463
- if (!clipId) {
464
- throw new Error("Cannot scrub without an active clip or clipId.");
465
- }
466
-
467
- let state = this.findStateByClip(clipId);
468
- if (!state) {
469
- if (options.createIfMissing === false) {
470
- throw new Error(`Cannot scrub inactive animation clip "${clipId}".`);
471
- }
472
-
473
- this.play(clipId, {
474
- startTime: time,
475
- paused: true,
476
- restart: true
477
- });
478
- state = this.findStateByClip(clipId);
479
- }
480
-
481
- if (!state) {
482
- throw new Error(`Cannot scrub animation clip "${clipId}".`);
483
- }
484
-
485
- const previousTime = state.localTime;
486
- state.previousLocalTime = previousTime;
487
- state.localTime = normalizeStateTime(time, state.duration, state.loopMode);
488
- state.playhead = state.localTime;
489
- state.normalizedTime = state.duration > 0 ? state.localTime / state.duration : 0;
490
- state.completed = false;
491
- state.status = options.play ? "playing" : "paused";
492
-
493
- if (options.emitEvents) {
494
- this.emitSampledEvents(state);
495
- }
496
-
497
- this.dispatcher.emit("scrub", {
498
- clipId,
499
- playbackId: state.id,
500
- fromTime: previousTime,
501
- toTime: state.localTime
502
- });
503
- this.emitStateChanged();
504
- return this.capturePose({ emitEvent: false });
505
- }
506
-
507
- capturePose(options: AnimationPoseCaptureOptions<TClipId> = {}): AnimationPoseSnapshot<TClipId> {
508
- const states = options.clipId
509
- ? this.selectStates(options.clipId)
510
- : this.getInternalStates().filter((state) => state.weight > 0);
511
-
512
- const pose = options.time !== undefined && options.clipId
513
- ? this.sampleSinglePose(this.registry.require(options.clipId), options.time)
514
- : this.blendStates(states);
515
-
516
- const snapshot: AnimationPoseSnapshot<TClipId> = {
517
- time: this.clockTime,
518
- pose,
519
- clips: states.map(cloneState)
520
- };
521
-
522
- if (options.emitEvent ?? true) {
523
- this.dispatcher.emit("poseCaptured", snapshot);
524
- }
525
-
526
- return snapshot;
527
- }
528
-
529
- state(clipId?: TClipId): AnimationClipPlaybackState<TClipId> | undefined {
530
- const state = clipId ? this.findStateByClip(clipId) : this.primaryState();
531
- return state ? cloneState(state) : undefined;
532
- }
533
-
534
- snapshot(): AnimationControllerSnapshot<TClipId> {
535
- return {
536
- time: this.clockTime,
537
- activeClipId: this.activeClipId(),
538
- clips: this.getInternalStates().map(cloneState)
539
- };
540
- }
541
-
542
- dispose(): void {
543
- this.states.clear();
544
- this.dispatcher.clear();
545
- this.pendingCrossfade = undefined;
546
- }
547
-
548
- private createPlaybackState(
549
- clip: RegisteredAnimationClip<TClipId, TEvent, AnimationPose>,
550
- options: AnimationPlayOptions<TClipId>
551
- ): InternalPlaybackState<TClipId, TEvent> {
552
- const targetWeight = sanitizeWeight(options.weight ?? 1);
553
- const fadeIn = sanitizeDuration(options.fadeIn ?? 0);
554
- const localTime = normalizeStateTime(options.startTime ?? 0, clip.duration, normalizeLoopMode(options.loop, clip.loop));
555
- const id = options.id ?? `${clip.id}:${this.nextPlaybackIndex}`;
556
- this.nextPlaybackIndex += 1;
557
-
558
- return {
559
- id,
560
- clip,
561
- clipId: clip.id,
562
- metadata: options.metadata,
563
- status: options.paused ? "paused" : "playing",
564
- localTime,
565
- previousLocalTime: localTime,
566
- normalizedTime: clip.duration > 0 ? localTime / clip.duration : 0,
567
- duration: clip.duration,
568
- speed: sanitizeSpeed(options.speed ?? 1),
569
- weight: fadeIn > 0 ? 0 : targetWeight,
570
- targetWeight,
571
- loopMode: normalizeLoopMode(options.loop, clip.loop),
572
- loopCount: 0,
573
- direction: options.direction ?? 1,
574
- inputDirection: options.direction ?? 1,
575
- completed: false,
576
- playhead: localTime,
577
- onceEvents: new Set<string>(),
578
- fade: fadeIn > 0
579
- ? {
580
- kind: "in",
581
- elapsed: 0,
582
- duration: fadeIn,
583
- fromWeight: 0,
584
- toWeight: targetWeight
585
- }
586
- : undefined
587
- };
588
- }
589
-
590
- private emitSampledEvents(state: InternalPlaybackState<TClipId, TEvent>): void {
591
- const invocations = sampleClipEvents(
592
- {
593
- id: state.clipId,
594
- duration: state.duration,
595
- events: state.clip.events
596
- },
597
- {
598
- from: state.previousLocalTime,
599
- to: state.localTime,
600
- duration: state.duration,
601
- loop: state.loopMode !== "once",
602
- direction: state.direction,
603
- loopCount: state.loopCount,
604
- playbackTime: this.clockTime
605
- }
606
- );
607
-
608
- for (const invocation of invocations) {
609
- if (invocation.event.once) {
610
- const key = animationClipEventKey(invocation.clipId, invocation.event);
611
- if (state.onceEvents.has(key)) continue;
612
- state.onceEvents.add(key);
613
- }
614
-
615
- this.dispatcher.emit("event", invocation);
616
- }
617
- }
618
-
619
- private updateFade(state: InternalPlaybackState<TClipId, TEvent>, dt: number): void {
620
- if (!state.fade) return;
621
-
622
- if (state.fade.duration === 0) {
623
- state.weight = state.fade.toWeight;
624
- state.fade = undefined;
625
- return;
626
- }
627
-
628
- const elapsed = Math.min(state.fade.duration, state.fade.elapsed + dt);
629
- const alpha = elapsed / state.fade.duration;
630
- state.weight = lerp(state.fade.fromWeight, state.fade.toWeight, alpha);
631
- state.fade = elapsed >= state.fade.duration
632
- ? undefined
633
- : {
634
- ...state.fade,
635
- elapsed
636
- };
637
- }
638
-
639
- private removeFinishedFadeOuts(): void {
640
- for (const state of this.getInternalStates()) {
641
- if (state.fade || state.weight > 0 || state.targetWeight > 0) continue;
642
- this.states.delete(state.id);
643
- state.status = "stopped";
644
- this.dispatcher.emit("end", cloneState(state));
645
- }
646
- }
647
-
648
- private finishCrossfadeIfReady(): void {
649
- if (!this.pendingCrossfade) return;
650
- const hasActiveFade = this.getInternalStates().some((state) => state.fade);
651
- if (!hasActiveFade) {
652
- this.finishCrossfade();
653
- }
654
- }
655
-
656
- private finishCrossfade(): void {
657
- if (!this.pendingCrossfade) return;
658
- this.dispatcher.emit("crossFadeEnd", this.pendingCrossfade);
659
- this.dispatcher.emit("crossfadeEnd", this.pendingCrossfade);
660
- this.pendingCrossfade = undefined;
661
- }
662
-
663
- private blendStates(states: readonly InternalPlaybackState<TClipId, TEvent>[]): AnimationPose {
664
- const weightedStates = states.filter((state) => state.weight > 0);
665
- if (weightedStates.length === 0) {
666
- return emptyPose();
667
- }
668
-
669
- const accumulators = new Map<string, BoneAccumulator>();
670
- const morphTargets: Record<string, number> = {};
671
- let totalWeight = 0;
672
-
673
- for (const state of weightedStates) {
674
- const pose = this.sampleSinglePose(state.clip, state.localTime, state);
675
- const weight = state.weight;
676
- totalWeight += weight;
677
-
678
- for (const [boneName, transform] of Object.entries(pose.bones)) {
679
- const accumulator = accumulators.get(boneName) ?? createBoneAccumulator();
680
- accumulateTransform(accumulator, transform, weight);
681
- accumulators.set(boneName, accumulator);
682
- }
683
-
684
- for (const [name, value] of Object.entries(pose.morphTargets ?? {})) {
685
- morphTargets[name] = (morphTargets[name] ?? 0) + value * weight;
686
- }
687
- }
688
-
689
- const bones: Record<string, AnimationPoseTransform> = {};
690
- for (const [boneName, accumulator] of accumulators) {
691
- bones[boneName] = resolveAccumulator(accumulator);
692
- }
693
-
694
- if (totalWeight > 0) {
695
- for (const name of Object.keys(morphTargets)) {
696
- morphTargets[name] /= totalWeight;
697
- }
698
- }
699
-
700
- return {
701
- bones,
702
- morphTargets
703
- };
704
- }
705
-
706
- private sampleSinglePose(
707
- clip: RegisteredAnimationClip<TClipId, TEvent, AnimationPose>,
708
- time: number,
709
- state?: InternalPlaybackState<TClipId, TEvent>
710
- ): AnimationPose {
711
- if (!clip.sample) {
712
- return emptyPose();
713
- }
714
-
715
- const localTime = normalizeStateTime(time, clip.duration, clip.loop ? "loop" : "once");
716
- const pose = clip.sample({
717
- clip,
718
- time: localTime,
719
- normalizedTime: clip.duration > 0 ? localTime / clip.duration : 0,
720
- playbackState: state ? cloneState(state) : undefined
721
- });
722
-
723
- return isAnimationPose(pose) ? pose : emptyPose();
724
- }
725
-
726
- private selectStates(clipId?: TClipId): InternalPlaybackState<TClipId, TEvent>[] {
727
- const states = this.getInternalStates();
728
- if (!clipId) return states;
729
- return states.filter((state) => state.clipId === clipId);
730
- }
731
-
732
- private getInternalStates(): InternalPlaybackState<TClipId, TEvent>[] {
733
- return [...this.states.values()];
734
- }
735
-
736
- private findStateByClip(clipId: TClipId): InternalPlaybackState<TClipId, TEvent> | undefined {
737
- return this.getInternalStates().find((state) => state.clipId === clipId);
738
- }
739
-
740
- private primaryState(): InternalPlaybackState<TClipId, TEvent> | undefined {
741
- return this.getInternalStates().sort((a, b) => b.weight - a.weight)[0];
742
- }
743
-
744
- private activeClipId(): TClipId | undefined {
745
- return this.primaryState()?.clipId;
746
- }
747
-
748
- private emitStateChanged(): void {
749
- this.dispatcher.emit("stateChanged", this.snapshot());
750
- }
751
- }
752
-
753
- interface AdvanceResult {
754
- readonly completed: boolean;
755
- readonly loopsPassed: number;
756
- }
757
-
758
- interface BoneAccumulator {
759
- position?: MutableVector3;
760
- rotation?: MutableQuaternion;
761
- scale?: MutableVector3;
762
- weight: number;
763
- }
764
-
765
- interface MutableVector3 {
766
- x: number;
767
- y: number;
768
- z: number;
769
- }
770
-
771
- interface MutableQuaternion {
772
- x: number;
773
- y: number;
774
- z: number;
775
- w: number;
776
- }
777
-
778
- function advanceState<TClipId extends string, TEvent extends AnimationClipEvent>(
779
- state: InternalPlaybackState<TClipId, TEvent>,
780
- dt: number
781
- ): AdvanceResult {
782
- const previousPlayhead = state.playhead;
783
- const delta = dt * state.speed * state.inputDirection;
784
- state.previousLocalTime = state.localTime;
785
- state.playhead += delta;
786
-
787
- if (state.duration === 0) {
788
- state.localTime = 0;
789
- state.normalizedTime = 0;
790
- state.direction = state.inputDirection;
791
- return {
792
- completed: state.loopMode === "once",
793
- loopsPassed: 0
794
- };
795
- }
796
-
797
- const loopsPassed = state.loopMode === "once"
798
- ? 0
799
- : countBoundariesBetween(previousPlayhead, state.playhead, state.duration);
800
-
801
- if (state.loopMode === "once") {
802
- state.localTime = clamp(state.playhead, 0, state.duration);
803
- state.direction = state.inputDirection;
804
- state.normalizedTime = state.localTime / state.duration;
805
- return {
806
- completed: state.playhead <= 0 || state.playhead >= state.duration,
807
- loopsPassed: 0
808
- };
809
- }
810
-
811
- if (state.loopMode === "loop") {
812
- state.localTime = positiveModulo(state.playhead, state.duration);
813
- state.direction = state.inputDirection;
814
- } else {
815
- const previousLocalTime = state.localTime;
816
- state.localTime = pingPongTime(state.playhead, state.duration);
817
- state.direction = state.localTime >= previousLocalTime ? 1 : -1;
818
- }
819
-
820
- state.loopCount += loopsPassed;
821
- state.normalizedTime = state.localTime / state.duration;
822
-
823
- return {
824
- completed: false,
825
- loopsPassed
826
- };
827
- }
828
-
829
- function normalizeLoopMode(loop: AnimationLoopMode | boolean | undefined, clipLoops: boolean): AnimationLoopMode {
830
- if (loop === true) return "loop";
831
- if (loop === false) return "once";
832
- if (loop) return loop;
833
- return clipLoops ? "loop" : "once";
834
- }
835
-
836
- function normalizeStateTime(time: number, duration: number, loopMode: AnimationLoopMode): number {
837
- if (!Number.isFinite(time) || duration <= 0) return 0;
838
- if (loopMode === "once") return clamp(time, 0, duration);
839
- if (loopMode === "pingpong") return pingPongTime(time, duration);
840
- return positiveModulo(time, duration);
841
- }
842
-
843
- function cloneState<TClipId extends string>(
844
- state: AnimationClipPlaybackState<TClipId>
845
- ): AnimationClipPlaybackState<TClipId> {
846
- return {
847
- id: state.id,
848
- clipId: state.clipId,
849
- status: state.status,
850
- localTime: state.localTime,
851
- previousLocalTime: state.previousLocalTime,
852
- normalizedTime: state.normalizedTime,
853
- duration: state.duration,
854
- speed: state.speed,
855
- weight: state.weight,
856
- targetWeight: state.targetWeight,
857
- loopMode: state.loopMode,
858
- loopCount: state.loopCount,
859
- direction: state.direction,
860
- completed: state.completed,
861
- fade: state.fade
862
- ? {
863
- ...state.fade
864
- }
865
- : undefined
866
- };
867
- }
868
-
869
- function emptyPose(): AnimationPose {
870
- return {
871
- bones: {}
872
- };
873
- }
874
-
875
- function isAnimationPose(value: unknown): value is AnimationPose {
876
- if (!value || typeof value !== "object") return false;
877
- return "bones" in value && typeof (value as AnimationPose).bones === "object";
878
- }
879
-
880
- function createBoneAccumulator(): BoneAccumulator {
881
- return {
882
- weight: 0
883
- };
884
- }
885
-
886
- function accumulateTransform(
887
- accumulator: BoneAccumulator,
888
- transform: AnimationPoseTransform,
889
- weight: number
890
- ): void {
891
- accumulator.weight += weight;
892
-
893
- if (transform.position) {
894
- accumulator.position = addWeightedVector(accumulator.position, transform.position, weight);
895
- }
896
-
897
- if (transform.rotation) {
898
- accumulator.rotation = addWeightedQuaternion(accumulator.rotation, transform.rotation, weight);
899
- }
900
-
901
- if (transform.scale) {
902
- accumulator.scale = addWeightedVector(accumulator.scale, transform.scale, weight);
903
- }
904
- }
905
-
906
- function resolveAccumulator(accumulator: BoneAccumulator): AnimationPoseTransform {
907
- const divisor = accumulator.weight > 0 ? accumulator.weight : 1;
908
- return {
909
- position: accumulator.position ? divideVector(accumulator.position, divisor) : undefined,
910
- rotation: accumulator.rotation ? normalizeQuaternion(divideQuaternion(accumulator.rotation, divisor)) : undefined,
911
- scale: accumulator.scale ? divideVector(accumulator.scale, divisor) : undefined
912
- };
913
- }
914
-
915
- function addWeightedVector(
916
- current: MutableVector3 | undefined,
917
- value: AnimationVector3,
918
- weight: number
919
- ): MutableVector3 {
920
- return {
921
- x: (current?.x ?? 0) + value.x * weight,
922
- y: (current?.y ?? 0) + value.y * weight,
923
- z: (current?.z ?? 0) + value.z * weight
924
- };
925
- }
926
-
927
- function addWeightedQuaternion(
928
- current: MutableQuaternion | undefined,
929
- value: AnimationQuaternion,
930
- weight: number
931
- ): MutableQuaternion {
932
- return {
933
- x: (current?.x ?? 0) + value.x * weight,
934
- y: (current?.y ?? 0) + value.y * weight,
935
- z: (current?.z ?? 0) + value.z * weight,
936
- w: (current?.w ?? 0) + value.w * weight
937
- };
938
- }
939
-
940
- function divideVector(value: MutableVector3, divisor: number): AnimationVector3 {
941
- return {
942
- x: value.x / divisor,
943
- y: value.y / divisor,
944
- z: value.z / divisor
945
- };
946
- }
947
-
948
- function divideQuaternion(value: MutableQuaternion, divisor: number): AnimationQuaternion {
949
- return {
950
- x: value.x / divisor,
951
- y: value.y / divisor,
952
- z: value.z / divisor,
953
- w: value.w / divisor
954
- };
955
- }
956
-
957
- function normalizeQuaternion(value: AnimationQuaternion): AnimationQuaternion {
958
- const length = Math.hypot(value.x, value.y, value.z, value.w);
959
- if (length === 0) {
960
- return {
961
- x: 0,
962
- y: 0,
963
- z: 0,
964
- w: 1
965
- };
966
- }
967
-
968
- return {
969
- x: value.x / length,
970
- y: value.y / length,
971
- z: value.z / length,
972
- w: value.w / length
973
- };
974
- }
975
-
976
- function countBoundariesBetween(from: number, to: number, interval: number): number {
977
- if (interval <= 0 || from === to) return 0;
978
- const low = Math.min(from, to);
979
- const high = Math.max(from, to);
980
- const first = Math.floor(low / interval) + 1;
981
- const last = Math.floor(high / interval);
982
- return Math.max(0, last - first + 1);
983
- }
984
-
985
- function pingPongTime(time: number, duration: number): number {
986
- if (duration <= 0) return 0;
987
- const period = duration * 2;
988
- const wrapped = positiveModulo(time, period);
989
- return wrapped <= duration ? wrapped : period - wrapped;
990
- }
991
-
992
- function sanitizeSpeed(speed: number): number {
993
- return Number.isFinite(speed) ? speed : 1;
994
- }
995
-
996
- function sanitizeWeight(weight: number): number {
997
- if (!Number.isFinite(weight)) return 1;
998
- return Math.max(0, weight);
999
- }
1000
-
1001
- function sanitizeDuration(duration: number): number {
1002
- if (!Number.isFinite(duration) || duration < 0) return 0;
1003
- return duration;
1004
- }
1005
-
1006
- function lerp(from: number, to: number, alpha: number): number {
1007
- return from + (to - from) * clamp(alpha, 0, 1);
1008
- }
1009
-
1010
- function positiveModulo(value: number, divisor: number): number {
1011
- if (divisor <= 0) return 0;
1012
- return ((value % divisor) + divisor) % divisor;
1013
- }
1014
-
1015
- function clamp(value: number, min: number, max: number): number {
1016
- return Math.min(max, Math.max(min, value));
1017
- }