@aura3d/animation 1.3.3 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/package.json +36 -3
  2. package/src/AnimationAction.ts +0 -166
  3. package/src/AnimationClip.ts +0 -63
  4. package/src/AnimationClipEvents.ts +0 -501
  5. package/src/AnimationClipRegistry.ts +0 -404
  6. package/src/AnimationController.ts +0 -1017
  7. package/src/AnimationEvents.ts +0 -34
  8. package/src/AnimationLayer.ts +0 -73
  9. package/src/AnimationMixer.ts +0 -402
  10. package/src/AnimationStateGraph.ts +0 -105
  11. package/src/AnimationStateMachine.ts +0 -265
  12. package/src/AnimationTrack.ts +0 -176
  13. package/src/BlendTree.ts +0 -89
  14. package/src/Bone.ts +0 -40
  15. package/src/CrowdAnimation.ts +0 -25
  16. package/src/ECSAnimationBridge.ts +0 -30
  17. package/src/FighterAnimationAdapter.ts +0 -124
  18. package/src/FootIk.ts +0 -251
  19. package/src/HumanoidBoneInference.ts +0 -323
  20. package/src/HumanoidRetargeting.ts +0 -700
  21. package/src/IK.ts +0 -117
  22. package/src/Inertialization.ts +0 -269
  23. package/src/Keyframe.ts +0 -159
  24. package/src/LocomotionController.ts +0 -155
  25. package/src/LocomotionKit.ts +0 -89
  26. package/src/MotionMatchingFixtures.ts +0 -254
  27. package/src/MotionQuality.ts +0 -182
  28. package/src/RigQuality.ts +0 -101
  29. package/src/RootMotion.ts +0 -124
  30. package/src/SceneAnimationBridge.ts +0 -80
  31. package/src/SecondaryAnimationFixtures.ts +0 -210
  32. package/src/Skeleton.ts +0 -78
  33. package/src/Skinning.ts +0 -18
  34. package/src/SpringBones.ts +0 -243
  35. package/src/browser-index.ts +0 -66
  36. package/src/index.ts +0 -66
  37. package/src/library/performanceStateGraph.ts +0 -173
  38. package/src/library/standardHumanoidClips.ts +0 -659
  39. package/src/threejs-compatibility/AnimationAction.ts +0 -27
  40. package/src/threejs-compatibility/AnimationClip.ts +0 -16
  41. package/src/threejs-compatibility/AnimationDiagnostics.ts +0 -42
  42. package/src/threejs-compatibility/AnimationMixer.ts +0 -24
  43. package/src/threejs-compatibility/MorphTargetMixer.ts +0 -16
  44. package/src/threejs-compatibility/Skeleton.ts +0 -9
  45. package/src/threejs-compatibility/SkinnedMesh.ts +0 -12
  46. package/src/threejs-compatibility/index.ts +0 -11
