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
@@ -1,7 +1,7 @@
|
||||
import { pauseAnimation, playAnimation, stopAnimation } from '../../../model/animation';
|
||||
import type { NodeDef, PortDef } from '../../NodeRegistry';
|
||||
import { asEntity, asString } from '../../values';
|
||||
import { exec, obj } from '../shared';
|
||||
import { asEntity, asNumber, asString } from '../../values';
|
||||
import { exec, num, obj, withDefault } from '../shared';
|
||||
|
||||
/**
|
||||
* The `animation` input of the three animation nodes; unconnected it shows the animation picker, the
|
||||
@@ -21,14 +21,15 @@ export const PlayAnimation: NodeDef = {
|
||||
description:
|
||||
'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.',
|
||||
inputs: [exec(), obj('object'), animationPort()],
|
||||
'frame but wins again over the others on the object. Blend fades numbers and colours over that many ' +
|
||||
'of its frames from what they held when it took over; everything else switches at once.',
|
||||
inputs: [exec(), obj('object'), animationPort(), withDefault(num('blend', 'blend (frames)'), 0)],
|
||||
outputs: [exec()],
|
||||
params: [],
|
||||
run: (_ctx, inputs) => {
|
||||
const target = asEntity(inputs.object);
|
||||
const animation = asString(inputs.animation);
|
||||
if (target && animation) playAnimation(target, animation);
|
||||
if (target && animation) playAnimation(target, animation, Math.max(0, asNumber(inputs.blend)));
|
||||
return { next: 'exec' };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -388,6 +388,24 @@ export interface AnimationPlayer {
|
||||
/** Start order across the whole run: of two players setting one property, the later start wins. */
|
||||
order: number;
|
||||
bases?: Record<string, PropertyValue>;
|
||||
blend?: AnimationBlend;
|
||||
}
|
||||
|
||||
/**
|
||||
* A crossfade into a player that just took over (#106): over `frames` of its own, a row that blends
|
||||
* mixes from what the property held into the player's value. `from` is taken on the first tick of the
|
||||
* blend; a property an earlier player writes in the same tick is mixed from that value instead, live.
|
||||
*/
|
||||
export interface AnimationBlend {
|
||||
frames: number;
|
||||
elapsed: number;
|
||||
from?: Record<string, PropertyValue>;
|
||||
}
|
||||
|
||||
/** How far a blend has come, 0…1; an animation without a frame rate has nothing to blend over. */
|
||||
export function blendShare(blend: AnimationBlend, fps: number): number {
|
||||
if (!(blend.frames > 0) || !(fps > 0)) return 1;
|
||||
return Math.min(1, (blend.elapsed * fps) / blend.frames);
|
||||
}
|
||||
|
||||
interface PlayerHolder {
|
||||
@@ -400,16 +418,21 @@ let startCount = 0;
|
||||
* Starts the animation on the object, or lets a paused one run on. One already running keeps its
|
||||
* frame, so a graph may ask for it every frame without holding it at its first frame – but it counts
|
||||
* as started last again: turning back to a walk that still runs has to show that walk.
|
||||
* `blendFrames` 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.
|
||||
*/
|
||||
export function playAnimation(holder: PlayerHolder, animationId: string): void {
|
||||
export function playAnimation(holder: PlayerHolder, animationId: string, blendFrames = 0): void {
|
||||
const players = (holder.animations ??= []);
|
||||
const known = players.find((p) => p.animationId === animationId);
|
||||
const blend = blendFrames > 0 ? { frames: blendFrames, elapsed: 0 } : undefined;
|
||||
if (known) {
|
||||
const leads = players.every((p) => p.order <= known.order);
|
||||
known.playing = true;
|
||||
known.order = ++startCount;
|
||||
if (!leads) known.blend = blend;
|
||||
return;
|
||||
}
|
||||
players.push({ animationId, time: 0, playing: true, order: ++startCount });
|
||||
players.push({ animationId, time: 0, playing: true, order: ++startCount, ...(blend && { blend }) });
|
||||
}
|
||||
|
||||
/** Ends it where it stands – the values stay; `''` ends every animation on the object. */
|
||||
|
||||
Reference in new issue
Block a user