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