@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.
Files changed (187) hide show
  1. package/README.md +19 -0
  2. package/dist/AnimationAction.d.ts +40 -0
  3. package/dist/AnimationAction.d.ts.map +1 -0
  4. package/dist/AnimationAction.js +143 -0
  5. package/dist/AnimationAction.js.map +1 -0
  6. package/dist/AnimationClip.d.ts +24 -0
  7. package/dist/AnimationClip.d.ts.map +1 -0
  8. package/dist/AnimationClip.js +45 -0
  9. package/dist/AnimationClip.js.map +1 -0
  10. package/dist/AnimationClipEvents.d.ts +57 -0
  11. package/dist/AnimationClipEvents.d.ts.map +1 -0
  12. package/dist/AnimationClipEvents.js +171 -0
  13. package/dist/AnimationClipEvents.js.map +1 -0
  14. package/dist/AnimationClipRegistry.d.ts +76 -0
  15. package/dist/AnimationClipRegistry.d.ts.map +1 -0
  16. package/dist/AnimationClipRegistry.js +130 -0
  17. package/dist/AnimationClipRegistry.js.map +1 -0
  18. package/dist/AnimationController.d.ts +168 -0
  19. package/dist/AnimationController.d.ts.map +1 -0
  20. package/dist/AnimationController.js +619 -0
  21. package/dist/AnimationController.js.map +1 -0
  22. package/dist/AnimationEvents.d.ts +10 -0
  23. package/dist/AnimationEvents.d.ts.map +1 -0
  24. package/dist/AnimationEvents.js +26 -0
  25. package/dist/AnimationEvents.js.map +1 -0
  26. package/dist/AnimationLayer.d.ts +27 -0
  27. package/dist/AnimationLayer.d.ts.map +1 -0
  28. package/dist/AnimationLayer.js +53 -0
  29. package/dist/AnimationLayer.js.map +1 -0
  30. package/dist/AnimationMixer.d.ts +54 -0
  31. package/dist/AnimationMixer.d.ts.map +1 -0
  32. package/dist/AnimationMixer.js +319 -0
  33. package/dist/AnimationMixer.js.map +1 -0
  34. package/dist/AnimationStateGraph.d.ts +2 -0
  35. package/dist/AnimationStateGraph.d.ts.map +1 -0
  36. package/dist/AnimationStateGraph.js +2 -0
  37. package/dist/AnimationStateGraph.js.map +1 -0
  38. package/dist/AnimationStateMachine.d.ts +59 -0
  39. package/dist/AnimationStateMachine.d.ts.map +1 -0
  40. package/dist/AnimationStateMachine.js +154 -0
  41. package/dist/AnimationStateMachine.js.map +1 -0
  42. package/dist/AnimationTrack.d.ts +23 -0
  43. package/dist/AnimationTrack.d.ts.map +1 -0
  44. package/dist/AnimationTrack.js +124 -0
  45. package/dist/AnimationTrack.js.map +1 -0
  46. package/dist/BlendTree.d.ts +23 -0
  47. package/dist/BlendTree.d.ts.map +1 -0
  48. package/dist/BlendTree.js +68 -0
  49. package/dist/BlendTree.js.map +1 -0
  50. package/dist/Bone.d.ts +20 -0
  51. package/dist/Bone.d.ts.map +1 -0
  52. package/dist/Bone.js +24 -0
  53. package/dist/Bone.js.map +1 -0
  54. package/dist/CrowdAnimation.d.ts +13 -0
  55. package/dist/CrowdAnimation.d.ts.map +1 -0
  56. package/dist/CrowdAnimation.js +14 -0
  57. package/dist/CrowdAnimation.js.map +1 -0
  58. package/dist/ECSAnimationBridge.d.ts +9 -0
  59. package/dist/ECSAnimationBridge.d.ts.map +1 -0
  60. package/dist/ECSAnimationBridge.js +24 -0
  61. package/dist/ECSAnimationBridge.js.map +1 -0
  62. package/dist/HumanoidRetargeting.d.ts +76 -0
  63. package/dist/HumanoidRetargeting.d.ts.map +1 -0
  64. package/dist/HumanoidRetargeting.js +331 -0
  65. package/dist/HumanoidRetargeting.js.map +1 -0
  66. package/dist/IK.d.ts +24 -0
  67. package/dist/IK.d.ts.map +1 -0
  68. package/dist/IK.js +76 -0
  69. package/dist/IK.js.map +1 -0
  70. package/dist/Keyframe.d.ts +50 -0
  71. package/dist/Keyframe.d.ts.map +1 -0
  72. package/dist/Keyframe.js +118 -0
  73. package/dist/Keyframe.js.map +1 -0
  74. package/dist/LocomotionController.d.ts +47 -0
  75. package/dist/LocomotionController.d.ts.map +1 -0
  76. package/dist/LocomotionController.js +106 -0
  77. package/dist/LocomotionController.js.map +1 -0
  78. package/dist/MotionMatchingFixtures.d.ts +58 -0
  79. package/dist/MotionMatchingFixtures.d.ts.map +1 -0
  80. package/dist/MotionMatchingFixtures.js +175 -0
  81. package/dist/MotionMatchingFixtures.js.map +1 -0
  82. package/dist/MotionQuality.d.ts +36 -0
  83. package/dist/MotionQuality.d.ts.map +1 -0
  84. package/dist/MotionQuality.js +117 -0
  85. package/dist/MotionQuality.js.map +1 -0
  86. package/dist/Retargeting.d.ts +12 -0
  87. package/dist/Retargeting.d.ts.map +1 -0
  88. package/dist/Retargeting.js +19 -0
  89. package/dist/Retargeting.js.map +1 -0
  90. package/dist/RootMotion.d.ts +21 -0
  91. package/dist/RootMotion.d.ts.map +1 -0
  92. package/dist/RootMotion.js +84 -0
  93. package/dist/RootMotion.js.map +1 -0
  94. package/dist/SceneAnimationBridge.d.ts +18 -0
  95. package/dist/SceneAnimationBridge.d.ts.map +1 -0
  96. package/dist/SceneAnimationBridge.js +69 -0
  97. package/dist/SceneAnimationBridge.js.map +1 -0
  98. package/dist/SecondaryAnimationFixtures.d.ts +55 -0
  99. package/dist/SecondaryAnimationFixtures.d.ts.map +1 -0
  100. package/dist/SecondaryAnimationFixtures.js +147 -0
  101. package/dist/SecondaryAnimationFixtures.js.map +1 -0
  102. package/dist/Skeleton.d.ts +9 -0
  103. package/dist/Skeleton.d.ts.map +1 -0
  104. package/dist/Skeleton.js +31 -0
  105. package/dist/Skeleton.js.map +1 -0
  106. package/dist/Skinning.d.ts +7 -0
  107. package/dist/Skinning.d.ts.map +1 -0
  108. package/dist/Skinning.js +12 -0
  109. package/dist/Skinning.js.map +1 -0
  110. package/dist/browser-index.d.ts +39 -0
  111. package/dist/browser-index.d.ts.map +1 -0
  112. package/dist/browser-index.js +34 -0
  113. package/dist/browser-index.js.map +1 -0
  114. package/dist/index.d.ts +40 -0
  115. package/dist/index.d.ts.map +1 -0
  116. package/dist/index.js +35 -0
  117. package/dist/index.js.map +1 -0
  118. package/dist/threejs-compatibility/AnimationAction.d.ts +16 -0
  119. package/dist/threejs-compatibility/AnimationAction.d.ts.map +1 -0
  120. package/dist/threejs-compatibility/AnimationAction.js +25 -0
  121. package/dist/threejs-compatibility/AnimationAction.js.map +1 -0
  122. package/dist/threejs-compatibility/AnimationClip.d.ts +14 -0
  123. package/dist/threejs-compatibility/AnimationClip.d.ts.map +1 -0
  124. package/dist/threejs-compatibility/AnimationClip.js +11 -0
  125. package/dist/threejs-compatibility/AnimationClip.js.map +1 -0
  126. package/dist/threejs-compatibility/AnimationDiagnostics.d.ts +44 -0
  127. package/dist/threejs-compatibility/AnimationDiagnostics.d.ts.map +1 -0
  128. package/dist/threejs-compatibility/AnimationDiagnostics.js +29 -0
  129. package/dist/threejs-compatibility/AnimationDiagnostics.js.map +1 -0
  130. package/dist/threejs-compatibility/AnimationMixer.d.ts +9 -0
  131. package/dist/threejs-compatibility/AnimationMixer.d.ts.map +1 -0
  132. package/dist/threejs-compatibility/AnimationMixer.js +24 -0
  133. package/dist/threejs-compatibility/AnimationMixer.js.map +1 -0
  134. package/dist/threejs-compatibility/MorphTargetMixer.d.ts +10 -0
  135. package/dist/threejs-compatibility/MorphTargetMixer.d.ts.map +1 -0
  136. package/dist/threejs-compatibility/MorphTargetMixer.js +10 -0
  137. package/dist/threejs-compatibility/MorphTargetMixer.js.map +1 -0
  138. package/dist/threejs-compatibility/Skeleton.d.ts +10 -0
  139. package/dist/threejs-compatibility/Skeleton.d.ts.map +1 -0
  140. package/dist/threejs-compatibility/Skeleton.js +8 -0
  141. package/dist/threejs-compatibility/Skeleton.js.map +1 -0
  142. package/dist/threejs-compatibility/SkinnedMesh.d.ts +11 -0
  143. package/dist/threejs-compatibility/SkinnedMesh.d.ts.map +1 -0
  144. package/dist/threejs-compatibility/SkinnedMesh.js +16 -0
  145. package/dist/threejs-compatibility/SkinnedMesh.js.map +1 -0
  146. package/dist/threejs-compatibility/index.d.ts +12 -0
  147. package/dist/threejs-compatibility/index.d.ts.map +1 -0
  148. package/dist/threejs-compatibility/index.js +8 -0
  149. package/dist/threejs-compatibility/index.js.map +1 -0
  150. package/package.json +17 -0
  151. package/src/AnimationAction.ts +166 -0
  152. package/src/AnimationClip.ts +63 -0
  153. package/src/AnimationClipEvents.ts +320 -0
  154. package/src/AnimationClipRegistry.ts +280 -0
  155. package/src/AnimationController.ts +967 -0
  156. package/src/AnimationEvents.ts +34 -0
  157. package/src/AnimationLayer.ts +73 -0
  158. package/src/AnimationMixer.ts +366 -0
  159. package/src/AnimationStateGraph.ts +8 -0
  160. package/src/AnimationStateMachine.ts +217 -0
  161. package/src/AnimationTrack.ts +176 -0
  162. package/src/BlendTree.ts +89 -0
  163. package/src/Bone.ts +35 -0
  164. package/src/CrowdAnimation.ts +25 -0
  165. package/src/ECSAnimationBridge.ts +30 -0
  166. package/src/HumanoidRetargeting.ts +476 -0
  167. package/src/IK.ts +107 -0
  168. package/src/Keyframe.ts +159 -0
  169. package/src/LocomotionController.ts +149 -0
  170. package/src/MotionMatchingFixtures.ts +254 -0
  171. package/src/MotionQuality.ts +156 -0
  172. package/src/Retargeting.ts +29 -0
  173. package/src/RootMotion.ts +124 -0
  174. package/src/SceneAnimationBridge.ts +80 -0
  175. package/src/SecondaryAnimationFixtures.ts +212 -0
  176. package/src/Skeleton.ts +34 -0
  177. package/src/Skinning.ts +18 -0
  178. package/src/browser-index.ts +55 -0
  179. package/src/index.ts +56 -0
  180. package/src/threejs-compatibility/AnimationAction.ts +27 -0
  181. package/src/threejs-compatibility/AnimationClip.ts +16 -0
  182. package/src/threejs-compatibility/AnimationDiagnostics.ts +42 -0
  183. package/src/threejs-compatibility/AnimationMixer.ts +24 -0
  184. package/src/threejs-compatibility/MorphTargetMixer.ts +16 -0
  185. package/src/threejs-compatibility/Skeleton.ts +9 -0
  186. package/src/threejs-compatibility/SkinnedMesh.ts +12 -0
  187. 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
+ }