@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
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aura3d/animation",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@aura3d/math": "1.0.6"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { AnimationClip } from "./AnimationClip.js";
|
|
2
|
+
import { eventsBetween, type AnimationEvent } from "./AnimationEvents.js";
|
|
3
|
+
|
|
4
|
+
export type LoopMode = "once" | "repeat" | "pingpong";
|
|
5
|
+
|
|
6
|
+
export type AnimationActionSnapshot = {
|
|
7
|
+
readonly clipName: string;
|
|
8
|
+
readonly duration: number;
|
|
9
|
+
readonly time: number;
|
|
10
|
+
readonly weight: number;
|
|
11
|
+
readonly timeScale: number;
|
|
12
|
+
readonly playing: boolean;
|
|
13
|
+
readonly paused: boolean;
|
|
14
|
+
readonly loopMode: LoopMode;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class AnimationAction {
|
|
18
|
+
readonly clip: AnimationClip;
|
|
19
|
+
time = 0;
|
|
20
|
+
weight = 1;
|
|
21
|
+
timeScale = 1;
|
|
22
|
+
loopMode: LoopMode = "repeat";
|
|
23
|
+
playing = false;
|
|
24
|
+
paused = false;
|
|
25
|
+
private pingPongForward = true;
|
|
26
|
+
private fadeDuration = 0;
|
|
27
|
+
private fadeElapsed = 0;
|
|
28
|
+
private fadeStartWeight = 1;
|
|
29
|
+
private fadeTargetWeight = 1;
|
|
30
|
+
|
|
31
|
+
constructor(clip: AnimationClip) {
|
|
32
|
+
this.clip = clip;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
play(): this {
|
|
36
|
+
this.playing = true;
|
|
37
|
+
this.paused = false;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pause(): this {
|
|
42
|
+
this.paused = true;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
stop(): this {
|
|
47
|
+
this.playing = false;
|
|
48
|
+
this.paused = false;
|
|
49
|
+
this.time = 0;
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setWeight(weight: number): this {
|
|
54
|
+
if (!Number.isFinite(weight) || weight < 0) {
|
|
55
|
+
throw new Error("AnimationAction weight must be finite and non-negative.");
|
|
56
|
+
}
|
|
57
|
+
this.weight = weight;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setTimeScale(timeScale: number): this {
|
|
62
|
+
if (!Number.isFinite(timeScale) || timeScale < 0) {
|
|
63
|
+
throw new Error("AnimationAction timeScale must be finite and non-negative.");
|
|
64
|
+
}
|
|
65
|
+
this.timeScale = timeScale;
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setLoop(loopMode: LoopMode): this {
|
|
70
|
+
this.loopMode = loopMode;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
reset(): this {
|
|
75
|
+
this.time = 0;
|
|
76
|
+
this.pingPongForward = true;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
seek(time: number): this {
|
|
81
|
+
if (!Number.isFinite(time) || time < 0) {
|
|
82
|
+
throw new Error("AnimationAction seek time must be finite and non-negative.");
|
|
83
|
+
}
|
|
84
|
+
this.time = this.clip.duration > 0 ? Math.min(time, this.clip.duration) : 0;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fadeTo(weight: number, duration: number): this {
|
|
89
|
+
if (!Number.isFinite(duration) || duration < 0 || !Number.isFinite(weight) || weight < 0) {
|
|
90
|
+
throw new Error("AnimationAction fade target and duration must be valid.");
|
|
91
|
+
}
|
|
92
|
+
this.fadeStartWeight = this.weight;
|
|
93
|
+
this.fadeTargetWeight = weight;
|
|
94
|
+
this.fadeDuration = duration;
|
|
95
|
+
this.fadeElapsed = 0;
|
|
96
|
+
if (duration === 0) {
|
|
97
|
+
this.weight = weight;
|
|
98
|
+
}
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
update(delta: number): readonly AnimationEvent[] {
|
|
103
|
+
if (!Number.isFinite(delta) || delta < 0) {
|
|
104
|
+
throw new Error("AnimationAction delta must be finite and non-negative.");
|
|
105
|
+
}
|
|
106
|
+
if (this.fadeDuration > 0) {
|
|
107
|
+
this.fadeElapsed = Math.min(this.fadeDuration, this.fadeElapsed + delta);
|
|
108
|
+
const t = this.fadeElapsed / this.fadeDuration;
|
|
109
|
+
this.weight = this.fadeStartWeight + (this.fadeTargetWeight - this.fadeStartWeight) * t;
|
|
110
|
+
if (this.fadeElapsed === this.fadeDuration) {
|
|
111
|
+
this.fadeDuration = 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!this.playing || this.paused || this.clip.duration === 0) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const previous = this.time;
|
|
118
|
+
const scaledDelta = delta * this.timeScale;
|
|
119
|
+
let next = previous + scaledDelta;
|
|
120
|
+
let looped = false;
|
|
121
|
+
if (this.loopMode === "repeat") {
|
|
122
|
+
while (next > this.clip.duration) {
|
|
123
|
+
next -= this.clip.duration;
|
|
124
|
+
looped = true;
|
|
125
|
+
}
|
|
126
|
+
} else if (this.loopMode === "once") {
|
|
127
|
+
next = Math.min(next, this.clip.duration);
|
|
128
|
+
if (next === this.clip.duration) {
|
|
129
|
+
this.playing = false;
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
if (this.pingPongForward) {
|
|
133
|
+
while (next > this.clip.duration) {
|
|
134
|
+
next = this.clip.duration - (next - this.clip.duration);
|
|
135
|
+
this.pingPongForward = false;
|
|
136
|
+
looped = true;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
next = previous - scaledDelta;
|
|
140
|
+
while (next < 0) {
|
|
141
|
+
next = -next;
|
|
142
|
+
this.pingPongForward = true;
|
|
143
|
+
looped = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
this.time = next;
|
|
148
|
+
if (this.clip.events.length === 0) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
return eventsBetween(this.clip.events, this.clip.name, previous, next, this.clip.duration, looped);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
snapshot(): AnimationActionSnapshot {
|
|
155
|
+
return {
|
|
156
|
+
clipName: this.clip.name,
|
|
157
|
+
duration: this.clip.duration,
|
|
158
|
+
time: this.time,
|
|
159
|
+
weight: this.weight,
|
|
160
|
+
timeScale: this.timeScale,
|
|
161
|
+
playing: this.playing,
|
|
162
|
+
paused: this.paused,
|
|
163
|
+
loopMode: this.loopMode
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AnimationEventMarker } from "./AnimationEvents.js";
|
|
2
|
+
import { AnimationTrack, type SerializedAnimationTrack } from "./AnimationTrack.js";
|
|
3
|
+
|
|
4
|
+
export type AnimationClipDescriptor = {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly duration?: number;
|
|
7
|
+
readonly tracks: readonly AnimationTrack[];
|
|
8
|
+
readonly events?: readonly AnimationEventMarker[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SerializedAnimationClip = {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly duration: number;
|
|
14
|
+
readonly tracks: readonly SerializedAnimationTrack[];
|
|
15
|
+
readonly events: readonly AnimationEventMarker[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class AnimationClip {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly tracks: readonly AnimationTrack[];
|
|
21
|
+
readonly events: readonly AnimationEventMarker[];
|
|
22
|
+
readonly duration: number;
|
|
23
|
+
|
|
24
|
+
constructor(descriptor: AnimationClipDescriptor) {
|
|
25
|
+
if (descriptor.name.trim().length === 0) {
|
|
26
|
+
throw new Error("AnimationClip name cannot be empty.");
|
|
27
|
+
}
|
|
28
|
+
this.name = descriptor.name;
|
|
29
|
+
this.tracks = [...descriptor.tracks];
|
|
30
|
+
this.events = [...(descriptor.events ?? [])].sort((a, b) => a.time - b.time || a.name.localeCompare(b.name));
|
|
31
|
+
const trackDuration = this.tracks.reduce((duration, track) => Math.max(duration, track.duration), 0);
|
|
32
|
+
this.duration = descriptor.duration ?? trackDuration;
|
|
33
|
+
if (!Number.isFinite(this.duration) || this.duration < 0) {
|
|
34
|
+
throw new Error("AnimationClip duration must be finite and non-negative.");
|
|
35
|
+
}
|
|
36
|
+
if (this.duration < trackDuration) {
|
|
37
|
+
throw new Error("AnimationClip duration cannot be shorter than its tracks.");
|
|
38
|
+
}
|
|
39
|
+
for (const event of this.events) {
|
|
40
|
+
if (!Number.isFinite(event.time) || event.time < 0 || event.time > this.duration) {
|
|
41
|
+
throw new Error(`Animation event ${event.name} is outside clip duration.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
toJSON(): SerializedAnimationClip {
|
|
47
|
+
return {
|
|
48
|
+
name: this.name,
|
|
49
|
+
duration: this.duration,
|
|
50
|
+
tracks: this.tracks.map((track) => track.toJSON()),
|
|
51
|
+
events: this.events.map((event) => ({ ...event }))
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static fromJSON(serialized: SerializedAnimationClip): AnimationClip {
|
|
56
|
+
return new AnimationClip({
|
|
57
|
+
name: serialized.name,
|
|
58
|
+
duration: serialized.duration,
|
|
59
|
+
tracks: serialized.tracks.map((track) => AnimationTrack.fromJSON(track)),
|
|
60
|
+
events: serialized.events
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
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
|
+
}
|