An animation is a timeline of rows over one asset, played on an object rather than worn as a picture (#97)

Part (a) of the animation rework: model, commands, saving, runtime and nodes.

- AnimationAsset holds a root (sprite, text, container or scene) and rows; a row keys one
  property of the root or of an object inside it, keys sit on whole frames.
- The last key decides the length (its frame counts as a frame of its own). The playhead runs
  between frames, so numbers and colors blend every tick even at 10 fps; everything else holds.
- Playback gains `once-reset`, which puts back what was there before the start. A row can be
  relative, adding to where its target stood.
- Play / Pause / Stop animation act on an object; each instance plays on its own, the later
  start wins on a shared property, and a behavior writing the same property still writes last.
- A sprite no longer wears an animation through `texture`. "Create animation from texture"
  builds a sprite plus a texture row keyed frame by frame instead.
- The Animation tab shows a placeholder until the timeline editor (part b) lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-16 20:02:25 +02:00
1 parent d3563897d8
commit 193378e6a9
29 files changed
+1194 -1499

No files matched your search

+4 -1
View File
@@ -1,3 +1,4 @@
import type { AnimationPlayer } from '../model/animation';
import type { Mat } from '../model/matrix';
import type { Position, PropertyBag } from '../model/Properties';
import { type ChainTransform, composeAlongChain } from '../model/walk';
@@ -28,9 +29,11 @@ export interface EntityHandle {
parent: EntityHandle | null;
/**
* Counts the Emit pulses a particle source has been given. Kept off `props` for the reason the
* animation clock is: what the world writes back to the model must not grow an override per frame.
* animation players are: what the world writes back to the model must not grow an override per frame.
*/
emits?: number;
/** What `Play animation` started on this object (see `model/animation.ts`). */
animations?: AnimationPlayer[];
}
/** Position, rotation, scale and mirror of every ancestor applied, nearest last. */
@@ -0,0 +1,64 @@
import { pauseAnimation, playAnimation, stopAnimation } from '../../../model/animation';
import { textOf } from '../../../model/Properties';
import type { PropertyDef } from '../../../properties/PropertySchema';
import type { NodeDef } from '../../NodeRegistry';
import { asEntity } from '../../values';
import { exec, obj } from '../shared';
const animationParam = (): PropertyDef => ({
key: 'animation',
label: 'Animation',
kind: 'asset',
default: '',
assetTypes: ['animation'],
});
export const PlayAnimation: NodeDef = {
type: 'PlayAnimation',
label: 'Play animation',
category: 'action',
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 is left alone.',
inputs: [exec(), obj('object')],
outputs: [exec()],
params: [animationParam()],
run: (_ctx, inputs, params) => {
const target = asEntity(inputs.object);
const animation = textOf(params.animation ?? '');
if (target && animation) playAnimation(target, animation);
return { next: 'exec' };
},
};
export const StopAnimation: NodeDef = {
type: 'StopAnimation',
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')],
outputs: [exec()],
params: [animationParam()],
run: (_ctx, inputs, params) => {
const target = asEntity(inputs.object);
if (target) stopAnimation(target, textOf(params.animation ?? ''));
return { next: 'exec' };
},
};
export const PauseAnimation: NodeDef = {
type: 'PauseAnimation',
label: 'Pause animation',
category: 'action',
description:
'Holds an animation on an object at its frame until Play animation lets it run on. Leave Animation at None to hold every one.',
inputs: [exec(), obj('object')],
outputs: [exec()],
params: [animationParam()],
run: (_ctx, inputs, params) => {
const target = asEntity(inputs.object);
if (target) pauseAnimation(target, textOf(params.animation ?? ''));
return { next: 'exec' };
},
};
+1 -1
View File
@@ -28,7 +28,7 @@ export const StringLiteral = literal('String', 'text', 'A constant text.');
export const TextureLiteral = literal(
'Texture',
'texture',
'A constant picture: a texture, one of its frames or an animation, as a sprite wears it.',
'A constant picture: a texture or one of its frames, as a sprite wears it.',
);
export const StateMachineLiteral = literal(
'StateMachine',
+4
View File
@@ -1,5 +1,6 @@
import { type NodeDef, registerNode } from '../NodeRegistry';
import { Destroy } from './action/Destroy';
import { PauseAnimation, PlayAnimation, StopAnimation } from './action/animationNodes';
import { Emit } from './action/Emit';
import { LoadScene } from './action/LoadScene';
import { Log } from './action/Log';
@@ -204,6 +205,9 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
Spawn,
Destroy,
Emit,
PlayAnimation,
PauseAnimation,
StopAnimation,
PlaySound,
StopSound,
LoadScene,