Play animation can blend into the new animation over a number of frames (#106)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
beeea355d9
commit
04dc254b3d
4 files changed
+177
-14
No files matched your search
@@ -592,6 +592,104 @@ describe('the AnimationSystem', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('blending into an animation that takes over (#106)', () => {
|
||||
/** `slide` plus a `Stand` holding `x` at `at`, at 10 fps. */
|
||||
function standing(at: number, name = 'Stand') {
|
||||
const s = slide();
|
||||
const stand = addAnimation(s.f, s.f.spriteId, { fps: 10 }, name);
|
||||
addRow(s.f, stand, 'x', [[0, at]]);
|
||||
return { ...s, stand };
|
||||
}
|
||||
|
||||
it('mixes from the animation it takes over, linearly over its blend frames', () => {
|
||||
const { walk, stand, sprite, frame } = standing(100);
|
||||
playAnimation(sprite, stand.id);
|
||||
frame(0.1);
|
||||
playAnimation(sprite, walk.id, 4);
|
||||
const seen: number[] = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
frame(0.1);
|
||||
seen.push(sprite.x);
|
||||
}
|
||||
// The walk at frames 0…4 is 0, 10, 20, 30, 40; mixed in by 0, 1/4, 1/2, 3/4, all.
|
||||
[100, 77.5, 60, 47.5, 40].forEach((x, i) => expect(seen[i]).toBeCloseTo(x));
|
||||
expect(sprite.animations?.find((p) => p.animationId === walk.id)?.blend).toBeUndefined();
|
||||
});
|
||||
|
||||
it('mixes from what the property held when no animation before it writes there – without easing off', () => {
|
||||
const { walk, sprite, frame } = slide();
|
||||
sprite.x = 100;
|
||||
playAnimation(sprite, walk.id, 4);
|
||||
frame(0.1);
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBeCloseTo(77.5);
|
||||
// Mixed from last tick's 77.5 it would read 48.75: the blend runs from the value at its start.
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBeCloseTo(60);
|
||||
});
|
||||
|
||||
it('switches a value that cannot blend at once', () => {
|
||||
const { f, walk, stand, sprite, frame } = standing(100);
|
||||
addRow(f, stand, 'mirrorX', [[0, false]]);
|
||||
addRow(f, walk, 'mirrorX', [[0, true]]);
|
||||
playAnimation(sprite, stand.id);
|
||||
frame(0.1);
|
||||
playAnimation(sprite, walk.id, 4);
|
||||
frame(0.1);
|
||||
frame(0.1);
|
||||
expect(sprite.props.mirrorX).toBe(true);
|
||||
expect(sprite.x).toBeCloseTo(77.5);
|
||||
});
|
||||
|
||||
it('keeps its blend when a graph asks for it every frame', () => {
|
||||
const { walk, sprite, frame } = slide();
|
||||
sprite.x = 100;
|
||||
const seen: number[] = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
playAnimation(sprite, walk.id, 4);
|
||||
frame(0.1);
|
||||
seen.push(sprite.x);
|
||||
}
|
||||
[100, 77.5, 60].forEach((x, i) => expect(seen[i]).toBeCloseTo(x));
|
||||
});
|
||||
|
||||
it('blends again when a running animation wins back, and not at all without blend frames', () => {
|
||||
const { walk, stand, sprite, frame } = standing(100);
|
||||
playAnimation(sprite, walk.id);
|
||||
frame(0.1);
|
||||
frame(0.1);
|
||||
playAnimation(sprite, stand.id);
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBe(100);
|
||||
playAnimation(sprite, walk.id, 2);
|
||||
frame(0.1);
|
||||
// The walk runs on at frame 3 (30), half blended in from the stand's 100.
|
||||
expect(sprite.x).toBeCloseTo(65);
|
||||
playAnimation(sprite, stand.id, 0);
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBe(100);
|
||||
});
|
||||
|
||||
it('lets a third start during a blend mix from the value already mixed, without a jump', () => {
|
||||
const { f, walk, stand, sprite, frame } = standing(100);
|
||||
const duck = addAnimation(f, f.spriteId, { fps: 10 }, 'Duck');
|
||||
addRow(f, duck, 'x', [[0, -100]]);
|
||||
playAnimation(sprite, stand.id);
|
||||
frame(0.1);
|
||||
playAnimation(sprite, walk.id, 4);
|
||||
frame(0.1);
|
||||
frame(0.1);
|
||||
playAnimation(sprite, duck.id, 2);
|
||||
const seen: number[] = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
frame(0.1);
|
||||
seen.push(sprite.x);
|
||||
}
|
||||
// Walk over stand gives 60, 47.5, 40; the duck takes each in by 0, 1/2, all.
|
||||
[60, -26.25, -100].forEach((x, i) => expect(seen[i]).toBeCloseTo(x));
|
||||
});
|
||||
});
|
||||
|
||||
describe('the reach of a played row (#109)', () => {
|
||||
/** A sprite packed into `Inner`, `Inner` packed into `Outer`, and `Outer` placed in the scene. */
|
||||
function nested() {
|
||||
@@ -821,4 +919,23 @@ describe('the animation nodes', () => {
|
||||
it('is not heard by an On animation finished naming another animation', () => {
|
||||
expect(listening('other').done).toBe(0);
|
||||
});
|
||||
|
||||
it('passes Blend on to the player it starts (#106)', () => {
|
||||
const f = makeSpriteScene();
|
||||
const box = place(f, f.spriteId, { x: 0, y: 0 });
|
||||
const walk = addAnimation(f, f.spriteId, { fps: 10 });
|
||||
const behavior = makeBehavior(f, 'Logic', []);
|
||||
place(f, behavior.id, { x: 0, y: 0 }, box);
|
||||
const edit = graphEditor(f, behavior);
|
||||
const start = edit.addNode('OnStart');
|
||||
const owner = edit.addNode('Owner');
|
||||
const play = edit.addNode('PlayAnimation', { animation: walk.id, blend: 6 });
|
||||
edit.wire([
|
||||
[start, 'exec', play, 'exec'],
|
||||
[owner, 'object', play, 'object'],
|
||||
]);
|
||||
const { world, frame } = startWorld(f, { behaviors: true });
|
||||
frame(0.1);
|
||||
expect(world.findByObjectId(box)!.animations?.[0]?.blend?.frames).toBe(6);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user