@@ -1,34 +0,0 @@
1
- export type AnimationEventMarker = {
2
- readonly name: string;
3
- readonly time: number;
4
- readonly payload?: unknown;
5
- };
6
-
7
- export type AnimationEvent = AnimationEventMarker & {
8
- readonly clipName: string;
9
- };
10
-
11
- export function eventsBetween(markers: readonly AnimationEventMarker[], clipName: string, fromTime: number, toTime: number, duration: number, looped: boolean): readonly AnimationEvent[] {
12
- const events: AnimationEvent[] = [];
13
- if (duration <= 0) {
14
- return events;
15
- }
16
- if (!looped || toTime >= fromTime) {
17
- for (const marker of markers) {
18
- if (marker.time > fromTime && marker.time <= toTime) {
19
- events.push({ ...marker, clipName });
20
- }
21
- }
22
- return events;
23
- }
24
- const lateEvents: AnimationEvent[] = [];
25
- const earlyEvents: AnimationEvent[] = [];
26
- for (const marker of markers) {
27
- if (marker.time > fromTime && marker.time <= duration) {
28
- lateEvents.push({ ...marker, clipName });
29
- } else if (marker.time >= 0 && marker.time <= toTime) {
30
- earlyEvents.push({ ...marker, clipName });
31
- }
32
- }
33
- return [...lateEvents, ...earlyEvents];
34
- }
@@ -1,73 +0,0 @@
1
- import type { AnimationAction } from "./AnimationAction.js";
2
-
3
- export type AnimationLayerSnapshot = {
4
- readonly name: string;
5
- readonly weight: number;
6
- readonly additive: boolean;
7
- readonly mask: readonly string[];
8
- readonly actions: readonly string[];
9
- };
10
-
11
- export type AnimationLayerOptions = {
12
- readonly weight?: number;
13
- readonly additive?: boolean;
14
- readonly mask?: readonly string[];
15
- };
16
-
17
- export class AnimationLayer {
18
- readonly name: string;
19
- weight: number;
20
- additive: boolean;
21
- readonly mask: readonly string[];
22
- readonly actions: AnimationAction[] = [];
23
- private readonly baseWeights = new WeakMap<AnimationAction, number>();
24
-
25
- constructor(name: string, weightOrOptions: number | AnimationLayerOptions = 1) {
26
- if (name.trim().length === 0) {
27
- throw new Error("AnimationLayer name cannot be empty.");
28
- }
29
- this.name = name;
30
- const options = typeof weightOrOptions === "number" ? { weight: weightOrOptions } : weightOrOptions;
31
- this.weight = options.weight ?? 1;
32
- this.additive = options.additive ?? false;
33
- this.mask = [...(options.mask ?? [])];
34
- if (!Number.isFinite(this.weight) || this.weight < 0) {
35
- throw new Error("AnimationLayer weight must be finite and non-negative.");
36
- }
37
- for (const entry of this.mask) {
38
- if (entry.trim().length === 0) {
39
- throw new Error("AnimationLayer mask entries cannot be empty.");
40
- }
41
- }
42
- }
43
-
44
- add(action: AnimationAction): void {
45
- if (!this.actions.includes(action)) {
46
- this.actions.push(action);
47
- this.baseWeights.set(action, action.weight);
48
- }
49
- }
50
-
51
- applyWeight(): void {
52
- for (const action of this.actions) {
53
- action.setWeight((this.baseWeights.get(action) ?? action.weight) * this.weight);
54
- }
55
- }
56
-
57
- capturesTarget(target: string): boolean {
58
- if (this.mask.length === 0) {
59
- return true;
60
- }
61
- return this.mask.some((entry) => target === entry || target.startsWith(`${entry}.`));
62
- }
63
-
64
- snapshot(): AnimationLayerSnapshot {
65
- return {
66
- name: this.name,
67
- weight: this.weight,
68
- additive: this.additive,
69
- mask: [...this.mask],
70
- actions: this.actions.map((action) => action.clip.name)
71
- };
72
- }
73
- }
@@ -1,402 +0,0 @@
1
- import { AnimationAction, type AnimationActionSnapshot } from "./AnimationAction.js";
2
- import type { AnimationClip } from "./AnimationClip.js";
3
- import type { AnimationEvent } from "./AnimationEvents.js";
4
- import type { AnimationLayer, AnimationLayerSnapshot } from "./AnimationLayer.js";
5
- import { cloneAnimationValue, normalizeQuat, slerpQuat, type AnimationValue } from "./Keyframe.js";
6
- import { DEFAULT_INERTIALIZATION_HALF_LIFE, inertializedTransitionWeight } from "./Inertialization.js";
7
- import { applyRootMotion, extractRootMotion, type RootMotionSample, type RootMotionTarget } from "./RootMotion.js";
8
-
9
- export type AnimationTarget = {
10
- setAnimationValue?: (target: string, value: AnimationValue) => void;
11
- applyRootMotion?: (sample: RootMotionSample) => void;
12
- position?: [number, number, number];
13
- };
14
-
15
- export type AnimationMixerOptions = {
16
- readonly applyRootMotion?: boolean;
17
- readonly rootMotionTrack?: string;
18
- readonly rootMotionScale?: number;
19
- };
20
-
21
- export type AnimationMixerSnapshot = {
22
- readonly timeScale: number;
23
- readonly actionCount: number;
24
- readonly actions: readonly AnimationActionSnapshot[];
25
- readonly layers: readonly AnimationLayerSnapshot[];
26
- readonly values: Readonly<Record<string, AnimationValue>>;
27
- readonly applyErrors: readonly AnimationApplyError[];
28
- };
29
-
30
- export type AnimationApplyError = {
31
- readonly target: string;
32
- readonly message: string;
33
- };
34
-
35
- type MixerLayer = Pick<AnimationLayer, "name" | "weight" | "additive" | "mask" | "actions" | "capturesTarget" | "snapshot">;
36
- type WeightedAccumulator = { value: AnimationValue; weight: number; type: string };
37
- type TargetAccumulator = { type: string; base?: WeightedAccumulator; additive?: AnimationValue };
38
-
39
- export class AnimationMixer {
40
- readonly target: AnimationTarget | undefined;
41
- timeScale = 1;
42
- private readonly actions: AnimationAction[] = [];
43
- private readonly layers: AnimationLayer[] = [];
44
- private readonly values = new Map<string, AnimationValue>();
45
- private applyErrors: AnimationApplyError[] = [];
46
- private readonly eventListeners = new Set<(event: AnimationEvent) => void>();
47
- private disposed = false;
48
- private inertialTransition: { from: AnimationAction; to: AnimationAction; elapsed: number; halfLife: number } | undefined;
49
-
50
- constructor(target?: AnimationTarget, private readonly options: AnimationMixerOptions = {}) {
51
- this.target = target;
52
- }
53
-
54
- play(clip: AnimationClip): AnimationAction {
55
- this.assertAlive();
56
- const action = new AnimationAction(clip).play();
57
- this.actions.push(action);
58
- return action;
59
- }
60
-
61
- addAction(action: AnimationAction): void {
62
- this.assertAlive();
63
- if (!this.actions.includes(action)) {
64
- this.actions.push(action);
65
- }
66
- }
67
-
68
- addLayer(layer: AnimationLayer): void {
69
- this.assertAlive();
70
- if (!this.layers.includes(layer)) {
71
- this.layers.push(layer);
72
- }
73
- }
74
-
75
- stopAll(): void {
76
- this.assertAlive();
77
- for (const action of this.actions) {
78
- action.stop();
79
- }
80
- }
81
-
82
- crossFade(from: AnimationAction, to: AnimationAction, duration: number): void {
83
- this.assertAlive();
84
- from.fadeTo(0, duration);
85
- to.play().fadeTo(1, duration);
86
- this.addAction(to);
87
- }
88
-
89
- /**
90
- * Inertialized transition between two actions: instead of the linear weight ramp of
91
- * {@link crossFade}, the source action's weight follows a critically-damped decay (zero initial
92
- * slope, momentum-preserving) so the blend carries through instead of dissolving. The transition
93
- * is advanced deterministically inside {@link update}; weights always sum to 1 and the source is
94
- * stopped once it has effectively faded. Pass a `halfLife` (seconds) to tune snappiness.
95
- */
96
- inertialCrossFade(from: AnimationAction, to: AnimationAction, halfLife = DEFAULT_INERTIALIZATION_HALF_LIFE): void {
97
- this.assertAlive();
98
- to.play();
99
- from.weight = 1;
100
- to.weight = 0;
101
- this.addAction(to);
102
- this.inertialTransition = { from, to, elapsed: 0, halfLife };
103
- }
104
-
105
- update(delta: number): readonly AnimationEvent[] {
106
- this.assertAlive();
107
- if (!Number.isFinite(delta) || delta < 0) {
108
- throw new Error("AnimationMixer delta must be finite and non-negative.");
109
- }
110
- this.advanceInertialTransition(delta * this.timeScale);
111
- const events: AnimationEvent[] = [];
112
- const accumulators = new Map<string, TargetAccumulator>();
113
- for (const action of this.actions) {
114
- const previousTime = action.time;
115
- const actionEvents = action.update(delta * this.timeScale);
116
- if (actionEvents.length > 0) {
117
- events.push(...actionEvents);
118
- }
119
- if (!action.playing && action.weight <= 0) {
120
- continue;
121
- }
122
- this.applyActionRootMotion(action, previousTime);
123
- for (const track of action.clip.tracks) {
124
- const sample = track.sample(action.time);
125
- for (const layer of this.layersForAction(action)) {
126
- if (layer.capturesTarget(track.target)) {
127
- blendInto(accumulators, track.target, track.valueType, sample, action.weight * layer.weight, layer.additive);
128
- }
129
- }
130
- }
131
- }
132
- this.values.clear();
133
- this.applyErrors = [];
134
- for (const [target, accumulator] of accumulators) {
135
- const value = finalizeTargetBlend(accumulator);
136
- this.values.set(target, value);
137
- try {
138
- this.target?.setAnimationValue?.(target, value);
139
- } catch (error) {
140
- this.applyErrors.push({
141
- target,
142
- message: error instanceof Error ? error.message : String(error)
143
- });
144
- }
145
- }
146
- for (const event of events) {
147
- for (const listener of Array.from(this.eventListeners)) {
148
- listener(event);
149
- }
150
- }
151
- return events;
152
- }
153
-
154
- getValue(target: string): AnimationValue | undefined {
155
- const value = this.values.get(target);
156
- return value === undefined ? undefined : cloneAnimationValue(value);
157
- }
158
-
159
- onEvent(listener: (event: AnimationEvent) => void): () => void {
160
- this.assertAlive();
161
- this.eventListeners.add(listener);
162
- return () => this.eventListeners.delete(listener);
163
- }
164
-
165
- snapshot(): AnimationMixerSnapshot {
166
- const values: Record<string, AnimationValue> = {};
167
- for (const [target, value] of this.values) {
168
- values[target] = cloneAnimationValue(value);
169
- }
170
- return {
171
- timeScale: this.timeScale,
172
- actionCount: this.actions.length,
173
- actions: this.actions.map((action) => action.snapshot()),
174
- layers: this.layers.map((layer) => layer.snapshot()),
175
- values,
176
- applyErrors: this.applyErrors.map((error) => ({ ...error }))
177
- };
178
- }
179
-
180
- dispose(): void {
181
- this.actions.length = 0;
182
- this.layers.length = 0;
183
- this.values.clear();
184
- this.applyErrors = [];
185
- this.eventListeners.clear();
186
- this.inertialTransition = undefined;
187
- this.disposed = true;
188
- }
189
-
190
- private assertAlive(): void {
191
- if (this.disposed) {
192
- throw new Error("AnimationMixer has been disposed.");
193
- }
194
- }
195
-
196
- private advanceInertialTransition(scaledDelta: number): void {
197
- const transition = this.inertialTransition;
198
- if (!transition) return;
199
- transition.elapsed += scaledDelta;
200
- const fromWeight = inertializedTransitionWeight(transition.elapsed, transition.halfLife);
201
- transition.from.weight = fromWeight;
202
- transition.to.weight = 1 - fromWeight;
203
- // Retire the transition once the source has effectively faded (offset within 1e-3).
204
- if (fromWeight <= 1e-3) {
205
- transition.from.weight = 0;
206
- transition.to.weight = 1;
207
- transition.from.stop();
208
- this.inertialTransition = undefined;
209
- }
210
- }
211
-
212
- private applyActionRootMotion(action: AnimationAction, previousTime: number): void {
213
- if (!this.options.applyRootMotion || action.weight <= 0) return;
214
- const rootMotionTarget = this.options.rootMotionTrack ?? "root.position";
215
- if (!action.clip.tracks.some((track) => track.target === rootMotionTarget)) return;
216
- const sample = extractRootMotion(action.clip, {
217
- fromTime: previousTime,
218
- toTime: action.time,
219
- loop: action.loopMode === "repeat",
220
- target: rootMotionTarget
221
- });
222
- const scaledSample = action.weight === 1
223
- ? sample
224
- : {
225
- ...sample,
226
- delta: [
227
- sample.delta[0] * action.weight,
228
- sample.delta[1] * action.weight,
229
- sample.delta[2] * action.weight
230
- ] as [number, number, number]
231
- };
232
- this.target?.applyRootMotion?.(scaledSample);
233
- if (this.target?.position) {
234
- applyRootMotion(this.target as RootMotionTarget, scaledSample, this.options.rootMotionScale ?? 1);
235
- }
236
- }
237
-
238
- private layersForAction(action: AnimationAction): readonly MixerLayer[] {
239
- if (this.layers.length === 0) {
240
- return UNMASKED_LAYERS;
241
- }
242
- const owningLayers = this.layers.filter((layer) => layer.actions.includes(action));
243
- return owningLayers.length > 0 ? owningLayers : UNMASKED_LAYERS;
244
- }
245
- }
246
-
247
- const UNMASKED_LAYER: MixerLayer = {
248
- name: "default",
249
- weight: 1,
250
- additive: false,
251
- mask: [],
252
- actions: [],
253
- capturesTarget: () => true,
254
- snapshot: () => ({ name: "default", weight: 1, additive: false, mask: [], actions: [] })
255
- };
256
- const UNMASKED_LAYERS: readonly MixerLayer[] = [UNMASKED_LAYER];
257
-
258
- function blendInto(accumulators: Map<string, TargetAccumulator>, target: string, type: string, value: AnimationValue, weight: number, additive: boolean): void {
259
- if (weight <= 0) {
260
- return;
261
- }
262
- const current = accumulators.get(target);
263
- if (!current) {
264
- const accumulator: TargetAccumulator = { type };
265
- if (additive) {
266
- accumulator.additive = additiveContribution(type, value, weight);
267
- } else {
268
- accumulator.base = { value: cloneAnimationValue(value), weight, type };
269
- }
270
- accumulators.set(target, accumulator);
271
- return;
272
- }
273
- if (current.type !== type) {
274
- throw new Error(`Cannot blend ${type} animation track into existing ${current.type} target ${target}.`);
275
- }
276
- if (additive) {
277
- current.additive = current.additive === undefined
278
- ? additiveContribution(type, value, weight)
279
- : combineAdditive(type, current.additive, value, weight);
280
- return;
281
- }
282
- if (!current.base) {
283
- current.base = { value: cloneAnimationValue(value), weight, type };
284
- return;
285
- }
286
- blendBase(current.base, type, value, weight);
287
- }
288
-
289
- function blendBase(current: WeightedAccumulator, type: string, value: AnimationValue, weight: number): void {
290
- const total = current.weight + weight;
291
- const t = weight / total;
292
- if (type === "scalar") {
293
- current.value = (current.value as number) + ((value as number) - (current.value as number)) * t;
294
- } else if (type === "vector3") {
295
- const a = current.value as [number, number, number];
296
- const b = value as [number, number, number];
297
- current.value = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
298
- } else if (type === "quaternion") {
299
- current.value = slerpQuat(current.value as [number, number, number, number], value as [number, number, number, number], t);
300
- } else if (type === "number-array") {
301
- const a = current.value as readonly number[];
302
- const b = value as readonly number[];
303
- if (a.length !== b.length) {
304
- throw new Error("Cannot blend number-array animation values with different lengths.");
305
- }
306
- current.value = a.map((component, index) => component + (b[index]! - component) * t);
307
- } else {
308
- current.value = cloneAnimationValue(value);
309
- }
310
- current.weight = total;
311
- }
312
-
313
- function finalizeTargetBlend(accumulator: TargetAccumulator): AnimationValue {
314
- const base = accumulator.base
315
- ? finalizeBaseBlend(accumulator.base)
316
- : accumulator.additive !== undefined
317
- ? additiveNeutral(accumulator.type, accumulator.additive)
318
- : undefined;
319
- if (base === undefined) {
320
- throw new Error("Animation target accumulator has no sampled value.");
321
- }
322
- if (accumulator.additive === undefined) {
323
- return base;
324
- }
325
- return applyAdditive(accumulator.type, base, accumulator.additive);
326
- }
327
-
328
- function finalizeBaseBlend(accumulator: WeightedAccumulator): AnimationValue {
329
- if (accumulator.type === "quaternion") {
330
- return normalizeQuat(accumulator.value as [number, number, number, number]);
331
- }
332
- return cloneAnimationValue(accumulator.value);
333
- }
334
-
335
- function additiveContribution(type: string, value: AnimationValue, weight: number): AnimationValue {
336
- if (type === "scalar") {
337
- return (value as number) * weight;
338
- }
339
- if (type === "vector3") {
340
- const vector = value as [number, number, number];
341
- return [vector[0] * weight, vector[1] * weight, vector[2] * weight];
342
- }
343
- if (type === "number-array") {
344
- return (value as readonly number[]).map((component) => component * weight);
345
- }
346
- if (type === "quaternion") {
347
- return slerpQuat([0, 0, 0, 1], normalizeQuat(value as [number, number, number, number]), weight);
348
- }
349
- throw new Error("Additive animation layers require numeric, vector, quaternion, or number-array tracks.");
350
- }
351
-
352
- function combineAdditive(type: string, current: AnimationValue, value: AnimationValue, weight: number): AnimationValue {
353
- return applyAdditive(type, current, additiveContribution(type, value, weight));
354
- }
355
-
356
- function applyAdditive(type: string, base: AnimationValue, delta: AnimationValue): AnimationValue {
357
- if (type === "scalar") {
358
- return (base as number) + (delta as number);
359
- }
360
- if (type === "vector3") {
361
- const a = base as [number, number, number];
362
- const b = delta as [number, number, number];
363
- return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
364
- }
365
- if (type === "number-array") {
366
- const a = base as readonly number[];
367
- const b = delta as readonly number[];
368
- if (a.length !== b.length) {
369
- throw new Error("Cannot add number-array animation values with different lengths.");
370
- }
371
- return a.map((component, index) => component + b[index]!);
372
- }
373
- if (type === "quaternion") {
374
- return normalizeQuat(multiplyQuat(base as [number, number, number, number], delta as [number, number, number, number]));
375
- }
376
- throw new Error("Additive animation layers require numeric, vector, quaternion, or number-array tracks.");
377
- }
378
-
379
- function additiveNeutral(type: string, value: AnimationValue): AnimationValue {
380
- if (type === "scalar") {
381
- return 0;
382
- }
383
- if (type === "vector3") {
384
- return [0, 0, 0];
385
- }
386
- if (type === "number-array") {
387
- return (value as readonly number[]).map(() => 0);
388
- }
389
- if (type === "quaternion") {
390
- return [0, 0, 0, 1];
391
- }
392
- throw new Error("Additive animation layers require numeric, vector, quaternion, or number-array tracks.");
393
- }
394
-
395
- function multiplyQuat(a: [number, number, number, number], b: [number, number, number, number]): [number, number, number, number] {
396
- return [
397
- a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
398
- a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
399
- a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
400
- a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
401
- ];
402
- }
@@ -1,105 +0,0 @@
1
- export {
2
- AnimationStateMachine as AnimationStateGraph,
3
- AnimationStateMachine,
4
- type AnimationState,
5
- type AnimationStateBlend,
6
- type AnimationStateMachineGraphSnapshot,
7
- type AnimationStateMachineGraphState,
8
- type AnimationStateMachineGraphTransition,
9
- type StateTransition
10
- } from "./AnimationStateMachine.js";
11
-
12
- import { AnimationStateMachine, type AnimationState } from "./AnimationStateMachine.js";
13
-
14
- export interface AnimationAnimationStateGraphOptions {
15
- readonly idleState?: string;
16
- }
17
-
18
- export interface AnimationAnimationStateGraphSample {
19
- readonly frame: number;
20
- readonly state: string;
21
- }
22
-
23
- export function createAnimationAnimationStateGraph(options: AnimationAnimationStateGraphOptions = {}): AnimationStateMachine {
24
- const idle = options.idleState ?? "idle";
25
- const states: readonly AnimationState[] = [
26
- {
27
- name: idle,
28
- transitions: [
29
- transition("speak", "isSpeaking", 50),
30
- transition("gesture", "gesture", 40, ["gesture"]),
31
- transition("walk", "isWalking", 30),
32
- transition("action", "action", 60, ["action"]),
33
- transition("listen", "isListening", 20)
34
- ]
35
- },
36
- { name: "listen", transitions: [transition("speak", "isSpeaking", 50), transition(idle, "isListening", 0, undefined, false)] },
37
- { name: "speak", transitions: [transition("gesture", "gesture", 60, ["gesture"]), transition(idle, "isSpeaking", 0, undefined, false)] },
38
- { name: "gesture", duration: 0.4, oneShot: true, onComplete: idle },
39
- { name: "walk", transitions: [transition(idle, "isWalking", 0, undefined, false), transition("action", "action", 60, ["action"])] },
40
- { name: "action", duration: 0.5, oneShot: true, onComplete: idle }
41
- ];
42
- return new AnimationStateMachine(states, idle);
43
- }
44
-
45
- export function sampleAnimationAnimationStateGraph(
46
- graph: AnimationStateMachine,
47
- inputs: readonly Readonly<Record<string, number | boolean | string>>[],
48
- delta = 1 / 30
49
- ): readonly AnimationAnimationStateGraphSample[] {
50
- return inputs.map((parameters, frame) => {
51
- for (const [key, value] of Object.entries(parameters)) graph.setParameter(key, value);
52
- return { frame, state: graph.update(delta) };
53
- });
54
- }
55
-
56
- export interface LocomotionAnimationStateGraphOptions {
57
- readonly idleState?: string;
58
- }
59
-
60
- /**
61
- * Reusable locomotion state graph (idle <-> walk <-> run) for character-controller and
62
- * Animation Studio templates. Parameters: `isMoving` (walk), `isRunning` (run). Mirrors
63
- * {@link createAnimationAnimationStateGraph} so both share the deterministic state-machine core.
64
- */
65
- export function createLocomotionAnimationStateGraph(options: LocomotionAnimationStateGraphOptions = {}): AnimationStateMachine {
66
- const idle = options.idleState ?? "idle";
67
- const states: readonly AnimationState[] = [
68
- {
69
- name: idle,
70
- transitions: [transition("run", "isRunning", 60), transition("walk", "isMoving", 40)]
71
- },
72
- {
73
- name: "walk",
74
- transitions: [transition("run", "isRunning", 60), transition(idle, "isMoving", 0, undefined, false)]
75
- },
76
- {
77
- name: "run",
78
- transitions: [transition("walk", "isRunning", 0, undefined, false), transition(idle, "isMoving", 0, undefined, false)]
79
- }
80
- ];
81
- return new AnimationStateMachine(states, idle);
82
- }
83
-
84
- function transition(
85
- to: string,
86
- parameter: string,
87
- priority: number,
88
- consumeParameters?: readonly string[],
89
- expected = true
90
- ) {
91
- return {
92
- to,
93
- priority,
94
- label: `${parameter}->${to}`,
95
- consumeParameters,
96
- condition: (parameters: Readonly<Record<string, number | boolean | string>>) => {
97
- const value = parameters[parameter];
98
- if (expected !== true) return value === expected;
99
- if (typeof value === "boolean") return value;
100
- if (typeof value === "string") return value.trim().length > 0;
101
- if (typeof value === "number") return value > 0;
102
- return false;
103
- }
104
- };
105
- }