@flighthq/animation 0.1.0 → 0.2.0

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.
@@ -1,5 +1,7 @@
1
1
  import type { AnimationChannel, AnimationClip, AnimationTrack } from '@flighthq/types';
2
+ export declare function cloneAnimationClip(clip: Readonly<AnimationClip>): AnimationClip;
2
3
  export declare function createAnimationChannel(track: AnimationTrack, targetRef: unknown): AnimationChannel;
3
4
  export declare function createAnimationClip(channels: AnimationChannel[], duration?: number): AnimationClip;
4
5
  export declare function getAnimationClipDuration(clip: Readonly<AnimationClip>): number;
6
+ export declare function sampleAnimationClip(out: number[] | Float32Array, clip: Readonly<AnimationClip>, time: number, visit: (sampled: Readonly<number[] | Float32Array>, channel: Readonly<AnimationChannel>, index: number) => void): void;
5
7
  //# sourceMappingURL=animationClip.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"animationClip.d.ts","sourceRoot":"","sources":["../src/animationClip.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGvF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,GAAG,gBAAgB,CAElG;AAGD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,aAAa,CAElG;AAGD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAE9E"}
1
+ {"version":3,"file":"animationClip.d.ts","sourceRoot":"","sources":["../src/animationClip.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAMvF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,aAAa,CAM/E;AAGD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,GAAG,gBAAgB,CAElG;AAGD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,aAAa,CAElG;AAGD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAE9E;AAOD,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,EAAE,GAAG,YAAY,EAC5B,IAAI,EAAE,QAAQ,CAAC,aAAa,CAAC,EAC7B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,EAAE,GAAG,YAAY,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAC9G,IAAI,CAON"}
@@ -1,3 +1,13 @@
1
+ import { cloneAnimationTrack, sampleAnimationTrack } from './animationTrack';
2
+ // Deep-copies a clip: each channel gets a freshly cloned track (buffers deep-copied) while its opaque
3
+ // `targetRef` is carried by reference (the clip core never interprets it, so it cannot deep-copy it).
4
+ export function cloneAnimationClip(clip) {
5
+ const channels = [];
6
+ for (const channel of clip.channels) {
7
+ channels.push(createAnimationChannel(cloneAnimationTrack(channel.track), channel.targetRef));
8
+ }
9
+ return { channels, duration: clip.duration };
10
+ }
1
11
  // Pairs a track with an opaque target reference (interpreted only by the domain binding layer).
2
12
  export function createAnimationChannel(track, targetRef) {
3
13
  return { targetRef, track };
@@ -10,6 +20,19 @@ export function createAnimationClip(channels, duration) {
10
20
  export function getAnimationClipDuration(clip) {
11
21
  return clip.duration;
12
22
  }
23
+ // Samples every channel of `clip` at `time`, reusing the caller-supplied `out` scratch buffer for each
24
+ // channel and handing it to `visit` along with the channel and its index — so a domain layer (scene,
25
+ // skeleton) can bind each sampled value to that channel's target through one shared loop instead of
26
+ // re-implementing the per-channel walk. `out` must be at least as wide as the widest channel's
27
+ // `components`; it is overwritten per channel, so `visit` must consume it before returning. Alloc-free.
28
+ export function sampleAnimationClip(out, clip, time, visit) {
29
+ const channels = clip.channels;
30
+ for (let i = 0; i < channels.length; i++) {
31
+ const channel = channels[i];
32
+ sampleAnimationTrack(out, channel.track, time);
33
+ visit(out, channel, i);
34
+ }
35
+ }
13
36
  function computeChannelsDuration(channels) {
14
37
  let max = 0;
15
38
  for (const channel of channels) {
@@ -1 +1 @@
1
- {"version":3,"file":"animationClip.js","sourceRoot":"","sources":["../src/animationClip.ts"],"names":[],"mappings":"AAEA,gGAAgG;AAChG,MAAM,UAAU,sBAAsB,CAAC,KAAqB,EAAE,SAAkB;IAC9E,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAC9B,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,mBAAmB,CAAC,QAA4B,EAAE,QAAiB;IACjF,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,IAAI,uBAAuB,CAAC,QAAQ,CAAC,EAAE,CAAC;AAC/E,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,wBAAwB,CAAC,IAA6B;IACpE,OAAO,IAAI,CAAC,QAAQ,CAAC;AACvB,CAAC;AAED,SAAS,uBAAuB,CAAC,QAA+C;IAC9E,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;QAClC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC;QAC1B,IAAI,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,GAAG;YAAE,GAAG,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"animationClip.js","sourceRoot":"","sources":["../src/animationClip.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7E,sGAAsG;AACtG,sGAAsG;AACtG,MAAM,UAAU,kBAAkB,CAAC,IAA6B;IAC9D,MAAM,QAAQ,GAAuB,EAAE,CAAC;IACxC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QACpC,QAAQ,CAAC,IAAI,CAAC,sBAAsB,CAAC,mBAAmB,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IAC/F,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;AAC/C,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,sBAAsB,CAAC,KAAqB,EAAE,SAAkB;IAC9E,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAC9B,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,mBAAmB,CAAC,QAA4B,EAAE,QAAiB;IACjF,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,IAAI,uBAAuB,CAAC,QAAQ,CAAC,EAAE,CAAC;AAC/E,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,wBAAwB,CAAC,IAA6B;IACpE,OAAO,IAAI,CAAC,QAAQ,CAAC;AACvB,CAAC;AAED,uGAAuG;AACvG,qGAAqG;AACrG,oGAAoG;AACpG,+FAA+F;AAC/F,wGAAwG;AACxG,MAAM,UAAU,mBAAmB,CACjC,GAA4B,EAC5B,IAA6B,EAC7B,IAAY,EACZ,KAA+G;IAE/G,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC5B,oBAAoB,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC/C,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;IACzB,CAAC;AACH,CAAC;AAED,SAAS,uBAAuB,CAAC,QAA+C;IAC9E,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;QAClC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC;QAC1B,IAAI,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,GAAG;YAAE,GAAG,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -1,10 +1,17 @@
1
- import type { AnimationClip, AnimationPlayer } from '@flighthq/types';
1
+ import type { AnimationClip, AnimationLoopMode, AnimationPlayer } from '@flighthq/types';
2
2
  export declare function advanceAnimationPlayer(player: AnimationPlayer, dt: number): void;
3
+ export declare function cloneAnimationPlayer(player: Readonly<AnimationPlayer>): AnimationPlayer;
3
4
  export declare function createAnimationPlayer(clip: AnimationClip, opts?: Readonly<{
4
5
  loop?: boolean;
6
+ loopMode?: AnimationLoopMode;
5
7
  playing?: boolean;
8
+ repeatCount?: number;
6
9
  speed?: number;
7
10
  time?: number;
8
11
  }>): AnimationPlayer;
12
+ export declare function enableAnimationPlayerSignals(player: AnimationPlayer): void;
13
+ export declare function getAnimationPlayerNormalizedTime(player: Readonly<AnimationPlayer>): number;
14
+ export declare function playAnimationPlayer(player: AnimationPlayer): void;
9
15
  export declare function seekAnimationPlayer(player: AnimationPlayer, time: number): void;
16
+ export declare function stopAnimationPlayer(player: AnimationPlayer): void;
10
17
  //# sourceMappingURL=animationPlayer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"animationPlayer.d.ts","sourceRoot":"","sources":["../src/animationPlayer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAMtE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAmBhF;AAGD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,aAAa,EACnB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GACpF,eAAe,CAQjB;AAGD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAG/E"}
1
+ {"version":3,"file":"animationPlayer.d.ts","sourceRoot":"","sources":["../src/animationPlayer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAUzF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAsEhF;AAKD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,eAAe,CAYvF;AAID,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,aAAa,EACnB,IAAI,CAAC,EAAE,QAAQ,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC,GACD,eAAe,CAYjB;AAID,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAG1E;AAID,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,MAAM,CAK1F;AAID,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAEjE;AAGD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAG/E;AAID,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAGjE"}
@@ -1,6 +1,11 @@
1
+ import { createSignal, emitSignal } from '@flighthq/signals';
2
+ import { AnimationLoopModePingPong, AnimationLoopModeRepeat } from '@flighthq/types';
1
3
  // Advances the playhead by `dt` seconds (scaled by `speed`; negative plays backward). When `loop`,
2
- // time wraps modulo the clip duration; otherwise it clamps to [0, duration] and clears `playing` when
3
- // it reaches an end. No-op while paused or when the clip has zero duration. The app calls this each
4
+ // time wraps at the clip duration per `loopMode` 'Repeat' jumps end→start, 'PingPong' reflects and
5
+ // reverses `speed`; each wrap/bounce spends one unit of `repeatCount` (negative/undefined = infinite)
6
+ // and playback stops when the budget is exhausted. When not looping, time clamps to [0, duration] and
7
+ // clears `playing` at either end. Emits the opt-in onLooped/onFinished signals (null-guarded, so bare
8
+ // players cost nothing). No-op while paused or when the clip has zero duration. The app calls this each
4
9
  // frame — nothing advances on its own.
5
10
  export function advanceAnimationPlayer(player, dt) {
6
11
  if (!player.playing)
@@ -11,34 +16,161 @@ export function advanceAnimationPlayer(player, dt) {
11
16
  return;
12
17
  }
13
18
  let time = player.time + dt * player.speed;
14
- if (player.loop) {
15
- time %= duration;
16
- if (time < 0)
17
- time += duration;
19
+ if (!player.loop) {
20
+ if (time >= duration) {
21
+ player.time = duration;
22
+ player.playing = false;
23
+ emitAnimationPlayerFinished(player);
24
+ }
25
+ else if (time < 0) {
26
+ player.time = 0;
27
+ player.playing = false;
28
+ emitAnimationPlayerFinished(player);
29
+ }
30
+ else {
31
+ player.time = time;
32
+ }
33
+ return;
18
34
  }
19
- else if (time >= duration) {
20
- time = duration;
21
- player.playing = false;
35
+ let looped = false;
36
+ if (player.loopMode === AnimationLoopModePingPong) {
37
+ // Reflect at each boundary, flipping the travel direction (`speed` sign). A loop handles a `dt`
38
+ // large enough to cross a boundary more than once.
39
+ for (;;) {
40
+ if (time > duration) {
41
+ if (!consumeAnimationPlayerLoop(player)) {
42
+ finishAnimationPlayerAt(player, duration);
43
+ return;
44
+ }
45
+ time = 2 * duration - time;
46
+ player.speed = -player.speed;
47
+ looped = true;
48
+ }
49
+ else if (time < 0) {
50
+ if (!consumeAnimationPlayerLoop(player)) {
51
+ finishAnimationPlayerAt(player, 0);
52
+ return;
53
+ }
54
+ time = -time;
55
+ player.speed = -player.speed;
56
+ looped = true;
57
+ }
58
+ else {
59
+ break;
60
+ }
61
+ }
22
62
  }
23
- else if (time < 0) {
24
- time = 0;
25
- player.playing = false;
63
+ else {
64
+ while (time >= duration) {
65
+ if (!consumeAnimationPlayerLoop(player)) {
66
+ finishAnimationPlayerAt(player, duration);
67
+ return;
68
+ }
69
+ time -= duration;
70
+ looped = true;
71
+ }
72
+ while (time < 0) {
73
+ if (!consumeAnimationPlayerLoop(player)) {
74
+ finishAnimationPlayerAt(player, 0);
75
+ return;
76
+ }
77
+ time += duration;
78
+ looped = true;
79
+ }
26
80
  }
27
81
  player.time = time;
82
+ if (looped)
83
+ emitAnimationPlayerLooped(player);
84
+ }
85
+ // Shallow-copies a player into a fresh, independent driver. The `clip` (and thus its buffers) is shared
86
+ // by reference — players are lightweight drivers over a clip; clone the clip separately for independent
87
+ // buffers. The clone starts signal-free (onFinished/onLooped null) so listeners are never shared.
88
+ export function cloneAnimationPlayer(player) {
89
+ return {
90
+ clip: player.clip,
91
+ loop: player.loop,
92
+ loopMode: player.loopMode,
93
+ onFinished: null,
94
+ onLooped: null,
95
+ playing: player.playing,
96
+ repeatCount: player.repeatCount,
97
+ speed: player.speed,
98
+ time: player.time,
99
+ };
28
100
  }
29
- // Allocates a player over `clip`. Defaults: looping, playing, speed 1, time 0.
101
+ // Allocates a player over `clip`. Defaults: looping, 'Repeat' loop mode, infinite repeats, playing,
102
+ // speed 1, time 0, and signal-free (onFinished/onLooped null until enableAnimationPlayerSignals).
30
103
  export function createAnimationPlayer(clip, opts) {
31
104
  return {
32
105
  clip,
33
106
  loop: opts?.loop ?? true,
107
+ loopMode: opts?.loopMode ?? AnimationLoopModeRepeat,
108
+ onFinished: null,
109
+ onLooped: null,
34
110
  playing: opts?.playing ?? true,
111
+ repeatCount: opts?.repeatCount ?? -1,
35
112
  speed: opts?.speed ?? 1,
36
113
  time: opts?.time ?? 0,
37
114
  };
38
115
  }
116
+ // Allocates and attaches the opt-in player signals (onFinished, onLooped) to a player created before
117
+ // they were needed. Idempotent — calling twice does not create duplicate signals.
118
+ export function enableAnimationPlayerSignals(player) {
119
+ if (player.onFinished == null)
120
+ player.onFinished = createSignal();
121
+ if (player.onLooped == null)
122
+ player.onLooped = createSignal();
123
+ }
124
+ // Returns the playhead as a fraction of the clip duration, clamped to [0, 1] (time / duration).
125
+ // Returns 0 for a zero-duration clip.
126
+ export function getAnimationPlayerNormalizedTime(player) {
127
+ const duration = player.clip.duration;
128
+ if (duration <= 0)
129
+ return 0;
130
+ const n = player.time / duration;
131
+ return n < 0 ? 0 : n > 1 ? 1 : n;
132
+ }
133
+ // Resumes advancement by setting `playing` true, leaving the playhead where it is. Symmetric with
134
+ // stopAnimationPlayer; pair with seekAnimationPlayer to choose a start point first.
135
+ export function playAnimationPlayer(player) {
136
+ player.playing = true;
137
+ }
39
138
  // Sets the playhead to `time`, clamped to [0, clip.duration]. Does not change `playing`.
40
139
  export function seekAnimationPlayer(player, time) {
41
140
  const duration = player.clip.duration;
42
141
  player.time = time < 0 ? 0 : time > duration ? duration : time;
43
142
  }
143
+ // Halts advancement and rewinds: clears `playing` and resets the playhead to 0. Symmetric with
144
+ // playAnimationPlayer; use seekAnimationPlayer to stop the player in place instead.
145
+ export function stopAnimationPlayer(player) {
146
+ player.playing = false;
147
+ player.time = 0;
148
+ }
149
+ // Spends one unit of the player's finite repeat budget, returning whether a wrap/bounce is still
150
+ // permitted. A negative or undefined `repeatCount` means infinite (always permitted); 0 means the
151
+ // budget is exhausted.
152
+ function consumeAnimationPlayerLoop(player) {
153
+ const rc = player.repeatCount;
154
+ if (rc === undefined || rc < 0)
155
+ return true;
156
+ if (rc === 0)
157
+ return false;
158
+ player.repeatCount = rc - 1;
159
+ return true;
160
+ }
161
+ function emitAnimationPlayerFinished(player) {
162
+ if (player.onFinished != null)
163
+ emitSignal(player.onFinished);
164
+ }
165
+ function emitAnimationPlayerLooped(player) {
166
+ if (player.onLooped != null)
167
+ emitSignal(player.onLooped);
168
+ }
169
+ // Clamps the playhead to `time`, clears `playing`, and emits onFinished — the shared end-of-playback
170
+ // path when a non-looping run or an exhausted repeat budget reaches a boundary.
171
+ function finishAnimationPlayerAt(player, time) {
172
+ player.time = time;
173
+ player.playing = false;
174
+ emitAnimationPlayerFinished(player);
175
+ }
44
176
  //# sourceMappingURL=animationPlayer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"animationPlayer.js","sourceRoot":"","sources":["../src/animationPlayer.ts"],"names":[],"mappings":"AAEA,mGAAmG;AACnG,sGAAsG;AACtG,oGAAoG;AACpG,uCAAuC;AACvC,MAAM,UAAU,sBAAsB,CAAC,MAAuB,EAAE,EAAU;IACxE,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO;IAC5B,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;IACtC,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;QAClB,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC;QAChB,OAAO;IACT,CAAC;IACD,IAAI,IAAI,GAAG,MAAM,CAAC,IAAI,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC;IAC3C,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAChB,IAAI,IAAI,QAAQ,CAAC;QACjB,IAAI,IAAI,GAAG,CAAC;YAAE,IAAI,IAAI,QAAQ,CAAC;IACjC,CAAC;SAAM,IAAI,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,GAAG,QAAQ,CAAC;QAChB,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC;IACzB,CAAC;SAAM,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;QACpB,IAAI,GAAG,CAAC,CAAC;QACT,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC;IACzB,CAAC;IACD,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;AACrB,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,qBAAqB,CACnC,IAAmB,EACnB,IAAqF;IAErF,OAAO;QACL,IAAI;QACJ,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,IAAI;QACxB,OAAO,EAAE,IAAI,EAAE,OAAO,IAAI,IAAI;QAC9B,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;QACvB,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC;KACtB,CAAC;AACJ,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,mBAAmB,CAAC,MAAuB,EAAE,IAAY;IACvE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;IACtC,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;AACjE,CAAC"}
1
+ {"version":3,"file":"animationPlayer.js","sourceRoot":"","sources":["../src/animationPlayer.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import type { AnimationInterpolation, AnimationTrack } from '@flighthq/types';
1
+ import type { AnimationInterpolation, AnimationTrack, AnimationTrackValidationDiagnostic } from '@flighthq/types';
2
+ export declare function cloneAnimationTrack(track: Readonly<AnimationTrack>): AnimationTrack;
2
3
  export declare function createAnimationTrack(opts: {
3
4
  times: ArrayLike<number>;
4
5
  values: ArrayLike<number>;
@@ -8,4 +9,6 @@ export declare function createAnimationTrack(opts: {
8
9
  easing?: AnimationTrack['easing'];
9
10
  }): AnimationTrack;
10
11
  export declare function sampleAnimationTrack(out: number[] | Float32Array, track: Readonly<AnimationTrack>, t: number): void;
12
+ export declare function trimAnimationTrack(track: Readonly<AnimationTrack>, startTime: number, endTime: number): AnimationTrack;
13
+ export declare function validateAnimationTrack(track: Readonly<AnimationTrack>): AnimationTrackValidationDiagnostic[] | null;
11
14
  //# sourceMappingURL=animationTrack.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"animationTrack.d.ts","sourceRoot":"","sources":["../src/animationTrack.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAM9E,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;CACnC,GAAG,cAAc,CASjB;AAOD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,YAAY,EAAE,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CA4CnH"}
1
+ {"version":3,"file":"animationTrack.d.ts","sourceRoot":"","sources":["../src/animationTrack.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,cAAc,EAAE,kCAAkC,EAAE,MAAM,iBAAiB,CAAC;AAMlH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,cAAc,CASnF;AAKD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;CACnC,GAAG,cAAc,CASjB;AAOD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,YAAY,EAAE,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAmDnH;AAOD,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC/B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,cAAc,CAqBhB;AAKD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,kCAAkC,EAAE,GAAG,IAAI,CAsBnH"}
@@ -1,4 +1,17 @@
1
1
  import { AnimationInterpolationLinear } from '@flighthq/types';
2
+ // Deep-copies an AnimationTrack, allocating fresh `times` and `values` buffers (preserving Float32Array
3
+ // vs number[] backing) so the clone shares no mutable state with the source. `easing` is a pure
4
+ // function and is carried by reference; all scalar fields are copied.
5
+ export function cloneAnimationTrack(track) {
6
+ return {
7
+ components: track.components,
8
+ easing: track.easing,
9
+ interpolation: track.interpolation,
10
+ quaternion: track.quaternion,
11
+ times: cloneNumberBuffer(track.times),
12
+ values: cloneNumberBuffer(track.values),
13
+ };
14
+ }
2
15
  // Allocates an AnimationTrack. `times` must be ascending; `values` is the flat keyframe buffer
3
16
  // (`components` numbers per keyframe for step/linear, 3 * `components` for cubic). Defaults:
4
17
  // linear interpolation, 1 component, non-quaternion, no easing.
@@ -33,10 +46,18 @@ export function sampleAnimationTrack(out, track, t) {
33
46
  copyKeyframeValue(out, track, count - 1);
34
47
  return;
35
48
  }
36
- // Locate the segment [i, i+1] containing t (times are ascending; t is strictly inside the range).
37
- let i = 0;
38
- while (i < count - 1 && times[i + 1] <= t)
39
- i++;
49
+ // Binary search for the segment [i, i+1] containing t: the largest i with times[i] <= t. Times are
50
+ // ascending and t is strictly inside (times[0] < t < times[count-1]), so i lands in [0, count-2].
51
+ let lo = 0;
52
+ let hi = count - 1;
53
+ while (lo < hi) {
54
+ const mid = (lo + hi + 1) >> 1;
55
+ if (times[mid] <= t)
56
+ lo = mid;
57
+ else
58
+ hi = mid - 1;
59
+ }
60
+ const i = lo;
40
61
  const t0 = times[i];
41
62
  const dt = times[i + 1] - t0;
42
63
  let alpha = dt > 0 ? (t - t0) / dt : 0;
@@ -62,6 +83,76 @@ export function sampleAnimationTrack(out, track, t) {
62
83
  out[c] = a + (values[oj + c] - a) * alpha;
63
84
  }
64
85
  }
86
+ // Extracts the keyframes of `track` whose time falls within [startTime, endTime] into a fresh subclip
87
+ // track, rebasing times so the range start maps to 0 (a keyframe at startTime becomes t=0). Buffers are
88
+ // deep-copied; endpoints are not synthesized — the trimmed track's first/last keyframes are simply the
89
+ // in-range keyframes, and sampling clamps outside them as usual. Returns an empty track when no
90
+ // keyframe falls in range.
91
+ export function trimAnimationTrack(track, startTime, endTime) {
92
+ const { components, times } = track;
93
+ const count = times.length;
94
+ const stride = keyframeStride(track);
95
+ const outTimes = [];
96
+ const outValues = [];
97
+ for (let k = 0; k < count; k++) {
98
+ const time = times[k];
99
+ if (time < startTime || time > endTime)
100
+ continue;
101
+ outTimes.push(time - startTime);
102
+ const off = k * stride;
103
+ for (let c = 0; c < stride; c++)
104
+ outValues.push(track.values[off + c]);
105
+ }
106
+ return {
107
+ components,
108
+ easing: track.easing,
109
+ interpolation: track.interpolation,
110
+ quaternion: track.quaternion,
111
+ times: outTimes,
112
+ values: outValues,
113
+ };
114
+ }
115
+ // Structurally validates a track, returning null when it is well-formed or an array of diagnostics
116
+ // otherwise. Checks that `times` is strictly ascending and that `values.length` equals
117
+ // keyCount * componentsPerKeyframe (components for step/linear, 3 * components for cubic). Never throws.
118
+ export function validateAnimationTrack(track) {
119
+ const diagnostics = [];
120
+ const { times, values } = track;
121
+ const count = times.length;
122
+ for (let k = 1; k < count; k++) {
123
+ if (times[k] <= times[k - 1]) {
124
+ diagnostics.push({
125
+ code: 'nonAscendingTimes',
126
+ index: k,
127
+ message: `times[${k}] (${times[k]}) is not greater than times[${k - 1}] (${times[k - 1]}); times must be strictly ascending.`,
128
+ });
129
+ }
130
+ }
131
+ const expected = count * keyframeStride(track);
132
+ if (values.length !== expected) {
133
+ diagnostics.push({
134
+ code: 'valuesLengthMismatch',
135
+ index: null,
136
+ message: `values.length (${values.length}) must equal keyCount * componentsPerKeyframe (${expected}).`,
137
+ });
138
+ }
139
+ return diagnostics.length > 0 ? diagnostics : null;
140
+ }
141
+ // Allocates a fresh number buffer with the same contents as `src`, preserving a Float32Array backing
142
+ // when the source is one (so clones of typed-array tracks stay typed).
143
+ function cloneNumberBuffer(src) {
144
+ if (src instanceof Float32Array)
145
+ return src.slice();
146
+ const out = new Array(src.length);
147
+ for (let i = 0; i < src.length; i++)
148
+ out[i] = src[i];
149
+ return out;
150
+ }
151
+ function copyKeyframeValue(out, track, k) {
152
+ const off = keyframeValueOffset(track, k);
153
+ for (let c = 0; c < track.components; c++)
154
+ out[c] = track.values[off + c];
155
+ }
65
156
  // Byte width of one keyframe block in the flat value buffer (cubic stores in/out tangents alongside
66
157
  // the value, so 3x).
67
158
  function keyframeStride(track) {
@@ -73,10 +164,16 @@ function keyframeValueOffset(track, k) {
73
164
  const stride = keyframeStride(track);
74
165
  return track.interpolation === 'Cubic' ? k * stride + track.components : k * stride;
75
166
  }
76
- function copyKeyframeValue(out, track, k) {
77
- const off = keyframeValueOffset(track, k);
78
- for (let c = 0; c < track.components; c++)
79
- out[c] = track.values[off + c];
167
+ function normalizeFlatQuaternion(out) {
168
+ const x = out[0], y = out[1], z = out[2], w = out[3];
169
+ const len = Math.hypot(x, y, z, w);
170
+ if (len > 0) {
171
+ const inv = 1 / len;
172
+ out[0] = x * inv;
173
+ out[1] = y * inv;
174
+ out[2] = z * inv;
175
+ out[3] = w * inv;
176
+ }
80
177
  }
81
178
  // glTF cubic-spline (Hermite) interpolation of segment [i, i+1] at `alpha`, with `dt` the segment
82
179
  // duration (tangents are derivatives, scaled by dt). Quaternion tracks are interpolated component-wise
@@ -132,15 +229,4 @@ function slerpFlatQuaternion(out, values, oa, ob, alpha) {
132
229
  out[2] = scale0 * az + scale1 * bz;
133
230
  out[3] = scale0 * aw + scale1 * bw;
134
231
  }
135
- function normalizeFlatQuaternion(out) {
136
- const x = out[0], y = out[1], z = out[2], w = out[3];
137
- const len = Math.hypot(x, y, z, w);
138
- if (len > 0) {
139
- const inv = 1 / len;
140
- out[0] = x * inv;
141
- out[1] = y * inv;
142
- out[2] = z * inv;
143
- out[3] = w * inv;
144
- }
145
- }
146
232
  //# sourceMappingURL=animationTrack.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"animationTrack.js","sourceRoot":"","sources":["../src/animationTrack.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"animationTrack.js","sourceRoot":"","sources":["../src/animationTrack.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@flighthq/animation",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/flighthq/flight.git",
7
+ "directory": "packages/animation"
8
+ },
4
9
  "type": "module",
5
10
  "main": "dist/index.js",
6
11
  "types": "dist/index.d.ts",
@@ -27,7 +32,8 @@
27
32
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
33
  },
29
34
  "dependencies": {
30
- "@flighthq/types": "0.1.0"
35
+ "@flighthq/signals": "0.2.0",
36
+ "@flighthq/types": "0.2.0"
31
37
  },
32
38
  "devDependencies": {
33
39
  "typescript": "^5.3.0"
@@ -1,10 +1,30 @@
1
- import { createAnimationChannel, createAnimationClip, getAnimationClipDuration } from './animationClip';
1
+ import {
2
+ cloneAnimationClip,
3
+ createAnimationChannel,
4
+ createAnimationClip,
5
+ getAnimationClipDuration,
6
+ sampleAnimationClip,
7
+ } from './animationClip';
2
8
  import { createAnimationTrack } from './animationTrack';
3
9
 
4
10
  function track(times: number[]) {
5
11
  return createAnimationTrack({ times, values: times.map(() => 0) });
6
12
  }
7
13
 
14
+ describe('cloneAnimationClip', () => {
15
+ it('deep-copies each channel track but shares targetRefs by reference', () => {
16
+ const target = {};
17
+ const clip = createAnimationClip([createAnimationChannel(track([0, 1]), target)]);
18
+ const clone = cloneAnimationClip(clip);
19
+ expect(clone).not.toBe(clip);
20
+ expect(clone.channels).not.toBe(clip.channels);
21
+ expect(clone.channels[0].track).not.toBe(clip.channels[0].track);
22
+ expect(clone.channels[0].track.times).not.toBe(clip.channels[0].track.times);
23
+ expect(clone.channels[0].targetRef).toBe(target);
24
+ expect(clone.duration).toBe(clip.duration);
25
+ });
26
+ });
27
+
8
28
  describe('createAnimationChannel', () => {
9
29
  it('pairs a track with an opaque target ref', () => {
10
30
  const t = track([0, 1]);
@@ -36,3 +56,31 @@ describe('getAnimationClipDuration', () => {
36
56
  expect(getAnimationClipDuration(clip)).toBe(4);
37
57
  });
38
58
  });
59
+
60
+ describe('sampleAnimationClip', () => {
61
+ it('samples every channel at the given time and visits each with its channel and index', () => {
62
+ const a = createAnimationTrack({ times: [0, 1], values: [0, 10] });
63
+ const b = createAnimationTrack({ times: [0, 1], values: [100, 200] });
64
+ const targetA = { id: 'a' };
65
+ const targetB = { id: 'b' };
66
+ const clip = createAnimationClip([createAnimationChannel(a, targetA), createAnimationChannel(b, targetB)]);
67
+ const out = [0];
68
+ const seen: Array<{ index: number; target: unknown; value: number }> = [];
69
+ sampleAnimationClip(out, clip, 0.5, (sampled, channel, index) => {
70
+ seen.push({ index, target: channel.targetRef, value: sampled[0] });
71
+ });
72
+ expect(seen).toEqual([
73
+ { index: 0, target: targetA, value: 5 },
74
+ { index: 1, target: targetB, value: 150 },
75
+ ]);
76
+ });
77
+
78
+ it('does not invoke the visitor for a channel-less clip', () => {
79
+ const clip = createAnimationClip([], 1);
80
+ let calls = 0;
81
+ sampleAnimationClip([0], clip, 0, () => {
82
+ calls++;
83
+ });
84
+ expect(calls).toBe(0);
85
+ });
86
+ });
@@ -1,5 +1,16 @@
1
+ import { connectSignal } from '@flighthq/signals';
2
+
1
3
  import { createAnimationClip } from './animationClip';
2
- import { advanceAnimationPlayer, createAnimationPlayer, seekAnimationPlayer } from './animationPlayer';
4
+ import {
5
+ advanceAnimationPlayer,
6
+ cloneAnimationPlayer,
7
+ createAnimationPlayer,
8
+ enableAnimationPlayerSignals,
9
+ getAnimationPlayerNormalizedTime,
10
+ playAnimationPlayer,
11
+ seekAnimationPlayer,
12
+ stopAnimationPlayer,
13
+ } from './animationPlayer';
3
14
 
4
15
  function player(duration: number, opts?: Parameters<typeof createAnimationPlayer>[1]) {
5
16
  return createAnimationPlayer(createAnimationClip([], duration), opts);
@@ -58,6 +69,73 @@ describe('advanceAnimationPlayer', () => {
58
69
  expect(p.time).toBeCloseTo(8.5);
59
70
  expect(p.playing).toBe(true);
60
71
  });
72
+
73
+ it('reflects and reverses speed at the end in ping-pong mode', () => {
74
+ const p = player(10, { loopMode: 'PingPong', speed: 1, time: 9 });
75
+ advanceAnimationPlayer(p, 2); // raw = 11 -> reflect to 20 - 11 = 9, speed flips to -1
76
+ expect(p.time).toBeCloseTo(9);
77
+ expect(p.speed).toBe(-1);
78
+ expect(p.playing).toBe(true);
79
+ });
80
+
81
+ it('reflects and reverses speed at the start in ping-pong mode', () => {
82
+ const p = player(10, { loopMode: 'PingPong', speed: -1, time: 1 });
83
+ advanceAnimationPlayer(p, 2); // raw = -1 -> reflect to 1, speed flips to +1
84
+ expect(p.time).toBeCloseTo(1);
85
+ expect(p.speed).toBe(1);
86
+ expect(p.playing).toBe(true);
87
+ });
88
+
89
+ it('stops at the end when a finite repeat budget is exhausted', () => {
90
+ const p = player(10, { loop: true, repeatCount: 0, time: 9 });
91
+ advanceAnimationPlayer(p, 2); // no wrap permitted -> clamp to duration and stop
92
+ expect(p.time).toBe(10);
93
+ expect(p.playing).toBe(false);
94
+ });
95
+
96
+ it('spends one repeat per wrap and stops once the budget runs out', () => {
97
+ const p = player(10, { loop: true, repeatCount: 1, time: 9 });
98
+ advanceAnimationPlayer(p, 2); // first wrap allowed
99
+ expect(p.time).toBeCloseTo(1);
100
+ expect(p.playing).toBe(true);
101
+ advanceAnimationPlayer(p, 10); // budget now exhausted -> stop
102
+ expect(p.playing).toBe(false);
103
+ });
104
+
105
+ it('emits onLooped when looping wraps (signals enabled)', () => {
106
+ const p = player(10, { loop: true, time: 9 });
107
+ enableAnimationPlayerSignals(p);
108
+ let looped = 0;
109
+ connectSignal(p.onLooped!, () => looped++);
110
+ advanceAnimationPlayer(p, 3);
111
+ expect(looped).toBe(1);
112
+ });
113
+
114
+ it('emits onFinished when a non-looping run reaches the end (signals enabled)', () => {
115
+ const p = player(10, { loop: false, time: 9 });
116
+ enableAnimationPlayerSignals(p);
117
+ let finished = 0;
118
+ connectSignal(p.onFinished!, () => finished++);
119
+ advanceAnimationPlayer(p, 5);
120
+ expect(finished).toBe(1);
121
+ });
122
+ });
123
+
124
+ describe('cloneAnimationPlayer', () => {
125
+ it('copies fields, shares the clip, and starts signal-free', () => {
126
+ const p = player(10, { loop: false, loopMode: 'PingPong', repeatCount: 3, speed: 2, time: 4 });
127
+ enableAnimationPlayerSignals(p);
128
+ const clone = cloneAnimationPlayer(p);
129
+ expect(clone).not.toBe(p);
130
+ expect(clone.clip).toBe(p.clip);
131
+ expect(clone.loop).toBe(false);
132
+ expect(clone.loopMode).toBe('PingPong');
133
+ expect(clone.repeatCount).toBe(3);
134
+ expect(clone.speed).toBe(2);
135
+ expect(clone.time).toBe(4);
136
+ expect(clone.onFinished).toBeNull();
137
+ expect(clone.onLooped).toBeNull();
138
+ });
61
139
  });
62
140
 
63
141
  describe('createAnimationPlayer', () => {
@@ -68,6 +146,56 @@ describe('createAnimationPlayer', () => {
68
146
  expect(p.speed).toBe(1);
69
147
  expect(p.time).toBe(0);
70
148
  });
149
+
150
+ it('defaults to Repeat loop mode, infinite repeats, and no signals', () => {
151
+ const p = player(5);
152
+ expect(p.loopMode).toBe('Repeat');
153
+ expect(p.repeatCount).toBe(-1);
154
+ expect(p.onFinished).toBeNull();
155
+ expect(p.onLooped).toBeNull();
156
+ });
157
+ });
158
+
159
+ describe('enableAnimationPlayerSignals', () => {
160
+ it('allocates the opt-in signals', () => {
161
+ const p = player(5);
162
+ enableAnimationPlayerSignals(p);
163
+ expect(p.onFinished).not.toBeNull();
164
+ expect(p.onLooped).not.toBeNull();
165
+ });
166
+
167
+ it('is idempotent (keeps the existing signal instances)', () => {
168
+ const p = player(5);
169
+ enableAnimationPlayerSignals(p);
170
+ const finished = p.onFinished;
171
+ const looped = p.onLooped;
172
+ enableAnimationPlayerSignals(p);
173
+ expect(p.onFinished).toBe(finished);
174
+ expect(p.onLooped).toBe(looped);
175
+ });
176
+ });
177
+
178
+ describe('getAnimationPlayerNormalizedTime', () => {
179
+ it('returns time / duration in [0, 1]', () => {
180
+ const p = player(10, { time: 2.5 });
181
+ expect(getAnimationPlayerNormalizedTime(p)).toBeCloseTo(0.25);
182
+ seekAnimationPlayer(p, 10);
183
+ expect(getAnimationPlayerNormalizedTime(p)).toBe(1);
184
+ });
185
+
186
+ it('returns 0 for a zero-duration clip', () => {
187
+ const p = player(0);
188
+ expect(getAnimationPlayerNormalizedTime(p)).toBe(0);
189
+ });
190
+ });
191
+
192
+ describe('playAnimationPlayer', () => {
193
+ it('sets playing true without moving the playhead', () => {
194
+ const p = player(10, { playing: false, time: 3 });
195
+ playAnimationPlayer(p);
196
+ expect(p.playing).toBe(true);
197
+ expect(p.time).toBe(3);
198
+ });
71
199
  });
72
200
 
73
201
  describe('seekAnimationPlayer', () => {
@@ -81,3 +209,12 @@ describe('seekAnimationPlayer', () => {
81
209
  expect(p.time).toBe(10);
82
210
  });
83
211
  });
212
+
213
+ describe('stopAnimationPlayer', () => {
214
+ it('clears playing and rewinds to 0', () => {
215
+ const p = player(10, { time: 5 });
216
+ stopAnimationPlayer(p);
217
+ expect(p.playing).toBe(false);
218
+ expect(p.time).toBe(0);
219
+ });
220
+ });
@@ -1,6 +1,48 @@
1
1
  import { AnimationInterpolationCubic, AnimationInterpolationLinear, AnimationInterpolationStep } from '@flighthq/types';
2
2
 
3
- import { createAnimationTrack, sampleAnimationTrack } from './animationTrack';
3
+ import {
4
+ cloneAnimationTrack,
5
+ createAnimationTrack,
6
+ sampleAnimationTrack,
7
+ trimAnimationTrack,
8
+ validateAnimationTrack,
9
+ } from './animationTrack';
10
+
11
+ describe('cloneAnimationTrack', () => {
12
+ it('deep-copies times and values so the clone shares no buffers', () => {
13
+ const track = createAnimationTrack({ components: 2, times: [0, 1], values: [0, 0, 2, 4] });
14
+ const clone = cloneAnimationTrack(track);
15
+ expect(clone.times).not.toBe(track.times);
16
+ expect(clone.values).not.toBe(track.values);
17
+ expect(Array.from(clone.times)).toEqual([0, 1]);
18
+ expect(Array.from(clone.values)).toEqual([0, 0, 2, 4]);
19
+ (clone.values as number[])[0] = 99;
20
+ expect((track.values as number[])[0]).toBe(0);
21
+ });
22
+
23
+ it('preserves a Float32Array backing', () => {
24
+ const track = createAnimationTrack({ times: new Float32Array([0, 1]), values: new Float32Array([0, 10]) });
25
+ const clone = cloneAnimationTrack(track);
26
+ expect(clone.times).toBeInstanceOf(Float32Array);
27
+ expect(clone.values).toBeInstanceOf(Float32Array);
28
+ });
29
+
30
+ it('carries scalar fields and the easing reference', () => {
31
+ const easing = (a: number) => a;
32
+ const track = createAnimationTrack({
33
+ components: 3,
34
+ easing,
35
+ interpolation: AnimationInterpolationStep,
36
+ quaternion: false,
37
+ times: [0],
38
+ values: [0, 0, 0],
39
+ });
40
+ const clone = cloneAnimationTrack(track);
41
+ expect(clone.components).toBe(3);
42
+ expect(clone.interpolation).toBe(AnimationInterpolationStep);
43
+ expect(clone.easing).toBe(easing);
44
+ });
45
+ });
4
46
 
5
47
  describe('createAnimationTrack', () => {
6
48
  it('fills defaults (linear, 1 component, non-quaternion, no easing)', () => {
@@ -20,6 +62,17 @@ describe('sampleAnimationTrack', () => {
20
62
  expect(out[0]).toBeCloseTo(5);
21
63
  });
22
64
 
65
+ it('locates the correct segment on a multi-key track (binary search parity)', () => {
66
+ // A monotonically increasing ramp: value == time * 10 everywhere, so any sample must land on the
67
+ // right segment for the linear result to match. Exercises the binary search across many segments.
68
+ const track = createAnimationTrack({ times: [0, 1, 2, 3, 4, 5], values: [0, 10, 20, 30, 40, 50] });
69
+ const out = [0];
70
+ for (const t of [0.25, 0.99, 1, 1.5, 2.5, 3.75, 4.999]) {
71
+ sampleAnimationTrack(out, track, t);
72
+ expect(out[0]).toBeCloseTo(t * 10, 6);
73
+ }
74
+ });
75
+
23
76
  it('clamps before the first and after the last keyframe', () => {
24
77
  const track = createAnimationTrack({ times: [1, 2], values: [3, 9] });
25
78
  const out = [0];
@@ -112,3 +165,81 @@ describe('sampleAnimationTrack', () => {
112
165
  expect(out[0]).toBe(99);
113
166
  });
114
167
  });
168
+
169
+ describe('trimAnimationTrack', () => {
170
+ it('keeps in-range keyframes and rebases times to start at 0', () => {
171
+ const track = createAnimationTrack({ times: [0, 1, 2, 3, 4], values: [0, 10, 20, 30, 40] });
172
+ const sub = trimAnimationTrack(track, 1, 3);
173
+ expect(Array.from(sub.times)).toEqual([0, 1, 2]);
174
+ expect(Array.from(sub.values)).toEqual([10, 20, 30]);
175
+ });
176
+
177
+ it('deep-copies value blocks for multi-component tracks', () => {
178
+ const track = createAnimationTrack({ components: 2, times: [0, 1, 2], values: [0, 0, 2, 4, 8, 16] });
179
+ const sub = trimAnimationTrack(track, 1, 2);
180
+ expect(Array.from(sub.times)).toEqual([0, 1]);
181
+ expect(Array.from(sub.values)).toEqual([2, 4, 8, 16]);
182
+ expect(sub.values).not.toBe(track.values);
183
+ });
184
+
185
+ it('copies the 3x cubic keyframe stride', () => {
186
+ const track = createAnimationTrack({
187
+ interpolation: AnimationInterpolationCubic,
188
+ times: [0, 1, 2],
189
+ values: [0, 0, 0, 1, 10, 1, 2, 20, 2],
190
+ });
191
+ const sub = trimAnimationTrack(track, 1, 2);
192
+ expect(Array.from(sub.times)).toEqual([0, 1]);
193
+ expect(Array.from(sub.values)).toEqual([1, 10, 1, 2, 20, 2]);
194
+ });
195
+
196
+ it('returns an empty track when no keyframe falls in range', () => {
197
+ const track = createAnimationTrack({ times: [0, 1, 2], values: [0, 10, 20] });
198
+ const sub = trimAnimationTrack(track, 5, 6);
199
+ expect(sub.times.length).toBe(0);
200
+ expect(sub.values.length).toBe(0);
201
+ });
202
+ });
203
+
204
+ describe('validateAnimationTrack', () => {
205
+ it('returns null for a well-formed track', () => {
206
+ const track = createAnimationTrack({ components: 3, times: [0, 1, 2], values: [0, 0, 0, 1, 1, 1, 2, 2, 2] });
207
+ expect(validateAnimationTrack(track)).toBeNull();
208
+ });
209
+
210
+ it('flags non-ascending times with the offending index', () => {
211
+ const track = createAnimationTrack({ times: [0, 2, 1], values: [0, 1, 2] });
212
+ const diagnostics = validateAnimationTrack(track);
213
+ expect(diagnostics).not.toBeNull();
214
+ expect(diagnostics![0].code).toBe('nonAscendingTimes');
215
+ expect(diagnostics![0].index).toBe(2);
216
+ });
217
+
218
+ it('flags a values-length mismatch', () => {
219
+ const track = createAnimationTrack({ components: 2, times: [0, 1], values: [0, 0, 2] });
220
+ const diagnostics = validateAnimationTrack(track);
221
+ expect(diagnostics).not.toBeNull();
222
+ expect(diagnostics!.some((d) => d.code === 'valuesLengthMismatch')).toBe(true);
223
+ });
224
+
225
+ it('accounts for the 3x cubic stride in the length check', () => {
226
+ const ok = createAnimationTrack({
227
+ interpolation: AnimationInterpolationCubic,
228
+ times: [0, 1],
229
+ values: [0, 0, 0, 0, 10, 0],
230
+ });
231
+ expect(validateAnimationTrack(ok)).toBeNull();
232
+ const bad = createAnimationTrack({
233
+ interpolation: AnimationInterpolationCubic,
234
+ times: [0, 1],
235
+ values: [0, 0, 0, 0],
236
+ });
237
+ expect(validateAnimationTrack(bad)).not.toBeNull();
238
+ });
239
+
240
+ it('does not throw on a duplicate time (reports it instead)', () => {
241
+ const track = createAnimationTrack({ times: [0, 0], values: [0, 1] });
242
+ const diagnostics = validateAnimationTrack(track);
243
+ expect(diagnostics![0].code).toBe('nonAscendingTimes');
244
+ });
245
+ });