Animations blend out on Stop and at the end of Once (reset), and Play can restart a running animation (#106)
Stop animation gets a blend (s) port that fades the animation back to what lies underneath. Play animation gets blend out (s) for Once (reset) and a restart port that starts a running, paused or fading animation from frame 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8a377d9239
commit
195fc94e08
4 files changed
+369
-29
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { pauseAnimation, playAnimation, stopAnimation } from '../../../model/animation';
|
||||
import type { NodeDef, PortDef } from '../../NodeRegistry';
|
||||
import { asEntity, asNumber, asString } from '../../values';
|
||||
import { exec, num, obj, withDefault } from '../shared';
|
||||
import { asBoolean, asEntity, asNumber, asString } from '../../values';
|
||||
import { bool, exec, num, obj, withDefault } from '../shared';
|
||||
|
||||
/**
|
||||
* The `animation` input of the three animation nodes; unconnected it shows the animation picker, the
|
||||
@@ -22,14 +22,31 @@ export const PlayAnimation: NodeDef = {
|
||||
'Plays an animation on an object: its rows reach the object and what is placed inside it, so every ' +
|
||||
'instance plays on its own. A paused one runs on from where it stands; one already running keeps its ' +
|
||||
'frame but wins again over the others on the object. Blend fades numbers and colours over that many ' +
|
||||
'seconds from what they held when it took over; everything else switches at once.',
|
||||
inputs: [exec(), obj('object'), animationPort(), withDefault(num('blend', 'blend (s)'), 0)],
|
||||
'seconds from what they held when it took over; everything else switches at once. Blend out is for ' +
|
||||
'Once (reset): when its frames run out it fades back to where it started over that many seconds. ' +
|
||||
'Restart starts one already running or paused over from its first frame – blended in from where it ' +
|
||||
'stands when Blend is set.',
|
||||
inputs: [
|
||||
exec(),
|
||||
obj('object'),
|
||||
animationPort(),
|
||||
withDefault(num('blend', 'blend (s)'), 0),
|
||||
withDefault(num('blendOut', 'blend out (s)'), 0),
|
||||
withDefault(bool('restart'), false),
|
||||
],
|
||||
outputs: [exec()],
|
||||
params: [],
|
||||
run: (_ctx, inputs) => {
|
||||
const target = asEntity(inputs.object);
|
||||
const animation = asString(inputs.animation);
|
||||
if (target && animation) playAnimation(target, animation, Math.max(0, asNumber(inputs.blend)));
|
||||
if (target && animation)
|
||||
playAnimation(
|
||||
target,
|
||||
animation,
|
||||
Math.max(0, asNumber(inputs.blend)),
|
||||
Math.max(0, asNumber(inputs.blendOut)),
|
||||
asBoolean(inputs.restart),
|
||||
);
|
||||
return { next: 'exec' };
|
||||
},
|
||||
};
|
||||
@@ -39,13 +56,15 @@ export const StopAnimation: NodeDef = {
|
||||
label: 'Stop animation',
|
||||
category: 'action',
|
||||
description:
|
||||
'Ends an animation on an object where it stands – the values it set stay. Leave Animation at None to end every one.',
|
||||
inputs: [exec(), obj('object'), animationPort()],
|
||||
'Ends an animation on an object where it stands – the values it set stay. With a blend it fades out ' +
|
||||
'over that many seconds instead, back to what another animation sets there or else to what the ' +
|
||||
'values were when it started. Leave Animation at None to end every one.',
|
||||
inputs: [exec(), obj('object'), animationPort(), withDefault(num('blend', 'blend (s)'), 0)],
|
||||
outputs: [exec()],
|
||||
params: [],
|
||||
run: (_ctx, inputs) => {
|
||||
const target = asEntity(inputs.object);
|
||||
if (target) stopAnimation(target, asString(inputs.animation));
|
||||
if (target) stopAnimation(target, asString(inputs.animation), Math.max(0, asNumber(inputs.blend)));
|
||||
return { next: 'exec' };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -389,6 +389,9 @@ export interface AnimationPlayer {
|
||||
order: number;
|
||||
bases?: Record<string, PropertyValue>;
|
||||
blend?: AnimationBlend;
|
||||
/** Seconds a `once-reset` pass takes to blend back to its starting values when its frames run out. */
|
||||
blendOut?: number;
|
||||
fade?: AnimationFade;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -403,8 +406,20 @@ export interface AnimationBlend {
|
||||
from?: Record<string, PropertyValue>;
|
||||
}
|
||||
|
||||
/** How far a blend has come, 0…1. */
|
||||
export function blendShare(blend: AnimationBlend): number {
|
||||
/**
|
||||
* A player on its way out (#106): over `seconds` a row that blends mixes from the player's value back
|
||||
* to what lies below it – what an earlier player writes in the same tick, live, or else the value the
|
||||
* row found at the start. Everything else goes back at once. Then the player is gone. `ended` marks
|
||||
* the fade of a `once-reset` pass whose frames ran out: playing it again starts it over.
|
||||
*/
|
||||
export interface AnimationFade {
|
||||
seconds: number;
|
||||
elapsed: number;
|
||||
ended?: boolean;
|
||||
}
|
||||
|
||||
/** How far a blend or a fade has come, 0…1. */
|
||||
export function blendShare(blend: { seconds: number; elapsed: number }): number {
|
||||
if (!(blend.seconds > 0)) return 1;
|
||||
return Math.min(1, blend.elapsed / blend.seconds);
|
||||
}
|
||||
@@ -421,25 +436,63 @@ let startCount = 0;
|
||||
* as started last again: turning back to a walk that still runs has to show that walk.
|
||||
* `blendSeconds` crossfades into it – but only when it takes over: asked for again while it already
|
||||
* leads on the object, it keeps the blend it has, so a graph calling every frame does not restart it.
|
||||
* One fading out turns round: it blends back in from the value it has reached, without a jump – or,
|
||||
* when a `once-reset` pass already ran out, starts over. `blendOutSeconds` is the fade at the end of
|
||||
* a `once-reset` pass. `restart` starts a known one over from its first frame in any case – blended in
|
||||
* from where it stands when `blendSeconds` is set – and keeps its starting values, so a relative row
|
||||
* does not add up.
|
||||
*/
|
||||
export function playAnimation(holder: PlayerHolder, animationId: string, blendSeconds = 0): void {
|
||||
export function playAnimation(
|
||||
holder: PlayerHolder,
|
||||
animationId: string,
|
||||
blendSeconds = 0,
|
||||
blendOutSeconds = 0,
|
||||
restart = false,
|
||||
): void {
|
||||
const players = (holder.animations ??= []);
|
||||
const known = players.find((p) => p.animationId === animationId);
|
||||
const blend = blendSeconds > 0 ? { seconds: blendSeconds, elapsed: 0 } : undefined;
|
||||
const blendOut = blendOutSeconds > 0 ? blendOutSeconds : undefined;
|
||||
if (known) {
|
||||
const leads = players.every((p) => p.order <= known.order);
|
||||
const fade = known.fade;
|
||||
known.playing = true;
|
||||
known.order = ++startCount;
|
||||
if (!leads) known.blend = blend;
|
||||
known.blendOut = blendOut;
|
||||
delete known.fade;
|
||||
if (restart || fade?.ended) {
|
||||
known.time = 0;
|
||||
known.blend = blend;
|
||||
} else if (fade && blend) {
|
||||
// Mixed back from the starting values it stands where the fade left it: `share` = 1 - fade share.
|
||||
blend.elapsed = blendSeconds * (1 - blendShare(fade));
|
||||
known.blend = { ...blend, ...(known.bases && { from: { ...known.bases } }) };
|
||||
} else if (fade || !leads) known.blend = blend;
|
||||
return;
|
||||
}
|
||||
players.push({ animationId, time: 0, playing: true, order: ++startCount, ...(blend && { blend }) });
|
||||
players.push({
|
||||
animationId,
|
||||
time: 0,
|
||||
playing: true,
|
||||
order: ++startCount,
|
||||
...(blend && { blend }),
|
||||
...(blendOut && { blendOut }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Ends it where it stands – the values stay; `''` ends every animation on the object. */
|
||||
export function stopAnimation(holder: PlayerHolder, animationId: string): void {
|
||||
/**
|
||||
* Ends it where it stands – the values stay; `''` ends every animation on the object. With
|
||||
* `blendSeconds` it fades out instead, back to what lies below it (see `AnimationFade`). A player that
|
||||
* has not reached anything yet ends at once either way.
|
||||
*/
|
||||
export function stopAnimation(holder: PlayerHolder, animationId: string, blendSeconds = 0): void {
|
||||
if (!holder.animations) return;
|
||||
holder.animations = holder.animations.filter((p) => animationId !== '' && p.animationId !== animationId);
|
||||
const hit = (p: AnimationPlayer) => animationId === '' || p.animationId === animationId;
|
||||
const fades = blendSeconds > 0;
|
||||
holder.animations = holder.animations.filter((p) => !hit(p) || (fades && p.bases !== undefined));
|
||||
if (fades)
|
||||
for (const player of holder.animations)
|
||||
if (hit(player) && !player.fade) player.fade = { seconds: blendSeconds, elapsed: 0 };
|
||||
}
|
||||
|
||||
/** Holds it at its frame until `Play animation` lets it run on; `''` holds every one. */
|
||||
|
||||
@@ -37,7 +37,7 @@ interface Binding {
|
||||
|
||||
/**
|
||||
* Plays what `Play animation` started. Players run in start order, so of two setting one property
|
||||
* the later start wins – or, while it blends in, mixes over what was there. Runs before the
|
||||
* the later start wins – or, while it blends in or fades out, mixes over what was there. Runs before the
|
||||
* BehaviorSystem: a behavior writing the same property writes last, and hears this frame's `finished`.
|
||||
*/
|
||||
export class AnimationSystem implements System {
|
||||
@@ -67,31 +67,53 @@ export class AnimationSystem implements System {
|
||||
// The first tick only takes the starting values and shows frame 0; the clock runs from the next.
|
||||
player.bases = {};
|
||||
for (const b of bindings) player.bases[b.base] = b.read();
|
||||
} else if (player.playing && isHandleEnabled(entity)) {
|
||||
player.time += dt;
|
||||
if (blend) blend.elapsed += dt;
|
||||
} else if (isHandleEnabled(entity)) {
|
||||
if (player.playing) {
|
||||
player.time += dt;
|
||||
if (blend) blend.elapsed += dt;
|
||||
}
|
||||
// A paused player that was stopped with a blend still fades out.
|
||||
if (player.fade) player.fade.elapsed += dt;
|
||||
}
|
||||
const head = animationPlayheadAt(asset, player.time);
|
||||
const share = blend ? blendShare(blend) : 1;
|
||||
const fade = player.fade;
|
||||
const fadeShare = fade ? blendShare(fade) : 0;
|
||||
for (const b of bindings) {
|
||||
let value = trackWriteAt(b.track, b.kind, head.frame, player.bases[b.base], head.reverse);
|
||||
if (value === undefined) continue;
|
||||
const below = written.has(b.base) ? b.read() : undefined;
|
||||
if (share < 1 && blends(b.kind)) {
|
||||
const from = written.has(b.base) ? b.read() : blend?.from?.[b.base];
|
||||
const from = below ?? blend?.from?.[b.base];
|
||||
if (from !== undefined) value = blendValue(b.kind, from, value, share);
|
||||
}
|
||||
if (fade) {
|
||||
const back = below ?? player.bases[b.base];
|
||||
if (back !== undefined) value = blends(b.kind) ? blendValue(b.kind, value, back, fadeShare) : back;
|
||||
}
|
||||
b.write(value);
|
||||
written.add(b.base);
|
||||
}
|
||||
if (blend && share >= 1) delete player.blend;
|
||||
if (fade) {
|
||||
if (fadeShare >= 1) drop(entity, player);
|
||||
continue;
|
||||
}
|
||||
if (!head.finished) continue;
|
||||
if (playbackOf(asset) === 'once-reset')
|
||||
for (const b of bindings) {
|
||||
const base = player.bases[b.base];
|
||||
if (base !== undefined) b.write(base);
|
||||
}
|
||||
drop(entity, player);
|
||||
this.finished.push({ entity, animationId: asset.id });
|
||||
if (playbackOf(asset) !== 'once-reset') {
|
||||
drop(entity, player);
|
||||
continue;
|
||||
}
|
||||
if (player.blendOut) {
|
||||
player.fade = { seconds: player.blendOut, elapsed: 0, ended: true };
|
||||
continue;
|
||||
}
|
||||
for (const b of bindings) {
|
||||
const base = player.bases[b.base];
|
||||
if (base !== undefined) b.write(base);
|
||||
}
|
||||
drop(entity, player);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -703,6 +703,189 @@ describe('blending into an animation that takes over (#106)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fading an animation out (#106)', () => {
|
||||
it('blends a once-reset pass back to where it started, while another animation runs on', () => {
|
||||
const { f, walk, box } = slide({ playback: 'once-reset' });
|
||||
const bob = addAnimation(f, f.spriteId, { fps: 10, playback: 'pingpong' }, 'Bob');
|
||||
addRow(f, bob, 'y', [
|
||||
[0, 0],
|
||||
[4, 40],
|
||||
]);
|
||||
const world = World.instantiate(f.project, f.sceneId);
|
||||
const system = new AnimationSystem();
|
||||
world.addSystem(system);
|
||||
const sprite = world.findByObjectId(box)!;
|
||||
sprite.x = 10;
|
||||
playAnimation(sprite, bob.id);
|
||||
playAnimation(sprite, walk.id, 0, 0.4);
|
||||
const reports: number[] = [];
|
||||
const tick = () => {
|
||||
world.tick(0.1);
|
||||
reports.push(system.finished.length);
|
||||
};
|
||||
for (let i = 0; i < 11; i++) tick();
|
||||
expect(sprite.x).toBe(90);
|
||||
const xs: number[] = [];
|
||||
const ys: number[] = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
tick();
|
||||
xs.push(sprite.x);
|
||||
ys.push(sprite.y);
|
||||
}
|
||||
// From the last key's 90 back to the starting 10 by 1/4 steps; the bob turns on at 40 meanwhile.
|
||||
[70, 50, 30, 10].forEach((x, i) => expect(xs[i]).toBeCloseTo(x));
|
||||
[30, 40, 30, 20].forEach((y, i) => expect(ys[i]).toBeCloseTo(y));
|
||||
expect(reports.filter((n) => n > 0)).toEqual([1]);
|
||||
expect(sprite.animations?.map((p) => p.animationId)).toEqual([bob.id]);
|
||||
});
|
||||
|
||||
it('fades a stopped animation back to what an earlier animation writes there, live', () => {
|
||||
const { f, walk, sprite, frame } = slide();
|
||||
const duck = addAnimation(f, f.spriteId, { fps: 10 }, 'Duck');
|
||||
addRow(f, duck, 'x', [[0, -100]]);
|
||||
playAnimation(sprite, walk.id);
|
||||
frame(0.1);
|
||||
playAnimation(sprite, duck.id);
|
||||
frame(0.1);
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBe(-100);
|
||||
stopAnimation(sprite, duck.id, 0.4);
|
||||
const seen: number[] = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
frame(0.1);
|
||||
seen.push(sprite.x);
|
||||
}
|
||||
// The walk runs on at 30, 40, 50, 60, 70; the duck gives way by 1/4, 1/2, 3/4, all – then is gone.
|
||||
[-67.5, -30, 12.5, 60, 70].forEach((x, i) => expect(seen[i]).toBeCloseTo(x));
|
||||
expect(sprite.animations?.map((p) => p.animationId)).toEqual([walk.id]);
|
||||
});
|
||||
|
||||
it('fades back to the starting value with nothing below, switches what cannot blend at once, also paused', () => {
|
||||
const { f, walk, sprite, frame } = slide();
|
||||
addRow(f, walk, 'mirrorX', [[0, true]]);
|
||||
sprite.x = 100;
|
||||
playAnimation(sprite, walk.id);
|
||||
frame(0.1);
|
||||
frame(0.2);
|
||||
expect(sprite.props.mirrorX).toBe(true);
|
||||
pauseAnimation(sprite, walk.id);
|
||||
stopAnimation(sprite, walk.id, 0.2);
|
||||
frame(0.1);
|
||||
// Held at 20 by the pause, half way back to 100.
|
||||
expect(sprite.x).toBeCloseTo(60);
|
||||
expect(sprite.props.mirrorX).toBe(false);
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBeCloseTo(100);
|
||||
expect(sprite.animations).toEqual([]);
|
||||
});
|
||||
|
||||
it('ends one that has not reached anything yet at once, blend or not', () => {
|
||||
const { walk, sprite } = slide();
|
||||
playAnimation(sprite, walk.id);
|
||||
stopAnimation(sprite, walk.id, 0.5);
|
||||
expect(sprite.animations).toEqual([]);
|
||||
});
|
||||
|
||||
it('turns a fade round without a jump when played again', () => {
|
||||
const { walk, sprite, frame } = slide();
|
||||
sprite.x = 100;
|
||||
playAnimation(sprite, walk.id);
|
||||
frame(0.1);
|
||||
frame(0.2);
|
||||
stopAnimation(sprite, walk.id, 0.8);
|
||||
frame(0.1);
|
||||
// The walk at 30, 1/8 of the way back to 100.
|
||||
expect(sprite.x).toBeCloseTo(38.75);
|
||||
playAnimation(sprite, walk.id, 1.6);
|
||||
expect(sprite.animations?.[0]?.fade).toBeUndefined();
|
||||
const seen: number[] = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
frame(0.1);
|
||||
seen.push(sprite.x);
|
||||
}
|
||||
// Blending in from 100 it stands at 7/8 – where the fade left it – and runs on from there: walk 40
|
||||
// at 15/16, walk 50 all in.
|
||||
[43.75, 50].forEach((x, i) => expect(seen[i]).toBeCloseTo(x));
|
||||
});
|
||||
|
||||
it('starts a once-reset pass over when played again while it fades back, from where it stands', () => {
|
||||
const { walk, sprite, frame } = slide({ playback: 'once-reset' });
|
||||
sprite.x = 10;
|
||||
playAnimation(sprite, walk.id, 0, 0.4);
|
||||
for (let i = 0; i < 13; i++) frame(0.1);
|
||||
expect(sprite.x).toBeCloseTo(50);
|
||||
playAnimation(sprite, walk.id, 0.2, 0.4);
|
||||
const player = sprite.animations?.[0];
|
||||
expect(player).toMatchObject({ time: 0, blendOut: 0.4 });
|
||||
expect(player?.fade).toBeUndefined();
|
||||
frame(0.1);
|
||||
// Frame 1 of the walk (10), blended in half way from the 50 the fade had reached.
|
||||
expect(sprite.x).toBeCloseTo(30);
|
||||
// Relative to the same start: it still goes back to 10 at the end.
|
||||
expect(player?.bases).toEqual({ [`${sprite.id} x`]: 10 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('restarting an animation that already runs', () => {
|
||||
/** `walk` played for 0.4 s: frame 4, x 40. */
|
||||
function walked() {
|
||||
const s = slide();
|
||||
s.sprite.x = 5;
|
||||
playAnimation(s.sprite, s.walk.id);
|
||||
s.frame(0.1);
|
||||
s.frame(0.2);
|
||||
s.frame(0.2);
|
||||
expect(s.sprite.x).toBeCloseTo(40);
|
||||
return s;
|
||||
}
|
||||
|
||||
it('starts a running one over from its first frame, keeping its starting values', () => {
|
||||
const { walk, sprite, frame } = walked();
|
||||
const bases = { ...sprite.animations?.[0]?.bases };
|
||||
playAnimation(sprite, walk.id);
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBeCloseTo(50);
|
||||
playAnimation(sprite, walk.id, 0, 0, true);
|
||||
expect(sprite.animations?.[0]?.time).toBe(0);
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBeCloseTo(10);
|
||||
expect(sprite.animations?.[0]?.bases).toEqual(bases);
|
||||
});
|
||||
|
||||
it('starts a paused one over and lets it run', () => {
|
||||
const { walk, sprite, frame } = walked();
|
||||
pauseAnimation(sprite, walk.id);
|
||||
frame(0.1);
|
||||
playAnimation(sprite, walk.id, 0, 0, true);
|
||||
frame(0.1);
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBeCloseTo(20);
|
||||
});
|
||||
|
||||
it('blends the new start in from where it stands', () => {
|
||||
const { walk, sprite, frame } = walked();
|
||||
playAnimation(sprite, walk.id, 0.2, 0, true);
|
||||
const seen: number[] = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
frame(0.1);
|
||||
seen.push(sprite.x);
|
||||
}
|
||||
// Walk 10 half blended in from 40, then walk 20 all in.
|
||||
[25, 20].forEach((x, i) => expect(seen[i]).toBeCloseTo(x));
|
||||
});
|
||||
|
||||
it('starts a fading one over instead of turning it round', () => {
|
||||
const { walk, sprite, frame } = walked();
|
||||
stopAnimation(sprite, walk.id, 0.8);
|
||||
frame(0.1);
|
||||
playAnimation(sprite, walk.id, 0, 0, true);
|
||||
expect(sprite.animations?.[0]).toMatchObject({ time: 0, playing: true });
|
||||
expect(sprite.animations?.[0]?.fade).toBeUndefined();
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBeCloseTo(10);
|
||||
});
|
||||
});
|
||||
|
||||
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() {
|
||||
@@ -942,13 +1125,76 @@ describe('the animation nodes', () => {
|
||||
const edit = graphEditor(f, behavior);
|
||||
const start = edit.addNode('OnStart');
|
||||
const owner = edit.addNode('Owner');
|
||||
const play = edit.addNode('PlayAnimation', { animation: walk.id, blend: 0.6 });
|
||||
const play = edit.addNode('PlayAnimation', { animation: walk.id, blend: 0.6, blendOut: 0.3 });
|
||||
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?.seconds).toBe(0.6);
|
||||
expect(world.findByObjectId(box)!.animations?.[0]).toMatchObject({
|
||||
blend: { seconds: 0.6 },
|
||||
blendOut: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
it('restarts the animation every time when Restart is on, and lets it run on when off', () => {
|
||||
const run = (restart: boolean) => {
|
||||
const f = makeSpriteScene();
|
||||
const box = place(f, f.spriteId, { x: 0, y: 0 });
|
||||
const walk = addAnimation(f, f.spriteId, { fps: 10 });
|
||||
addRow(f, walk, 'x', [
|
||||
[0, 0],
|
||||
[9, 90],
|
||||
]);
|
||||
const behavior = makeBehavior(f, 'Logic', []);
|
||||
place(f, behavior.id, { x: 0, y: 0 }, box);
|
||||
const edit = graphEditor(f, behavior);
|
||||
const update = edit.addNode('OnUpdate');
|
||||
const owner = edit.addNode('Owner');
|
||||
const play = edit.addNode('PlayAnimation', { animation: walk.id, restart });
|
||||
edit.wire([
|
||||
[update, 'exec', play, 'exec'],
|
||||
[owner, 'object', play, 'object'],
|
||||
]);
|
||||
const { world, frame } = startWorld(f, { animations: true, behaviors: true });
|
||||
for (let i = 0; i < 5; i++) frame(0.1);
|
||||
return world.findByObjectId(box)!.animations?.[0]?.time;
|
||||
};
|
||||
expect(run(false)).toBeGreaterThan(0.3);
|
||||
expect(run(true)).toBeLessThan(0.15);
|
||||
});
|
||||
|
||||
it('passes Blend on to Stop animation, which fades the player out (#106)', () => {
|
||||
const f = makeSpriteScene();
|
||||
const box = place(f, f.spriteId, { x: 0, y: 0 });
|
||||
const walk = addAnimation(f, f.spriteId, { fps: 10 });
|
||||
addRow(f, walk, 'x', [
|
||||
[0, 0],
|
||||
[9, 90],
|
||||
]);
|
||||
const cue = addAnimation(f, f.spriteId, { fps: 10, playback: 'once' }, 'Cue');
|
||||
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 playWalk = edit.addNode('PlayAnimation', { animation: walk.id });
|
||||
const playCue = edit.addNode('PlayAnimation', { animation: cue.id });
|
||||
const finished = edit.addNode('OnAnimationFinished', { animation: cue.id });
|
||||
const stop = edit.addNode('StopAnimation', { animation: walk.id, blend: 0.5 });
|
||||
edit.wire([
|
||||
[start, 'exec', playWalk, 'exec'],
|
||||
[playWalk, 'exec', playCue, 'exec'],
|
||||
[owner, 'object', playWalk, 'object'],
|
||||
[owner, 'object', playCue, 'object'],
|
||||
[finished, 'exec', stop, 'exec'],
|
||||
[owner, 'object', stop, 'object'],
|
||||
]);
|
||||
const { world, frame } = startWorld(f, { animations: true, behaviors: true });
|
||||
for (let i = 0; i < 3; i++) frame(0.1);
|
||||
expect(world.findByObjectId(box)!.animations).toMatchObject([
|
||||
{ animationId: walk.id, fade: { seconds: 0.5 } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user