Animation rows can target behavior variables and shader parameters (#104)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b278605c53
commit
beeea355d9
9 files changed
+273
-77
No files matched your search
@@ -14,10 +14,16 @@ import {
|
||||
SetKeyInterpolationCommand,
|
||||
SetTrackRelativeCommand,
|
||||
} from '../../src/core/commands/animationCommands';
|
||||
import { CreateAssetCommand, CreateTextureCommand } from '../../src/core/commands/assetCommands';
|
||||
import {
|
||||
CreateAssetCommand,
|
||||
CreateTextureCommand,
|
||||
SetAssetPropertyCommand,
|
||||
} from '../../src/core/commands/assetCommands';
|
||||
import { CommandError } from '../../src/core/commands/Command';
|
||||
import { CreateContainerCommand } from '../../src/core/commands/containerCommands';
|
||||
import { AddNodeCommand } from '../../src/core/commands/graphCommands';
|
||||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||||
import { RenameVariableCommand } from '../../src/core/commands/variableCommands';
|
||||
import {
|
||||
animationDuration,
|
||||
frameCount,
|
||||
@@ -25,8 +31,12 @@ import {
|
||||
playAnimation,
|
||||
playheadAt,
|
||||
stopAnimation,
|
||||
targetDefs,
|
||||
trackDef,
|
||||
trackLabel,
|
||||
trackValueAt,
|
||||
} from '../../src/core/model/animation';
|
||||
import { SHADER_KEY, SHADER_PROPERTY } from '../../src/core/properties/shaders';
|
||||
import { PREVIEW_ROOT_ID, previewHolder } from '../../src/core/model/animationPreview';
|
||||
import type {
|
||||
AnimationAsset,
|
||||
@@ -77,6 +87,8 @@ function addRow(
|
||||
}
|
||||
|
||||
const rowOf = (animation: AnimationAsset, id: string) => animation.tracks.find((t) => t.id === id)!;
|
||||
const current = (f: SceneFixture, animation: AnimationAsset) =>
|
||||
findAssetOf(f.project, animation.id, 'animation')!;
|
||||
|
||||
describe('where the playhead stands', () => {
|
||||
it('counts the last key frame as a frame of its own, so keys on 0…9 last one second at 10 fps', () => {
|
||||
@@ -649,6 +661,124 @@ describe('the reach of a played row (#109)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior variables and shader parameters as rows (#104)', () => {
|
||||
/** A box in the scene with a `Mover` behavior below it, and a shader declaring `glow`. */
|
||||
function rig() {
|
||||
const f = makeSpriteScene();
|
||||
const box = place(f, f.spriteId, { x: 0, y: 0 });
|
||||
const mover = makeBehavior(f, 'Mover', ['speed', 'x']);
|
||||
const moverObject = place(f, mover.id, { x: 0, y: 0 }, box);
|
||||
const shader = new CreateAssetCommand('shader', 'Glow');
|
||||
f.stack.execute(shader);
|
||||
f.stack.execute(
|
||||
new AddNodeCommand(shader.asset.id, SHADER_PROPERTY, 0, 0, {
|
||||
name: 'glow',
|
||||
default: 1,
|
||||
min: 0,
|
||||
max: 100,
|
||||
}),
|
||||
);
|
||||
const walk = addAnimation(f, f.sceneId, { fps: 10 });
|
||||
return { f, box, mover, moverObject, shaderId: shader.asset.id, walk };
|
||||
}
|
||||
|
||||
const ramp: [number, AnimationKey['value']][] = [
|
||||
[0, 0],
|
||||
[10, 100],
|
||||
];
|
||||
const keysOn = (f: SceneFixture, walk: AnimationAsset, target: string) =>
|
||||
targetDefs(f.project, walk, target)!.map((d) => d.key);
|
||||
|
||||
it("offers a behavior's variables and no position, and a variable `x` is the variable", () => {
|
||||
const { f, moverObject, walk } = rig();
|
||||
expect(keysOn(f, walk, moverObject)).toEqual(['enabled', 'runInEditor', 'speed', 'x']);
|
||||
const row = addRow(f, walk, 'x', ramp, moverObject);
|
||||
expect(trackLabel(f.project, current(f, walk), rowOf(current(f, walk), row))).toBe('Mover · x');
|
||||
const { world, frame } = startWorld(f, { animations: true });
|
||||
const entity = world.findByObjectId(moverObject)!;
|
||||
playAnimation(entity, walk.id);
|
||||
frame(0.1);
|
||||
frame(0.5);
|
||||
expect(entity.props.x).toBeCloseTo(50);
|
||||
expect(entity.x).toBe(0);
|
||||
const drawn = previewHolder(f.project, current(f, walk), 5)!.objects.find((o) => o.id === moverObject)!;
|
||||
expect(drawn.overrides.x).toBeCloseTo(50);
|
||||
expect(drawn.position.x).toBe(0);
|
||||
});
|
||||
|
||||
it('writes a variable before the behavior runs, so the behavior reads it this frame', () => {
|
||||
const { f, box, mover, moverObject, walk } = rig();
|
||||
addRow(f, walk, 'speed', ramp, moverObject);
|
||||
// The graph copies `speed` into `x` every frame: what it reads is what the animation just wrote.
|
||||
const edit = graphEditor(f, mover);
|
||||
const update = edit.addNode('OnUpdate');
|
||||
const read = edit.variables(['speed']);
|
||||
const write = edit.variables(['x'], { write: true });
|
||||
edit.wire([
|
||||
[update, 'exec', write, 'exec'],
|
||||
[read, 'speed', write, 'x'],
|
||||
]);
|
||||
const { world, frame } = startWorld(f, { animations: true, behaviors: true });
|
||||
playAnimation(world.findByObjectId(box)!, walk.id);
|
||||
frame(0.1);
|
||||
frame(0.5);
|
||||
const entity = world.findByObjectId(moverObject)!;
|
||||
expect(entity.props.speed).toBeCloseTo(50);
|
||||
expect(entity.props.x).toBeCloseTo(50);
|
||||
});
|
||||
|
||||
it('offers the parameters of the shader the object wears – through its own override, too', () => {
|
||||
const { f, box, shaderId, walk } = rig();
|
||||
const plain = place(f, f.spriteId, { x: 50, y: 0 });
|
||||
expect(keysOn(f, walk, box)).not.toContain('glow');
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, box, SHADER_KEY, shaderId));
|
||||
expect(keysOn(f, walk, box)).toContain('glow');
|
||||
expect(keysOn(f, walk, plain)).not.toContain('glow');
|
||||
expect(() => f.stack.execute(new AddAnimationTrackCommand(walk.id, plain, 'glow'))).toThrow(CommandError);
|
||||
});
|
||||
|
||||
it('plays a shader parameter at runtime and in the preview alike', () => {
|
||||
const { f, box, shaderId, walk } = rig();
|
||||
f.stack.execute(new SetAssetPropertyCommand(f.spriteId, SHADER_KEY, shaderId));
|
||||
addRow(f, walk, 'glow', ramp, box);
|
||||
const { world, frame } = startWorld(f, { animations: true });
|
||||
const entity = world.findByObjectId(box)!;
|
||||
playAnimation(entity, walk.id);
|
||||
frame(0.1);
|
||||
frame(0.5);
|
||||
expect(entity.props.glow).toBeCloseTo(50);
|
||||
const drawn = previewHolder(f.project, current(f, walk), 5)!.objects.find((o) => o.id === box)!;
|
||||
expect(drawn.overrides.glow).toBeCloseTo(50);
|
||||
expect(f.sceneOf().objects.find((o) => o.id === box)!.overrides.glow).toBeUndefined();
|
||||
});
|
||||
|
||||
it('reads as missing once the variable is renamed or the shader taken off, and keeps its keys', () => {
|
||||
const { f, box, mover, moverObject, shaderId, walk } = rig();
|
||||
f.stack.execute(new SetAssetPropertyCommand(f.spriteId, SHADER_KEY, shaderId));
|
||||
const speed = addRow(f, walk, 'speed', ramp, moverObject);
|
||||
const glow = addRow(f, walk, 'glow', ramp, box);
|
||||
f.stack.execute(new RenameVariableCommand(mover.id, 'speed', 'pace'));
|
||||
f.stack.execute(new SetAssetPropertyCommand(f.spriteId, SHADER_KEY, ''));
|
||||
const now = current(f, walk);
|
||||
for (const id of [speed, glow]) {
|
||||
expect(trackDef(f.project, now, rowOf(now, id))).toBeUndefined();
|
||||
expect(trackLabel(f.project, now, rowOf(now, id))).toMatch(/\(missing\)$/);
|
||||
expect(rowOf(now, id).keys).toHaveLength(2);
|
||||
}
|
||||
const { world, frame } = startWorld(f, { animations: true });
|
||||
playAnimation(world.findByObjectId(box)!, walk.id);
|
||||
frame(0.1);
|
||||
frame(0.5);
|
||||
expect(world.findByObjectId(moverObject)!.props.speed).toBeUndefined();
|
||||
expect(world.findByObjectId(box)!.props.glow).toBeUndefined();
|
||||
// A missing row may go anywhere; the kind rule holds again once it aims at something.
|
||||
f.stack.execute(new RetargetAnimationTrackCommand(walk.id, speed, moverObject, 'pace'));
|
||||
expect(() =>
|
||||
f.stack.execute(new RetargetAnimationTrackCommand(walk.id, speed, moverObject, 'enabled')),
|
||||
).toThrow(CommandError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the animation nodes', () => {
|
||||
/** A behavior below the sprite plays `walk` on its owner at start and counts `On animation finished`. */
|
||||
function listening(filter: 'walk' | 'other') {
|
||||
|
||||
Reference in new issue
Block a user