28
Animations
MelekTaus edited this page 2026-10-18 19:01:45 +02:00

An animation asset is a timeline over one root asset — a sprite, text, container or scene. It has rows, each aiming at one property of the root itself or of an object placed inside it (a container's or scene's tree), and each row has keys: a frame number and a value. It is played on an object (Play animation), and its rows reach that object and what its template placed inside it, so every placed instance of a container plays the same animation on its own. A row reaches into containers placed inside the root, however deep — a container's row names a path of template ids, which every instance resolves below itself. Played on an object of another container built alike, a row whose ids are not there finds its object by the names on the way down instead: a row on the root's Sprite reaches the Sprite of a slime and of a skeleton alike, and one on Weapon/Hitbox goes level by level. A row whose name is missing at some level, or taken twice, is skipped. A scene animation reaches the scene's objects by id while that scene runs, the content of its placed containers included (the same ids whether play bakes that content or the edit mode resolves it), and it can key the scene itself: its own properties such as Background, but no position, since a scene is placed nowhere. When a container template loses the object a row aims at, the row reads (missing) like any other whose target is gone.

A placed root or object offers its position as the first rows: X, Y and Z. Z is the draw order (higher is on top), and the order is sorted again on every render, so a keyed Z moves the object behind or in front of others while the animation plays — a weapon that swings up can pass behind its character. Blended values between two keys count as they are: at Z 4.5 the object is above everything on 4 and below everything on 5. A child with Sort with parent on moves only against its parent and siblings, not against the whole scene (see Scene editing, #232).

Besides the schema's own properties, a row can key what an object declares (#104): a behavior's variables — pick the behavior (it sits below its owner in the object list) and its variables are offered next to Enabled; a behavior offers no X/Y/Z, so a variable named x is that variable — and the parameters of the shader a sprite wears, whether the asset wears it or the placed object overrides it. Shader parameters are numbers, as the Property node declares them, and keys are clamped to its min/max. A variable is written before the behaviors run, so a graph reads this frame's value. When the behavior is removed, the variable renamed or the shader swapped for one without the parameter, the row reads (missing) and keeps its keys. A variable switched Private (see Behaviors) is not offered for a new row (#270). A row that aims at it already stays: it plays, keeps its label, and Change target… in its inspector still lists its own variable. Under the hood the dialog asks offeredDefs in src/core/model/animation.ts, while the label and the runtime read animatableDefsOf, which knows nothing of the lock.

Its two properties are Frames per second (default 10) and Playback: Loop, Once (hold) (ends on the last value and leaves it), Once (reset) (ends and puts back what every target held before the start) or Ping-pong. The last key over all rows decides how long one pass lasts — the last key's frame counts as a frame of its own, so keys on 0…9 last one second at 10 fps. A pause at the end is a last key that repeats the value. Ping-pong turns on the last key, so neither end plays twice: frames keyed on 0…9 play 0…9…1 and start over, eighteen frames a bounce. On the way back a value that jumps holds the key the playhead passed last, the one above it. Between two keys the playhead runs on every tick, not only on whole frames: at 10 fps a moving x moves each frame, the fps only sets how far apart the keys are in time.

Values. Numbers and colours blend linearly between keys; a texture, a boolean, a text or a choice holds until the next key (that is how a picture sequence works: a texture row keyed frame by frame). A sprite wearing a video keys its Video time to show the picture of a moment (see Videos). Before the first key a row holds its first value, after the last one its last. A row may be relative: its numbers are added to what the target held when the animation started, so one walk cycle can bob every enemy where it stands. Rotation blends like any number — from 350 to 10 goes backwards over 340°, a full turn is 0 → 360. A row whose object is gone stays, labelled (missing), and is skipped when playing. The same object and property cannot be keyed twice on one animation.

Easing. Every key of a number or colour row has an Easing for the way from it to the next key. The inspector shows it as two fields:

  • Easing is the curve: Linear (the default, stored as no field at all), Step (holds until the next key, like a texture does), or one of the ten curves also known from CSS and easings.net: Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back, Elastic, Bounce. Or Curve: an easing you drew yourself as a curve asset, picked in the Curve row that appears below. Its first curve is picked for you; a curve that is gone blends linearly, and the inspector says so.
  • Direction only appears for a curve: In starts slow, Out starts fast and ends slow, In-out is slow at both ends and halfway at half the time. A new curve keeps the direction the key had; a key that was linear or a step gets In-out.

The same choice stands on a Lerp node and on Set sound FX parameters (see Behaviors and Sounds).

How strongly a curve bends, at half the time going In: Sine 0.29 of the way, Quad 0.25, Cubic 0.125, Quart 0.06, Quint 0.03, Expo 0.03, Circ 0.13. Out is the same curve mirrored, In-out runs the In curve on the first half and the Out curve on the second. Back first pulls back about 10 % against the way (or shoots 10 % past the end going Out), Elastic swings around the end several times, Bounce hops into the end like a dropped ball.

Back and Elastic leave the range between the two keys. For position or rotation that is the point. A number property with a min or max is held to it, so a Back in from scale 0 stays at 0 instead of turning the sprite over. A colour is held to its channels.

Stored, a curve is <curve>-<direction> (cubic-out, back-in-out). Quad keeps the name it had when it was the only curve, ease-in, ease-out and ease-in-out, so older projects play as before. Set the easing in the inspector for all picked keys at once; keys of a row that cannot blend are left alone. A new value or a move keeps a key's easing. The timeline draws the direction into the key: the right half is the start of the way, the left half its end, a round half a slow one — a diamond is linear, a circle In-out or a drawn Curve, a half-round key In or Out, a square a step. Player and preview evaluate it in one place, so both show the same curve.

The Animation tab. Each animation opens in a tab of its own. The header picks what it animates (the root), + Row (a dialog: the root or one of its objects, then a property of that; the inspector's Change target… reopens it to move a row's keys to another property of the same kind), Play / Pause and Fit. Below the header the preview shows the root at the playhead, drawn like the Scene tab draws a scene but never edited — a drag pans, the wheel zooms about the pointer, Fit frames the content again. It is built from a copy of the root with every row's value at the playhead written over it, so the document stays untouched and undo never hears of scrubbing; past the end of a Once (reset) pass it shows the starting values, as the player would. Under the preview lies the timeline: a row per target and property, a frame ruler on top. The rows are grouped by object (#408): each object has a head line with its name, indented by its depth, and the groups stand in the order of the hierarchy, the root first. Under its head an object's rows follow the order the object lists its properties in (X, Y, Z, then the inspector's order), so they sort themselves however they were added. Rows whose object is gone come last. Only the view is sorted, the animation keeps its rows as they are. Each row shows its value at the playhead on the right of its header, in the field the inspector uses for that kind (a number, a tick, a colour, a picture …). Before the row has keys it shows what the object holds, a relative row shows its offset. Editing that field sets a key at the playhead (or changes the one there) and picks it; while playing, it pauses first. Double-click a cell to set a key with the value that holds there, on a row without keys the value its field shows (then edit the value in the row or in the inspector while the key is selected); click a key to pick it, drag a band to pick several, drag keys to move them (one landing on another replaces it), Delete removes the picked keys — or the whole row when its header is picked. Ctrl+drag duplicates keys: the originals stay, the copies land where the pointer lets go. Ctrl+C copies the picked keys and Ctrl+V pastes them with the earliest one on the playhead's frame, keeping their distances: into the rows they came from, or — for keys of a single row — into the row picked now, if it is of the same kind. In another animation each row finds the row animating the same property; rows without one are left out. A pasted key replaces one on its frame. Right-click the timeline for Copy, Paste, Insert frames… and Remove frames…: both ask for a count and work at the playhead over every row — inserting moves every key from the playhead's frame on later, removing drops the keys in the removed frames and pulls the rest earlier. Each is one undo step. The same menu has Delete row for the row under the pointer; right-clicking a row's header also picks that row. On a Video time row it also offers to key the video at 1x to its end (see Videos). Click or drag the ruler to scrub. The wheel zooms the frames about the pointer, from 48 pixels a frame out to 50 frames a pixel, so an hour at 30 fps fits a screen (#407). Shift+wheel scrolls across, Shift+drag pans across, the middle button pans both ways. More rows than room (#413): the wheel over the header column scrolls the rows up and down, the ruler stays where it is, and a scrollbar at the right edge can be dragged. A row picked or added out of view is scrolled into it. Folding a group (#414): a click on an object's head, or the triangle in front of its name, folds its rows away, and the groups of the objects below it fold with it. The root's head stands level with its children, so folding it hides only its own rows. Every head shows summary keys, folded or not: a diamond on each frame on which any row of the group, or of a group below it, has a key. A summary key stands for all of those keys: a click picks them, a drag moves them (Ctrl+drag duplicates), a band catches them, Delete removes them, each one undo step. Values stay with the rows, as they differ from row to row. Folding is a way of looking: it is no undo step and lasts for the session, per animation. A row picked on its own while its group is folded, one just added for example, opens the group. Scrubbing while the animation plays pauses it, and the button turns back to Play.

Play, stop, pause. Play animation starts an animation on an object (both are ports: the object as any action takes it, the animation from an Animation literal, a list item tagged $animation, or a variable). A paused one runs on; one already running keeps its frame but counts as started last again — so a graph may play the walk it wants every frame without freezing it, and turning back to a walk that still runs shows that walk. Stop animation ends it where it stands (the values stay), Pause animation holds it; leave Animation at None to end or hold every animation on the object. Several animations may run on one object; of two setting the same property the later start wins, and a behavior writing it in the same frame writes last. The AnimationSystem keeps each player on the entity, not in props, and ticks before the behaviors, so On animation finished fires in the frame a Once animation reaches its end, exactly once, with the object it played on as object. It is heard by the behaviors of that object's owner subtree — the object itself or one placed below the owner next to the behavior — and by a behavior without an owner for every object; its Animation setting narrows it to one. A behavior also hears every animation it played itself, wherever that object stands: Play animation on the parent of the owner, or on an object across the scene, is heard by the behavior that asked for it. Two behaviors that both played it each hear it once.

Blend. Play animation has a blend (s) port, 0 by default. Above 0, an animation that takes over fades in over that many seconds, whatever its frame rate: numbers and colours mix linearly from what the property held into the animation's value; a texture, a boolean, a text or a choice switches at once. "What the property held" is live where an animation started earlier writes there in the same frame — the walk it takes over from keeps moving under the fade — and otherwise the value when the blend began, be it the template's or one a stopped animation left. A third start during a blend fades from the value already mixed, so nothing jumps. The blend starts only when the animation takes over: played again every frame while it already leads, it keeps the blend it has. Winning back one that still runs blends again with the time given then; the tab's preview plays one animation and has no blend.

Blending out. Stop animation has a blend (s) port as well. At 0 it ends the animation where it stands and the values stay; above 0 the animation fades out over that many seconds instead, back to what lies below it: live what an animation started earlier writes there, otherwise the value the row found when the animation started. A texture, a boolean, a text or a choice goes back at once. A paused animation fades out too. Play animation has blend out (s) for Once (reset): when its frames run out it fades back to where it started over that many seconds instead of jumping; On animation finished fires when the frames run out, not when the fade ends. Playing an animation again while it fades out turns it round from where it stands, without a jump — a Once (reset) pass that already ran out starts over, blending in from the value the fade had reached.

Restart. Play animation has a restart switch, off by default. On, an animation that is already running, paused or fading out starts over from its first frame and counts as started last; with blend (s) above 0 it blends in from where the property stands, otherwise it jumps. Its starting values stay those of its first start, so a relative row does not add up. Played with restart on every frame, an animation never gets past its first frame — use it on a single event such as a key press.

Asking whether it still runs. Animation playing is a data node with the inputs object and animation and three answers, read at the moment they are asked for — right after a Play animation in the same pulse it already says yes:

  • playing is on while the animation runs. A paused one does not run, and neither does one that Stop animation ended or is fading out. A Once (reset) pass blending back to where it started still runs: it still shows. In the frame On animation finished fires for a Once animation it is off.
  • progress is where the playhead stands on the timeline, 0 to 1, in every playback. On Loop it starts over with each pass; on Ping-pong it rises to 1 at the last key and falls again on the way back.
  • time left (s) is the time until the playhead reaches the end of its pass: the end of the animation for the two Once modes, with blend out (s) added for Once (reset); the end of the round on Loop; the next turn on Ping-pong. A paused animation keeps its place and its time left.

Leave Animation at None to ask for whatever runs on the object: the animation started last among those running, or the one started last at all when none runs. With no object, or an animation that is not on it, the answers are off, 0 and 0. So "no new attack while the swing runs" is one Branch on playing instead of a timer that has to repeat the animation's length.

Animation length gives the seconds one pass takes, from the timeline and the frame rate alone — for a cooldown that is meant to be longer than the animation (length + 0.1).

Under the hood: the rule is playerStatus and leadingPlayer in src/core/model/animation.ts; the nodes reach the animation through GraphLookup.animation, which hands a run only the frame count, the frame rate and the playback.

It runs in play only. The Scene and Container tabs show the objects as stored; the edit mode has no clock. The fastest way in is right-clicking a texture → Create animation from texture, which makes a sprite of the texture and an animation over it with a texture row keyed frame by frame (the whole picture when the sheet has no frames), both named after the texture, in one undo step.

Deleting a texture counts the animations that key its frames besides the sprites wearing it; deleting an animation counts the graphs that name it. Either leaves dangling references — a missing picture draws the plain colour, a missing animation plays nothing — and undo brings them back.