@flighthq/animation 0.2.1-next.640.ec2ea9a → 0.2.1-next.652.c550eaa
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/animationCrossfade.d.ts +6 -0
- package/dist/animationCrossfade.d.ts.map +1 -0
- package/dist/animationCrossfade.js +120 -0
- package/dist/animationCrossfade.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/animationBlend.test.ts +10 -0
- package/src/animationCrossfade.test.ts +223 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AnimationChannel, AnimationCrossfade, AnimationCrossfadeOptions, AnimationPlayer } from '@flighthq/types';
|
|
2
|
+
export declare function advanceAnimationCrossfade(state: AnimationCrossfade, dt: number): void;
|
|
3
|
+
export declare function createAnimationCrossfade(from: AnimationPlayer, to: AnimationPlayer, duration: number, opts?: Readonly<AnimationCrossfadeOptions>): AnimationCrossfade;
|
|
4
|
+
export declare function isAnimationCrossfadeComplete(state: Readonly<AnimationCrossfade>): boolean;
|
|
5
|
+
export declare function sampleAnimationCrossfade(out: number[] | Float32Array, state: Readonly<AnimationCrossfade>, visit: (sampled: Readonly<number[] | Float32Array>, channel: Readonly<AnimationChannel>, index: number) => void): void;
|
|
6
|
+
//# sourceMappingURL=animationCrossfade.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animationCrossfade.d.ts","sourceRoot":"","sources":["../src/animationCrossfade.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAChB,kBAAkB,EAElB,yBAAyB,EACzB,eAAe,EAChB,MAAM,iBAAiB,CAAC;AAQzB,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAKrF;AAKD,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,eAAe,EACrB,EAAE,EAAE,eAAe,EACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,QAAQ,CAAC,yBAAyB,CAAC,GACzC,kBAAkB,CAiBpB;AAKD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAEzF;AAMD,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,EAAE,GAAG,YAAY,EAC5B,KAAK,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EACnC,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,CAkBN"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity';
|
|
2
|
+
import { blendAnimationSamples } from './animationBlend';
|
|
3
|
+
import { advanceAnimationPlayer } from './animationPlayer';
|
|
4
|
+
import { sampleAnimationTrack } from './animationTrack';
|
|
5
|
+
// Advances both source players by the same delta and updates the transition weight. Nothing advances
|
|
6
|
+
// implicitly; callers drive this alongside their other animation state.
|
|
7
|
+
export function advanceAnimationCrossfade(state, dt) {
|
|
8
|
+
advanceAnimationPlayer(state.from, dt);
|
|
9
|
+
advanceAnimationPlayer(state.to, dt);
|
|
10
|
+
state.elapsed += dt;
|
|
11
|
+
state.weight = state.curve(getLinearAnimationCrossfadeWeight(state.elapsed, state.duration));
|
|
12
|
+
}
|
|
13
|
+
// Allocates a two-player transition, target-correspondence layout, and reusable sample scratch.
|
|
14
|
+
// Channels correspond when their opaque targetRef values have identity equality. A target present in
|
|
15
|
+
// only one clip remains in the layout as a pass-through channel.
|
|
16
|
+
export function createAnimationCrossfade(from, to, duration, opts) {
|
|
17
|
+
const resolvedDuration = Math.max(0, duration);
|
|
18
|
+
const curve = opts?.curve ?? linearAnimationCrossfadeCurve;
|
|
19
|
+
const channels = createAnimationCrossfadeChannels(from, to);
|
|
20
|
+
let sampleWidth = 0;
|
|
21
|
+
for (const entry of channels)
|
|
22
|
+
sampleWidth = Math.max(sampleWidth, entry.channel.track.components);
|
|
23
|
+
return createEntity({
|
|
24
|
+
channels,
|
|
25
|
+
curve,
|
|
26
|
+
duration: resolvedDuration,
|
|
27
|
+
elapsed: 0,
|
|
28
|
+
from,
|
|
29
|
+
fromSample: new Float32Array(sampleWidth),
|
|
30
|
+
to,
|
|
31
|
+
toSample: new Float32Array(sampleWidth),
|
|
32
|
+
weight: curve(getLinearAnimationCrossfadeWeight(0, resolvedDuration)),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Reports when elapsed transition time has reached duration. Completion deliberately ignores the
|
|
36
|
+
// curved weight because back/elastic easing may overshoot 1 before the lifecycle duration ends. The
|
|
37
|
+
// caller owns retirement/removal of a completed transition; the controller has no hidden scheduler.
|
|
38
|
+
export function isAnimationCrossfadeComplete(state) {
|
|
39
|
+
return state.duration <= 0 || state.elapsed >= state.duration;
|
|
40
|
+
}
|
|
41
|
+
// Samples every target correspondence using the caller-supplied per-channel scratch and visitor,
|
|
42
|
+
// mirroring sampleAnimationClip. Matched targets blend by the destination weight; one-sided targets
|
|
43
|
+
// pass their existing clip value through unchanged. The visitor must consume `out` before returning
|
|
44
|
+
// because the same buffer is overwritten for the next channel. Allocation-free after construction.
|
|
45
|
+
export function sampleAnimationCrossfade(out, state, visit) {
|
|
46
|
+
const fromChannels = state.from.clip.channels;
|
|
47
|
+
const toChannels = state.to.clip.channels;
|
|
48
|
+
for (let index = 0; index < state.channels.length; index++) {
|
|
49
|
+
const entry = state.channels[index];
|
|
50
|
+
if (entry.fromIndex === null) {
|
|
51
|
+
sampleAnimationTrack(out, toChannels[entry.toIndex].track, state.to.time);
|
|
52
|
+
}
|
|
53
|
+
else if (entry.toIndex === null) {
|
|
54
|
+
sampleAnimationTrack(out, fromChannels[entry.fromIndex].track, state.from.time);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const fromTrack = fromChannels[entry.fromIndex].track;
|
|
58
|
+
const toTrack = toChannels[entry.toIndex].track;
|
|
59
|
+
sampleAnimationTrack(state.fromSample, fromTrack, state.from.time);
|
|
60
|
+
sampleAnimationTrack(state.toSample, toTrack, state.to.time);
|
|
61
|
+
blendAnimationSamples(out, state.fromSample, state.toSample, state.weight, fromTrack.quaternion);
|
|
62
|
+
}
|
|
63
|
+
visit(out, entry.channel, index);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function createAnimationCrossfadeChannels(from, to) {
|
|
67
|
+
const fromChannels = from.clip.channels;
|
|
68
|
+
const toChannels = to.clip.channels;
|
|
69
|
+
assertUniqueAnimationCrossfadeTargets(fromChannels, 'source');
|
|
70
|
+
assertUniqueAnimationCrossfadeTargets(toChannels, 'destination');
|
|
71
|
+
const toByTarget = new Map();
|
|
72
|
+
for (let index = 0; index < toChannels.length; index++) {
|
|
73
|
+
if (!toByTarget.has(toChannels[index].targetRef))
|
|
74
|
+
toByTarget.set(toChannels[index].targetRef, index);
|
|
75
|
+
}
|
|
76
|
+
const channels = [];
|
|
77
|
+
const matchedTo = new Set();
|
|
78
|
+
for (let fromIndex = 0; fromIndex < fromChannels.length; fromIndex++) {
|
|
79
|
+
const fromChannel = fromChannels[fromIndex];
|
|
80
|
+
const toIndex = toByTarget.get(fromChannel.targetRef);
|
|
81
|
+
if (toIndex === undefined) {
|
|
82
|
+
channels.push({ channel: fromChannel, fromIndex, toIndex: null });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const toChannel = toChannels[toIndex];
|
|
86
|
+
if (fromChannel.track.components !== toChannel.track.components) {
|
|
87
|
+
throw new RangeError(`AnimationCrossfade target has different component widths (${fromChannel.track.components} and ${toChannel.track.components}).`);
|
|
88
|
+
}
|
|
89
|
+
if (fromChannel.track.quaternion !== toChannel.track.quaternion) {
|
|
90
|
+
throw new TypeError('AnimationCrossfade target has incompatible quaternion flags.');
|
|
91
|
+
}
|
|
92
|
+
channels.push({ channel: toChannel, fromIndex, toIndex });
|
|
93
|
+
matchedTo.add(toIndex);
|
|
94
|
+
}
|
|
95
|
+
for (let toIndex = 0; toIndex < toChannels.length; toIndex++) {
|
|
96
|
+
if (!matchedTo.has(toIndex)) {
|
|
97
|
+
channels.push({ channel: toChannels[toIndex], fromIndex: null, toIndex });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return channels;
|
|
101
|
+
}
|
|
102
|
+
function assertUniqueAnimationCrossfadeTargets(channels, clipLabel) {
|
|
103
|
+
const targets = new Set();
|
|
104
|
+
for (const channel of channels) {
|
|
105
|
+
if (targets.has(channel.targetRef)) {
|
|
106
|
+
throw new TypeError(`AnimationCrossfade ${clipLabel} clip contains a duplicate targetRef.`);
|
|
107
|
+
}
|
|
108
|
+
targets.add(channel.targetRef);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function getLinearAnimationCrossfadeWeight(elapsed, duration) {
|
|
112
|
+
if (duration <= 0)
|
|
113
|
+
return 1;
|
|
114
|
+
const normalized = elapsed / duration;
|
|
115
|
+
return normalized < 0 ? 0 : normalized > 1 ? 1 : normalized;
|
|
116
|
+
}
|
|
117
|
+
function linearAnimationCrossfadeCurve(t) {
|
|
118
|
+
return t;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=animationCrossfade.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animationCrossfade.js","sourceRoot":"","sources":["../src/animationCrossfade.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/animation",
|
|
3
|
-
"version": "0.2.1-next.
|
|
3
|
+
"version": "0.2.1-next.652.c550eaa",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@flighthq/entity": "0.2.1-next.
|
|
36
|
-
"@flighthq/signals": "0.2.1-next.
|
|
37
|
-
"@flighthq/types": "0.2.1-next.
|
|
35
|
+
"@flighthq/entity": "0.2.1-next.652.c550eaa",
|
|
36
|
+
"@flighthq/signals": "0.2.1-next.652.c550eaa",
|
|
37
|
+
"@flighthq/types": "0.2.1-next.652.c550eaa"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"typescript": "^5.3.0"
|
|
@@ -46,6 +46,16 @@ describe('addAnimationSample', () => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
describe('blendAnimationSamples', () => {
|
|
49
|
+
it('supports output aliasing either input', () => {
|
|
50
|
+
const a = [0, 10];
|
|
51
|
+
blendAnimationSamples(a, a, [10, 20], 0.25);
|
|
52
|
+
expect(a).toEqual([2.5, 12.5]);
|
|
53
|
+
|
|
54
|
+
const b = [10, 20];
|
|
55
|
+
blendAnimationSamples(b, [0, 10], b, 0.25);
|
|
56
|
+
expect(b).toEqual([2.5, 12.5]);
|
|
57
|
+
});
|
|
58
|
+
|
|
49
59
|
it('linearly blends components and clamps alpha', () => {
|
|
50
60
|
const out = [0, 0];
|
|
51
61
|
blendAnimationSamples(out, [0, 10], [10, 20], 0.25);
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { EntityRuntimeKey } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { createAnimationChannel, createAnimationClip } from './animationClip';
|
|
4
|
+
import {
|
|
5
|
+
advanceAnimationCrossfade,
|
|
6
|
+
createAnimationCrossfade,
|
|
7
|
+
isAnimationCrossfadeComplete,
|
|
8
|
+
sampleAnimationCrossfade,
|
|
9
|
+
} from './animationCrossfade';
|
|
10
|
+
import { createAnimationPlayer } from './animationPlayer';
|
|
11
|
+
import { createAnimationTrack } from './animationTrack';
|
|
12
|
+
|
|
13
|
+
function player(targetRef: unknown, values: number[], opts?: Readonly<{ components?: number; quaternion?: boolean }>) {
|
|
14
|
+
const track = createAnimationTrack({
|
|
15
|
+
components: opts?.components,
|
|
16
|
+
quaternion: opts?.quaternion,
|
|
17
|
+
times: [0, 1],
|
|
18
|
+
values,
|
|
19
|
+
});
|
|
20
|
+
return createAnimationPlayer(createAnimationClip([createAnimationChannel(track, targetRef)]));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('advanceAnimationCrossfade', () => {
|
|
24
|
+
it('advances both players by the same delta and updates linear weight', () => {
|
|
25
|
+
const target = {};
|
|
26
|
+
const from = player(target, [0, 10]);
|
|
27
|
+
const to = player(target, [20, 30]);
|
|
28
|
+
const state = createAnimationCrossfade(from, to, 2);
|
|
29
|
+
advanceAnimationCrossfade(state, 0.5);
|
|
30
|
+
expect(from.time).toBe(0.5);
|
|
31
|
+
expect(to.time).toBe(0.5);
|
|
32
|
+
expect(state.elapsed).toBe(0.5);
|
|
33
|
+
expect(state.weight).toBe(0.25);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('applies a caller-provided curve to clamped normalized time', () => {
|
|
37
|
+
const target = {};
|
|
38
|
+
const state = createAnimationCrossfade(player(target, [0, 1]), player(target, [1, 2]), 1, {
|
|
39
|
+
curve: (t) => t * t,
|
|
40
|
+
});
|
|
41
|
+
advanceAnimationCrossfade(state, 0.5);
|
|
42
|
+
expect(state.weight).toBe(0.25);
|
|
43
|
+
advanceAnimationCrossfade(state, 2);
|
|
44
|
+
expect(state.weight).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('createAnimationCrossfade', () => {
|
|
49
|
+
it('creates an Entity with linear weight and reusable scratch by default', () => {
|
|
50
|
+
const target = {};
|
|
51
|
+
const state = createAnimationCrossfade(player(target, [0, 1]), player(target, [1, 2]), 2);
|
|
52
|
+
expect(EntityRuntimeKey in state).toBe(true);
|
|
53
|
+
expect(state.elapsed).toBe(0);
|
|
54
|
+
expect(state.weight).toBe(0);
|
|
55
|
+
expect(state.fromSample).toBeInstanceOf(Float32Array);
|
|
56
|
+
expect(state.toSample).toBeInstanceOf(Float32Array);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('completes a zero-duration transition immediately', () => {
|
|
60
|
+
const target = {};
|
|
61
|
+
const state = createAnimationCrossfade(player(target, [0, 1]), player(target, [1, 2]), 0);
|
|
62
|
+
expect(state.weight).toBe(1);
|
|
63
|
+
expect(isAnimationCrossfadeComplete(state)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('matches reordered channels by target and retains one-sided channels', () => {
|
|
67
|
+
const shared = {};
|
|
68
|
+
const fromOnly = {};
|
|
69
|
+
const toOnly = {};
|
|
70
|
+
const from = createAnimationPlayer(
|
|
71
|
+
createAnimationClip([
|
|
72
|
+
createAnimationChannel(createAnimationTrack({ times: [0], values: [1] }), shared),
|
|
73
|
+
createAnimationChannel(createAnimationTrack({ times: [0], values: [2] }), fromOnly),
|
|
74
|
+
]),
|
|
75
|
+
);
|
|
76
|
+
const to = createAnimationPlayer(
|
|
77
|
+
createAnimationClip([
|
|
78
|
+
createAnimationChannel(createAnimationTrack({ times: [0], values: [3] }), toOnly),
|
|
79
|
+
createAnimationChannel(createAnimationTrack({ times: [0], values: [4] }), shared),
|
|
80
|
+
]),
|
|
81
|
+
);
|
|
82
|
+
const state = createAnimationCrossfade(from, to, 1);
|
|
83
|
+
expect(state.channels).toEqual([
|
|
84
|
+
{ channel: to.clip.channels[1], fromIndex: 0, toIndex: 1 },
|
|
85
|
+
{ channel: from.clip.channels[1], fromIndex: 1, toIndex: null },
|
|
86
|
+
{ channel: to.clip.channels[0], fromIndex: null, toIndex: 0 },
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('rejects incompatible tracks that claim the same target', () => {
|
|
91
|
+
const target = {};
|
|
92
|
+
const scalar = player(target, [0, 1]);
|
|
93
|
+
const vector = player(target, [0, 0, 1, 1], { components: 2 });
|
|
94
|
+
expect(() => createAnimationCrossfade(scalar, vector, 1)).toThrow(/different component widths/);
|
|
95
|
+
|
|
96
|
+
const quaternion = player(target, [0, 0, 0, 1, 0, 0, 0, 1], {
|
|
97
|
+
components: 4,
|
|
98
|
+
quaternion: true,
|
|
99
|
+
});
|
|
100
|
+
const vector4 = player(target, [0, 0, 0, 1, 0, 0, 0, 1], { components: 4 });
|
|
101
|
+
expect(() => createAnimationCrossfade(quaternion, vector4, 1)).toThrow(/incompatible quaternion flags/);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('rejects duplicate target references in either clip', () => {
|
|
105
|
+
const duplicate = {};
|
|
106
|
+
const unique = {};
|
|
107
|
+
const track = createAnimationTrack({ times: [0], values: [1] });
|
|
108
|
+
const duplicateClip = createAnimationClip([
|
|
109
|
+
createAnimationChannel(track, duplicate),
|
|
110
|
+
createAnimationChannel(track, duplicate),
|
|
111
|
+
]);
|
|
112
|
+
const uniqueClip = createAnimationClip([
|
|
113
|
+
createAnimationChannel(track, duplicate),
|
|
114
|
+
createAnimationChannel(track, unique),
|
|
115
|
+
]);
|
|
116
|
+
expect(() =>
|
|
117
|
+
createAnimationCrossfade(createAnimationPlayer(duplicateClip), createAnimationPlayer(uniqueClip), 1),
|
|
118
|
+
).toThrow(/source clip contains a duplicate targetRef/);
|
|
119
|
+
expect(() =>
|
|
120
|
+
createAnimationCrossfade(createAnimationPlayer(uniqueClip), createAnimationPlayer(duplicateClip), 1),
|
|
121
|
+
).toThrow(/destination clip contains a duplicate targetRef/);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('isAnimationCrossfadeComplete', () => {
|
|
126
|
+
it('uses elapsed duration as the retirement condition', () => {
|
|
127
|
+
const target = {};
|
|
128
|
+
const state = createAnimationCrossfade(player(target, [0, 1]), player(target, [1, 2]), 2);
|
|
129
|
+
expect(isAnimationCrossfadeComplete(state)).toBe(false);
|
|
130
|
+
advanceAnimationCrossfade(state, 2);
|
|
131
|
+
expect(isAnimationCrossfadeComplete(state)).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('does not retire early when an easing curve overshoots 1', () => {
|
|
135
|
+
const target = {};
|
|
136
|
+
const state = createAnimationCrossfade(player(target, [0, 1]), player(target, [1, 2]), 1, {
|
|
137
|
+
curve: (t) => 1 + t,
|
|
138
|
+
});
|
|
139
|
+
advanceAnimationCrossfade(state, 0.7);
|
|
140
|
+
expect(state.weight).toBeGreaterThan(1);
|
|
141
|
+
expect(isAnimationCrossfadeComplete(state)).toBe(false);
|
|
142
|
+
advanceAnimationCrossfade(state, 0.31);
|
|
143
|
+
expect(isAnimationCrossfadeComplete(state)).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('sampleAnimationCrossfade', () => {
|
|
148
|
+
it('samples both playheads and blends through a distinct output buffer', () => {
|
|
149
|
+
const target = {};
|
|
150
|
+
const state = createAnimationCrossfade(player(target, [0, 10]), player(target, [20, 40]), 2);
|
|
151
|
+
advanceAnimationCrossfade(state, 0.5);
|
|
152
|
+
const out = [0];
|
|
153
|
+
const seen: number[] = [];
|
|
154
|
+
sampleAnimationCrossfade(out, state, (sampled) => seen.push(sampled[0]));
|
|
155
|
+
expect(seen).toEqual([11.25]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('supports output aliasing either internal blend input', () => {
|
|
159
|
+
const target = {};
|
|
160
|
+
const state = createAnimationCrossfade(player(target, [0, 10]), player(target, [20, 40]), 2);
|
|
161
|
+
advanceAnimationCrossfade(state, 0.5);
|
|
162
|
+
const seen: number[] = [];
|
|
163
|
+
sampleAnimationCrossfade(state.fromSample, state, (sampled) => seen.push(sampled[0]));
|
|
164
|
+
expect(seen).toEqual([11.25]);
|
|
165
|
+
sampleAnimationCrossfade(state.toSample, state, (sampled) => seen.push(sampled[0]));
|
|
166
|
+
expect(seen).toEqual([11.25, 11.25]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('visits matched and one-sided channels in stable target order', () => {
|
|
170
|
+
const shared = {};
|
|
171
|
+
const fromOnly = {};
|
|
172
|
+
const toOnly = {};
|
|
173
|
+
const from = createAnimationPlayer(
|
|
174
|
+
createAnimationClip([
|
|
175
|
+
createAnimationChannel(createAnimationTrack({ times: [0], values: [2] }), shared),
|
|
176
|
+
createAnimationChannel(createAnimationTrack({ times: [0], values: [7] }), fromOnly),
|
|
177
|
+
]),
|
|
178
|
+
);
|
|
179
|
+
const to = createAnimationPlayer(
|
|
180
|
+
createAnimationClip([
|
|
181
|
+
createAnimationChannel(createAnimationTrack({ times: [0], values: [9] }), toOnly),
|
|
182
|
+
createAnimationChannel(createAnimationTrack({ times: [0], values: [6] }), shared),
|
|
183
|
+
]),
|
|
184
|
+
);
|
|
185
|
+
const state = createAnimationCrossfade(from, to, 2);
|
|
186
|
+
advanceAnimationCrossfade(state, 1);
|
|
187
|
+
const seen: Array<{ index: number; target: unknown; value: number }> = [];
|
|
188
|
+
sampleAnimationCrossfade([0], state, (sampled, channel, index) => {
|
|
189
|
+
seen.push({ index, target: channel.targetRef, value: sampled[0] });
|
|
190
|
+
});
|
|
191
|
+
expect(seen).toEqual([
|
|
192
|
+
{ index: 0, target: shared, value: 4 },
|
|
193
|
+
{ index: 1, target: fromOnly, value: 7 },
|
|
194
|
+
{ index: 2, target: toOnly, value: 9 },
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('preserves alpha endpoints and reuses scratch buffers', () => {
|
|
199
|
+
const target = {};
|
|
200
|
+
const state = createAnimationCrossfade(player(target, [0, 10]), player(target, [20, 40]), 1);
|
|
201
|
+
const fromSample = state.fromSample;
|
|
202
|
+
const toSample = state.toSample;
|
|
203
|
+
const values: number[] = [];
|
|
204
|
+
sampleAnimationCrossfade([0], state, (sampled) => values.push(sampled[0]));
|
|
205
|
+
advanceAnimationCrossfade(state, 2);
|
|
206
|
+
sampleAnimationCrossfade([0], state, (sampled) => values.push(sampled[0]));
|
|
207
|
+
expect(values).toEqual([0, 20]);
|
|
208
|
+
expect(state.fromSample).toBe(fromSample);
|
|
209
|
+
expect(state.toSample).toBe(toSample);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('uses shortest-arc quaternion slerp', () => {
|
|
213
|
+
const target = {};
|
|
214
|
+
const from = player(target, [0, 0, 0, 1, 0, 0, 0, 1], { components: 4, quaternion: true });
|
|
215
|
+
const to = player(target, [0, 0, -1, 0, 0, 0, -1, 0], { components: 4, quaternion: true });
|
|
216
|
+
const state = createAnimationCrossfade(from, to, 1);
|
|
217
|
+
advanceAnimationCrossfade(state, 0.5);
|
|
218
|
+
sampleAnimationCrossfade([0, 0, 0, 0], state, (sampled) => {
|
|
219
|
+
expect(Math.abs(sampled[2])).toBeCloseTo(Math.SQRT1_2);
|
|
220
|
+
expect(Math.abs(sampled[3])).toBeCloseTo(Math.SQRT1_2);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|