An animation is a value a graph can pick, and playing a running one makes it win again (#97)

Play, Pause and Stop animation take the animation on a port instead of a
fixed setting, fed by the new Animation literal or a list item tagged
`$animation`. Playing an animation already running keeps its frame but
counts it as started last, so turning back to a walk that never stopped
shows that walk. The actor demo walks through one Play animation over
`ItemAt(walk, facing)` again and stops every animation to stand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-16 20:47:31 +02:00
1 parent 5af0a1c83a
commit 4ed2edd0f0
10 files changed
+158 -71

No files matched your search

+31 -19
View File
@@ -7,13 +7,13 @@
* (down, left, right, up). This demo uses the character in the top left corner, so its frames are
* the columns 1-3 of the rows 1-4.
*
* Walking is one animation per direction, playing those three frames as `pingpong`
* (left-stand-right-stand …) – the original walk cycle. Standing is not an animation at all but the
* middle frame of the row.
* Walking is one animation per direction over the sprite: a `texture` row keying those three frames,
* played `pingpong` (left-stand-right-stand …) – the original walk cycle. Standing is not an
* animation at all but the middle frame of the row.
*
* The graph picks a picture the way the editor does: two list variables hold the four walk
* animations and the four standing frames, each item chosen with the texture picker, and `facing`
* is the row index that indexes both. Nothing is built out of an id spelled by hand.
* The graph picks the way the editor does: two list variables hold the four walk animations and the
* four standing frames, each item chosen with its picker, and `facing` is the row index that indexes
* both. Nothing is built out of an id spelled by hand.
*
* Usage: node tools/seed-actor.mjs [projectId] [baseUrl]
*/
@@ -68,11 +68,19 @@ const walkAnimations = DIRECTIONS.map((dir, row) => ({
type: 'animation',
name: `Walk ${dir}`,
props: { fps: 8, playback: 'pingpong' },
frames: [frameId(row, 0), frameId(row, 1), frameId(row, 2)],
root: 'actor-sprite',
tracks: [
{
id: `${WALK(dir)}-texture`,
target: '',
property: 'texture',
keys: [0, 1, 2].map((column) => ({ frame: column, value: frameId(row, column) })),
},
],
}));
/** The two lists the graph indexes: one walk animation and one standing frame per direction. */
const WALK_PICTURES = DIRECTIONS.map((dir) => tagged('texture', WALK(dir)));
const WALK_ANIMATIONS = DIRECTIONS.map((dir) => tagged('animation', WALK(dir)));
const IDLE_PICTURES = DIRECTIONS.map((_, row) => tagged('texture', idleFrame(row)));
/**
@@ -129,9 +137,10 @@ const nodes = [
face('c-up', 'up', 1920, 940),
face('c-down', 'down', 1920, 1080),
// What the sprite shows. Both picture lists are indexed by the row the character faces, so there
// is one node per list and no branch per direction.
// Setting the same animation again lets it keep running, so writing it every frame is free.
// What the sprite shows. Both lists are indexed by the row the character faces, so there is one
// node per list and no branch per direction. Playing a running walk again keeps its frame but makes
// it the one that wins, so turning back to a walk still running shows that walk. Standing stops
// every animation and wears the standing frame.
node('br-move', 'Branch', 1460, 1280),
node('v-pics', 'Variables', 1400, 1480, {}, [
port('walk', 'list'),
@@ -140,8 +149,9 @@ const nodes = [
]),
node('pick-walk', 'ItemAt', 1680, 1430),
node('pick-idle', 'ItemAt', 1680, 1640),
node('wear-walk', 'Properties', 1920, 1280, propertiesOf('sprite'), [written('texture', 'texture')]),
node('wear-idle', 'Properties', 1920, 1510, propertiesOf('sprite'), [written('texture', 'texture')]),
node('play-walk', 'PlayAnimation', 1920, 1280),
node('stop-walk', 'StopAnimation', 1920, 1510),
node('wear-idle', 'Properties', 2160, 1510, propertiesOf('sprite'), [written('texture', 'texture')]),
];
const edges = [
@@ -198,12 +208,14 @@ const edges = [
edge('e48', 'v-pics', 'facing', 'pick-walk', 'index'),
edge('e49', 'v-pics', 'idle', 'pick-idle', 'list'),
edge('e50', 'v-pics', 'facing', 'pick-idle', 'index'),
edge('e51', 'br-move', 'then', 'wear-walk', 'exec'),
edge('e52', 'owner', 'object', 'wear-walk', 'object'),
edge('e53', 'pick-walk', 'item', 'wear-walk', 'texture'),
edge('e54', 'br-move', 'else', 'wear-idle', 'exec'),
edge('e51', 'br-move', 'then', 'play-walk', 'exec'),
edge('e52', 'owner', 'object', 'play-walk', 'object'),
edge('e53', 'pick-walk', 'item', 'play-walk', 'animation'),
edge('e54', 'br-move', 'else', 'stop-walk', 'exec'),
edge('e55', 'owner', 'object', 'wear-idle', 'object'),
edge('e56', 'pick-idle', 'item', 'wear-idle', 'texture'),
edge('e57', 'owner', 'object', 'stop-walk', 'object'),
edge('e58', 'stop-walk', 'exec', 'wear-idle', 'exec'),
];
const sheet = await uploadTexture(baseUrl, SHEET_FILE);
@@ -251,11 +263,11 @@ const project = {
id: 'player',
type: 'behavior',
name: 'Player',
props: { speed: SPEED, facing: 0, walk: WALK_PICTURES, idle: IDLE_PICTURES },
props: { speed: SPEED, facing: 0, walk: WALK_ANIMATIONS, idle: IDLE_PICTURES },
variables: [
variable('speed', 'number', SPEED),
variable('facing', 'number', 0),
variable('walk', 'list', WALK_PICTURES),
variable('walk', 'list', WALK_ANIMATIONS),
variable('idle', 'list', IDLE_PICTURES),
],
graph: { nodes, edges },