@flighthq/animation 0.1.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.
- package/dist/animationClip.d.ts +5 -0
- package/dist/animationClip.d.ts.map +1 -0
- package/dist/animationClip.js +23 -0
- package/dist/animationClip.js.map +1 -0
- package/dist/animationPlayer.d.ts +10 -0
- package/dist/animationPlayer.d.ts.map +1 -0
- package/dist/animationPlayer.js +44 -0
- package/dist/animationPlayer.js.map +1 -0
- package/dist/animationTrack.d.ts +11 -0
- package/dist/animationTrack.d.ts.map +1 -0
- package/dist/animationTrack.js +146 -0
- package/dist/animationTrack.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/package.json +37 -0
- package/src/animationClip.test.ts +38 -0
- package/src/animationPlayer.test.ts +83 -0
- package/src/animationTrack.test.ts +114 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AnimationChannel, AnimationClip, AnimationTrack } from '@flighthq/types';
|
|
2
|
+
export declare function createAnimationChannel(track: AnimationTrack, targetRef: unknown): AnimationChannel;
|
|
3
|
+
export declare function createAnimationClip(channels: AnimationChannel[], duration?: number): AnimationClip;
|
|
4
|
+
export declare function getAnimationClipDuration(clip: Readonly<AnimationClip>): number;
|
|
5
|
+
//# sourceMappingURL=animationClip.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Pairs a track with an opaque target reference (interpreted only by the domain binding layer).
|
|
2
|
+
export function createAnimationChannel(track, targetRef) {
|
|
3
|
+
return { targetRef, track };
|
|
4
|
+
}
|
|
5
|
+
// Bundles channels into a clip. `duration` defaults to the latest keyframe time across all channels.
|
|
6
|
+
export function createAnimationClip(channels, duration) {
|
|
7
|
+
return { channels, duration: duration ?? computeChannelsDuration(channels) };
|
|
8
|
+
}
|
|
9
|
+
// Returns the clip's total duration in seconds.
|
|
10
|
+
export function getAnimationClipDuration(clip) {
|
|
11
|
+
return clip.duration;
|
|
12
|
+
}
|
|
13
|
+
function computeChannelsDuration(channels) {
|
|
14
|
+
let max = 0;
|
|
15
|
+
for (const channel of channels) {
|
|
16
|
+
const times = channel.track.times;
|
|
17
|
+
const last = times.length;
|
|
18
|
+
if (last > 0 && times[last - 1] > max)
|
|
19
|
+
max = times[last - 1];
|
|
20
|
+
}
|
|
21
|
+
return max;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=animationClip.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AnimationClip, AnimationPlayer } from '@flighthq/types';
|
|
2
|
+
export declare function advanceAnimationPlayer(player: AnimationPlayer, dt: number): void;
|
|
3
|
+
export declare function createAnimationPlayer(clip: AnimationClip, opts?: Readonly<{
|
|
4
|
+
loop?: boolean;
|
|
5
|
+
playing?: boolean;
|
|
6
|
+
speed?: number;
|
|
7
|
+
time?: number;
|
|
8
|
+
}>): AnimationPlayer;
|
|
9
|
+
export declare function seekAnimationPlayer(player: AnimationPlayer, time: number): void;
|
|
10
|
+
//# sourceMappingURL=animationPlayer.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// 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
|
+
// frame — nothing advances on its own.
|
|
5
|
+
export function advanceAnimationPlayer(player, dt) {
|
|
6
|
+
if (!player.playing)
|
|
7
|
+
return;
|
|
8
|
+
const duration = player.clip.duration;
|
|
9
|
+
if (duration <= 0) {
|
|
10
|
+
player.time = 0;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
let time = player.time + dt * player.speed;
|
|
14
|
+
if (player.loop) {
|
|
15
|
+
time %= duration;
|
|
16
|
+
if (time < 0)
|
|
17
|
+
time += duration;
|
|
18
|
+
}
|
|
19
|
+
else if (time >= duration) {
|
|
20
|
+
time = duration;
|
|
21
|
+
player.playing = false;
|
|
22
|
+
}
|
|
23
|
+
else if (time < 0) {
|
|
24
|
+
time = 0;
|
|
25
|
+
player.playing = false;
|
|
26
|
+
}
|
|
27
|
+
player.time = time;
|
|
28
|
+
}
|
|
29
|
+
// Allocates a player over `clip`. Defaults: looping, playing, speed 1, time 0.
|
|
30
|
+
export function createAnimationPlayer(clip, opts) {
|
|
31
|
+
return {
|
|
32
|
+
clip,
|
|
33
|
+
loop: opts?.loop ?? true,
|
|
34
|
+
playing: opts?.playing ?? true,
|
|
35
|
+
speed: opts?.speed ?? 1,
|
|
36
|
+
time: opts?.time ?? 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Sets the playhead to `time`, clamped to [0, clip.duration]. Does not change `playing`.
|
|
40
|
+
export function seekAnimationPlayer(player, time) {
|
|
41
|
+
const duration = player.clip.duration;
|
|
42
|
+
player.time = time < 0 ? 0 : time > duration ? duration : time;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=animationPlayer.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AnimationInterpolation, AnimationTrack } from '@flighthq/types';
|
|
2
|
+
export declare function createAnimationTrack(opts: {
|
|
3
|
+
times: ArrayLike<number>;
|
|
4
|
+
values: ArrayLike<number>;
|
|
5
|
+
components?: number;
|
|
6
|
+
interpolation?: AnimationInterpolation;
|
|
7
|
+
quaternion?: boolean;
|
|
8
|
+
easing?: AnimationTrack['easing'];
|
|
9
|
+
}): AnimationTrack;
|
|
10
|
+
export declare function sampleAnimationTrack(out: number[] | Float32Array, track: Readonly<AnimationTrack>, t: number): void;
|
|
11
|
+
//# sourceMappingURL=animationTrack.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { AnimationInterpolationLinear } from '@flighthq/types';
|
|
2
|
+
// Allocates an AnimationTrack. `times` must be ascending; `values` is the flat keyframe buffer
|
|
3
|
+
// (`components` numbers per keyframe for step/linear, 3 * `components` for cubic). Defaults:
|
|
4
|
+
// linear interpolation, 1 component, non-quaternion, no easing.
|
|
5
|
+
export function createAnimationTrack(opts) {
|
|
6
|
+
return {
|
|
7
|
+
components: opts.components ?? 1,
|
|
8
|
+
easing: opts.easing ?? null,
|
|
9
|
+
interpolation: opts.interpolation ?? AnimationInterpolationLinear,
|
|
10
|
+
quaternion: opts.quaternion ?? false,
|
|
11
|
+
times: opts.times,
|
|
12
|
+
values: opts.values,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
// Samples `track` at time `t`, writing `track.components` numbers into `out`. `t` is clamped to the
|
|
16
|
+
// track's time range (before the first keyframe yields the first value; after the last yields the
|
|
17
|
+
// last). Step holds the previous keyframe; Linear interpolates component-wise (or slerps a quaternion
|
|
18
|
+
// track); Cubic is a glTF-style Hermite spline over the per-keyframe in/out tangents. A non-null
|
|
19
|
+
// `track.easing` reshapes the per-segment alpha first. Alloc-free; safe for hot loops.
|
|
20
|
+
export function sampleAnimationTrack(out, track, t) {
|
|
21
|
+
const { components, times, values } = track;
|
|
22
|
+
const count = times.length;
|
|
23
|
+
if (count === 0) {
|
|
24
|
+
for (let c = 0; c < components; c++)
|
|
25
|
+
out[c] = 0;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (count === 1 || t <= times[0]) {
|
|
29
|
+
copyKeyframeValue(out, track, 0);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (t >= times[count - 1]) {
|
|
33
|
+
copyKeyframeValue(out, track, count - 1);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
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++;
|
|
40
|
+
const t0 = times[i];
|
|
41
|
+
const dt = times[i + 1] - t0;
|
|
42
|
+
let alpha = dt > 0 ? (t - t0) / dt : 0;
|
|
43
|
+
if (track.easing !== null)
|
|
44
|
+
alpha = track.easing(alpha);
|
|
45
|
+
if (track.interpolation === 'Step') {
|
|
46
|
+
copyKeyframeValue(out, track, i);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (track.interpolation === 'Cubic') {
|
|
50
|
+
sampleCubicSegment(out, track, i, alpha, dt);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Linear.
|
|
54
|
+
const oi = keyframeValueOffset(track, i);
|
|
55
|
+
const oj = keyframeValueOffset(track, i + 1);
|
|
56
|
+
if (track.quaternion && components === 4) {
|
|
57
|
+
slerpFlatQuaternion(out, values, oi, oj, alpha);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (let c = 0; c < components; c++) {
|
|
61
|
+
const a = values[oi + c];
|
|
62
|
+
out[c] = a + (values[oj + c] - a) * alpha;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Byte width of one keyframe block in the flat value buffer (cubic stores in/out tangents alongside
|
|
66
|
+
// the value, so 3x).
|
|
67
|
+
function keyframeStride(track) {
|
|
68
|
+
return track.interpolation === 'Cubic' ? track.components * 3 : track.components;
|
|
69
|
+
}
|
|
70
|
+
// Offset of keyframe `k`'s VALUE within the flat buffer (the middle slot for cubic, where the layout
|
|
71
|
+
// is [inTangent, value, outTangent]).
|
|
72
|
+
function keyframeValueOffset(track, k) {
|
|
73
|
+
const stride = keyframeStride(track);
|
|
74
|
+
return track.interpolation === 'Cubic' ? k * stride + track.components : k * stride;
|
|
75
|
+
}
|
|
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];
|
|
80
|
+
}
|
|
81
|
+
// glTF cubic-spline (Hermite) interpolation of segment [i, i+1] at `alpha`, with `dt` the segment
|
|
82
|
+
// duration (tangents are derivatives, scaled by dt). Quaternion tracks are interpolated component-wise
|
|
83
|
+
// then renormalized.
|
|
84
|
+
function sampleCubicSegment(out, track, i, alpha, dt) {
|
|
85
|
+
const { components, values } = track;
|
|
86
|
+
const stride = components * 3;
|
|
87
|
+
const a2 = alpha * alpha;
|
|
88
|
+
const a3 = a2 * alpha;
|
|
89
|
+
const h00 = 2 * a3 - 3 * a2 + 1;
|
|
90
|
+
const h10 = a3 - 2 * a2 + alpha;
|
|
91
|
+
const h01 = -2 * a3 + 3 * a2;
|
|
92
|
+
const h11 = a3 - a2;
|
|
93
|
+
const base0 = i * stride;
|
|
94
|
+
const base1 = (i + 1) * stride;
|
|
95
|
+
for (let c = 0; c < components; c++) {
|
|
96
|
+
const p0 = values[base0 + components + c]; // value at i
|
|
97
|
+
const m0 = values[base0 + components * 2 + c]; // out-tangent at i
|
|
98
|
+
const p1 = values[base1 + components + c]; // value at i+1
|
|
99
|
+
const m1 = values[base1 + c]; // in-tangent at i+1
|
|
100
|
+
out[c] = h00 * p0 + h10 * dt * m0 + h01 * p1 + h11 * dt * m1;
|
|
101
|
+
}
|
|
102
|
+
if (track.quaternion && components === 4)
|
|
103
|
+
normalizeFlatQuaternion(out);
|
|
104
|
+
}
|
|
105
|
+
// Spherical-linear interpolation of two unit quaternions stored flat at `oa`/`ob` in `values`, written
|
|
106
|
+
// to `out[0..3]`. Picks the shorter arc; falls back to normalized-lerp for nearly-parallel quaternions.
|
|
107
|
+
function slerpFlatQuaternion(out, values, oa, ob, alpha) {
|
|
108
|
+
const ax = values[oa], ay = values[oa + 1], az = values[oa + 2], aw = values[oa + 3];
|
|
109
|
+
let bx = values[ob], by = values[ob + 1], bz = values[ob + 2], bw = values[ob + 3];
|
|
110
|
+
let cosom = ax * bx + ay * by + az * bz + aw * bw;
|
|
111
|
+
if (cosom < 0) {
|
|
112
|
+
cosom = -cosom;
|
|
113
|
+
bx = -bx;
|
|
114
|
+
by = -by;
|
|
115
|
+
bz = -bz;
|
|
116
|
+
bw = -bw;
|
|
117
|
+
}
|
|
118
|
+
let scale0;
|
|
119
|
+
let scale1;
|
|
120
|
+
if (1 - cosom > 1e-6) {
|
|
121
|
+
const omega = Math.acos(cosom);
|
|
122
|
+
const sinom = Math.sin(omega);
|
|
123
|
+
scale0 = Math.sin((1 - alpha) * omega) / sinom;
|
|
124
|
+
scale1 = Math.sin(alpha * omega) / sinom;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
scale0 = 1 - alpha;
|
|
128
|
+
scale1 = alpha;
|
|
129
|
+
}
|
|
130
|
+
out[0] = scale0 * ax + scale1 * bx;
|
|
131
|
+
out[1] = scale0 * ay + scale1 * by;
|
|
132
|
+
out[2] = scale0 * az + scale1 * bz;
|
|
133
|
+
out[3] = scale0 * aw + scale1 * bw;
|
|
134
|
+
}
|
|
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
|
+
//# sourceMappingURL=animationTrack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animationTrack.js","sourceRoot":"","sources":["../src/animationTrack.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/animation",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/types": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"typescript": "^5.3.0"
|
|
34
|
+
},
|
|
35
|
+
"description": "Target-free animation core: keyframe sampler (step/linear/cubic, quaternion slerp), clips, and an explicit playhead",
|
|
36
|
+
"sideEffects": false
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createAnimationChannel, createAnimationClip, getAnimationClipDuration } from './animationClip';
|
|
2
|
+
import { createAnimationTrack } from './animationTrack';
|
|
3
|
+
|
|
4
|
+
function track(times: number[]) {
|
|
5
|
+
return createAnimationTrack({ times, values: times.map(() => 0) });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe('createAnimationChannel', () => {
|
|
9
|
+
it('pairs a track with an opaque target ref', () => {
|
|
10
|
+
const t = track([0, 1]);
|
|
11
|
+
const target = {};
|
|
12
|
+
const channel = createAnimationChannel(t, target);
|
|
13
|
+
expect(channel.track).toBe(t);
|
|
14
|
+
expect(channel.targetRef).toBe(target);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('createAnimationClip', () => {
|
|
19
|
+
it('derives duration from the latest keyframe across channels', () => {
|
|
20
|
+
const clip = createAnimationClip([
|
|
21
|
+
createAnimationChannel(track([0, 1]), null),
|
|
22
|
+
createAnimationChannel(track([0, 2.5]), null),
|
|
23
|
+
]);
|
|
24
|
+
expect(clip.duration).toBe(2.5);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('honors an explicit duration override', () => {
|
|
28
|
+
const clip = createAnimationClip([createAnimationChannel(track([0, 1]), null)], 10);
|
|
29
|
+
expect(clip.duration).toBe(10);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('getAnimationClipDuration', () => {
|
|
34
|
+
it('returns the clip duration', () => {
|
|
35
|
+
const clip = createAnimationClip([], 4);
|
|
36
|
+
expect(getAnimationClipDuration(clip)).toBe(4);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createAnimationClip } from './animationClip';
|
|
2
|
+
import { advanceAnimationPlayer, createAnimationPlayer, seekAnimationPlayer } from './animationPlayer';
|
|
3
|
+
|
|
4
|
+
function player(duration: number, opts?: Parameters<typeof createAnimationPlayer>[1]) {
|
|
5
|
+
return createAnimationPlayer(createAnimationClip([], duration), opts);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe('advanceAnimationPlayer', () => {
|
|
9
|
+
it('advances the playhead by dt * speed', () => {
|
|
10
|
+
const p = player(10, { loop: false, speed: 2 });
|
|
11
|
+
advanceAnimationPlayer(p, 1);
|
|
12
|
+
expect(p.time).toBe(2);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('wraps modulo duration when looping', () => {
|
|
16
|
+
const p = player(10, { loop: true, time: 9 });
|
|
17
|
+
advanceAnimationPlayer(p, 3);
|
|
18
|
+
expect(p.time).toBeCloseTo(2);
|
|
19
|
+
expect(p.playing).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('clamps and stops at the end when not looping', () => {
|
|
23
|
+
const p = player(10, { loop: false, time: 9 });
|
|
24
|
+
advanceAnimationPlayer(p, 5);
|
|
25
|
+
expect(p.time).toBe(10);
|
|
26
|
+
expect(p.playing).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('is a no-op for a zero-duration clip', () => {
|
|
30
|
+
const p = player(0, { time: 0 });
|
|
31
|
+
advanceAnimationPlayer(p, 1);
|
|
32
|
+
expect(p.time).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('is a no-op while paused', () => {
|
|
36
|
+
const p = player(10, { playing: false, time: 3 });
|
|
37
|
+
advanceAnimationPlayer(p, 5);
|
|
38
|
+
expect(p.time).toBe(3);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('plays backward with negative speed', () => {
|
|
42
|
+
const p = player(10, { speed: -1, time: 5 });
|
|
43
|
+
advanceAnimationPlayer(p, 1);
|
|
44
|
+
expect(p.time).toBeCloseTo(4);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('stops at time 0 when playing backward without looping', () => {
|
|
48
|
+
const p = player(10, { speed: -1, loop: false, time: 0.5 });
|
|
49
|
+
advanceAnimationPlayer(p, 2);
|
|
50
|
+
expect(p.time).toBe(0);
|
|
51
|
+
expect(p.playing).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('wraps backward when looping with negative speed', () => {
|
|
55
|
+
// time=0.5, speed=-1, dt=2 -> raw=-1.5; -1.5 % 10 = -1.5; +10 = 8.5
|
|
56
|
+
const p = player(10, { speed: -1, loop: true, time: 0.5 });
|
|
57
|
+
advanceAnimationPlayer(p, 2);
|
|
58
|
+
expect(p.time).toBeCloseTo(8.5);
|
|
59
|
+
expect(p.playing).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('createAnimationPlayer', () => {
|
|
64
|
+
it('defaults to looping, playing, speed 1, time 0', () => {
|
|
65
|
+
const p = player(5);
|
|
66
|
+
expect(p.loop).toBe(true);
|
|
67
|
+
expect(p.playing).toBe(true);
|
|
68
|
+
expect(p.speed).toBe(1);
|
|
69
|
+
expect(p.time).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('seekAnimationPlayer', () => {
|
|
74
|
+
it('clamps the playhead to [0, duration]', () => {
|
|
75
|
+
const p = player(10);
|
|
76
|
+
seekAnimationPlayer(p, 4);
|
|
77
|
+
expect(p.time).toBe(4);
|
|
78
|
+
seekAnimationPlayer(p, -1);
|
|
79
|
+
expect(p.time).toBe(0);
|
|
80
|
+
seekAnimationPlayer(p, 99);
|
|
81
|
+
expect(p.time).toBe(10);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { AnimationInterpolationCubic, AnimationInterpolationLinear, AnimationInterpolationStep } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { createAnimationTrack, sampleAnimationTrack } from './animationTrack';
|
|
4
|
+
|
|
5
|
+
describe('createAnimationTrack', () => {
|
|
6
|
+
it('fills defaults (linear, 1 component, non-quaternion, no easing)', () => {
|
|
7
|
+
const track = createAnimationTrack({ times: [0, 1], values: [0, 1] });
|
|
8
|
+
expect(track.interpolation).toBe(AnimationInterpolationLinear);
|
|
9
|
+
expect(track.components).toBe(1);
|
|
10
|
+
expect(track.quaternion).toBe(false);
|
|
11
|
+
expect(track.easing).toBeNull();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('sampleAnimationTrack', () => {
|
|
16
|
+
it('linearly interpolates a scalar mid-segment', () => {
|
|
17
|
+
const track = createAnimationTrack({ times: [0, 1], values: [0, 10] });
|
|
18
|
+
const out = [0];
|
|
19
|
+
sampleAnimationTrack(out, track, 0.5);
|
|
20
|
+
expect(out[0]).toBeCloseTo(5);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('clamps before the first and after the last keyframe', () => {
|
|
24
|
+
const track = createAnimationTrack({ times: [1, 2], values: [3, 9] });
|
|
25
|
+
const out = [0];
|
|
26
|
+
sampleAnimationTrack(out, track, -5);
|
|
27
|
+
expect(out[0]).toBe(3);
|
|
28
|
+
sampleAnimationTrack(out, track, 99);
|
|
29
|
+
expect(out[0]).toBe(9);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('holds the previous keyframe for step interpolation', () => {
|
|
33
|
+
const track = createAnimationTrack({ interpolation: AnimationInterpolationStep, times: [0, 1], values: [0, 10] });
|
|
34
|
+
const out = [0];
|
|
35
|
+
sampleAnimationTrack(out, track, 0.9);
|
|
36
|
+
expect(out[0]).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('interpolates a Vector3 component-wise', () => {
|
|
40
|
+
const track = createAnimationTrack({ components: 3, times: [0, 1], values: [0, 0, 0, 2, 4, 6] });
|
|
41
|
+
const out = [0, 0, 0];
|
|
42
|
+
sampleAnimationTrack(out, track, 0.5);
|
|
43
|
+
expect(out).toEqual([1, 2, 3]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('slerps a quaternion track to the half-angle', () => {
|
|
47
|
+
// identity -> 90deg about +Z; midpoint is 45deg about +Z.
|
|
48
|
+
const s = Math.sin(Math.PI / 4);
|
|
49
|
+
const c = Math.cos(Math.PI / 4);
|
|
50
|
+
const track = createAnimationTrack({
|
|
51
|
+
components: 4,
|
|
52
|
+
quaternion: true,
|
|
53
|
+
times: [0, 1],
|
|
54
|
+
values: [0, 0, 0, 1, 0, 0, s, c],
|
|
55
|
+
});
|
|
56
|
+
const out = [0, 0, 0, 0];
|
|
57
|
+
sampleAnimationTrack(out, track, 0.5);
|
|
58
|
+
expect(out[2]).toBeCloseTo(Math.sin(Math.PI / 8), 5);
|
|
59
|
+
expect(out[3]).toBeCloseTo(Math.cos(Math.PI / 8), 5);
|
|
60
|
+
expect(Math.hypot(out[0], out[1], out[2], out[3])).toBeCloseTo(1, 5);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('cubic-splines to the midpoint with zero tangents', () => {
|
|
64
|
+
// Per-keyframe layout [inTangent, value, outTangent]; zero tangents -> smoothstep, 0.5 -> midpoint.
|
|
65
|
+
const track = createAnimationTrack({
|
|
66
|
+
interpolation: AnimationInterpolationCubic,
|
|
67
|
+
times: [0, 1],
|
|
68
|
+
values: [0, 0, 0, 0, 10, 0],
|
|
69
|
+
});
|
|
70
|
+
const out = [0];
|
|
71
|
+
sampleAnimationTrack(out, track, 0.5);
|
|
72
|
+
expect(out[0]).toBeCloseTo(5);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('applies a non-null easing to the segment alpha', () => {
|
|
76
|
+
const track = createAnimationTrack({ easing: () => 0, times: [0, 1], values: [0, 10] });
|
|
77
|
+
const out = [0];
|
|
78
|
+
sampleAnimationTrack(out, track, 0.75);
|
|
79
|
+
// easing forces alpha to 0 -> first keyframe value regardless of t.
|
|
80
|
+
expect(out[0]).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns the first value when t is exactly on the first keyframe', () => {
|
|
84
|
+
const track = createAnimationTrack({ times: [2, 5], values: [7, 42] });
|
|
85
|
+
const out = [0];
|
|
86
|
+
sampleAnimationTrack(out, track, 2);
|
|
87
|
+
expect(out[0]).toBe(7);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns the last value when t is exactly on the last keyframe', () => {
|
|
91
|
+
const track = createAnimationTrack({ times: [2, 5], values: [7, 42] });
|
|
92
|
+
const out = [0];
|
|
93
|
+
sampleAnimationTrack(out, track, 5);
|
|
94
|
+
expect(out[0]).toBe(42);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns zeros for all components when the track is empty', () => {
|
|
98
|
+
const track = createAnimationTrack({ components: 3, times: [], values: [] });
|
|
99
|
+
const out = [9, 9, 9];
|
|
100
|
+
sampleAnimationTrack(out, track, 0);
|
|
101
|
+
expect(out).toEqual([0, 0, 0]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns the only keyframe value for a single-keyframe track', () => {
|
|
105
|
+
const track = createAnimationTrack({ times: [5], values: [99] });
|
|
106
|
+
const out = [0];
|
|
107
|
+
sampleAnimationTrack(out, track, 0);
|
|
108
|
+
expect(out[0]).toBe(99);
|
|
109
|
+
sampleAnimationTrack(out, track, 5);
|
|
110
|
+
expect(out[0]).toBe(99);
|
|
111
|
+
sampleAnimationTrack(out, track, 1000);
|
|
112
|
+
expect(out[0]).toBe(99);
|
|
113
|
+
});
|
|
114
|
+
});
|