Animations: the timeline model, the tab with preview over timeline, play/stop/pause (#97, #98, #99)

admin committed 2026-09-16 21:00:09 +02:00
1 parent 770f891cc9
commit 9370c8aacf
5 files changed
+67 -42

No files matched your search

+61 -37
@@ -1,41 +1,65 @@
An **animation asset** is a list of pictures played one after another. Each entry is what a
sprite's `texture` can hold — a whole texture or one of its frames — so an animation may mix
frames of several sheets, and a picture listed twice is held twice as long; there is no
per-frame duration beyond that. Its two properties are **Frames per second** (default 10) and
**Playback**: *Loop*, *Once* (stops on the last frame) or *Ping-pong* (back and forth, without
showing the turning frames twice).
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 scene animation reaches the scene's objects by id while that scene runs.
**A sprite wears it through `texture`** — the same property, the same picker (animations are
listed first, marked "(animation)") and the same instance overrides as a texture or a frame.
The sprite stays what is drawn, sized, tinted and collided with; the animation is only a picture
that changes over time. *Set to W x H px* takes the size of the first frame. The fastest way in
is right-clicking a texture → **Create animation from texture**, which plays its frames in
order (the whole picture when it has none), named after the texture, in one undo step.
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. 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.
**The Animation tab** plays the animation above a strip of its frames. `▶` / `❚❚` pauses on the
frame on screen, and resumes from the selected one. Clicking a tile pauses on it, dragging a
tile into another gap reorders the frames, `Delete` removes the selected one, right-click offers
*Duplicate frame* and *Remove frame*, and **+ Frames ▾** inserts after the selection: each
sheet's frames at once, or single pictures. The selected tile belongs to the tab, not to the
editor's selection, so the inspector keeps showing the animation's two properties meanwhile.
The preview fits each frame on its own until the wheel zooms it about the pointer; from then on
every frame is drawn at that one scale, centred, so frames of different sizes show their real
proportion. The middle button (or `Shift`+drag) pans, **Fit** returns to fitting, and over the
strip the wheel scrolls the tiles instead.
**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). 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. Every key has an
`interpolation` field that may be absent and then means linear; easing lands there later. 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.
**It runs in play only.** The Scene and Container tabs show the first frame: the edit mode has
no clock and redraws on changes only. The `AnimationSystem` gives every sprite its own clock, kept on the entity and
not in `props` (the world writes changed props back as overrides). The one rule that makes it
usable from a graph: **a different animation starts from its first frame, the same one set again
runs on** — so `On update → Properties (Sprite, texture on Set)` may choose Run or Idle every
frame without freezing the animation on frame one. The system ticks before the behaviors, so
**`On animation finished`** fires in the frame a *Once* animation reaches its end, exactly once,
with the sprite as `object`. Like collisions it is heard by the behaviors of the sprite's owner
subtree — the sprite itself, or a sprite placed next to the behavior below the same object — and
by a behavior without an owner for every sprite; its *Animation* setting narrows it to one.
There is no node to restart an animation that is already running; that is left for when a game
needs it.
**The Animation tab.** The header picks the animation, 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 **Centre**. 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, *Centre* 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. **Double-click** a cell to set a key with the value that holds there (then edit the value 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. Click or drag the ruler to scrub. The wheel zooms the
frames about the pointer, `Shift`+wheel scrolls, the middle button (or `Shift`+drag) pans, **Fit**
returns to frame 0 at the default zoom. Rows are not collapsed or reordered.
Deleting a texture counts the animations that play its frames besides the sprites wearing it;
deleting an animation counts the sprites wearing it. Either leaves dangling references that draw
the plain colour and come back on undo.
**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.
**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.
+2 -1
@@ -101,7 +101,8 @@ src/editor/graph/ GraphEditor (header + picker), GraphCanvas, EdgeLayer (cu
src/editor/container/ ContainerEditor (header + picker), ContainerCanvas (a SceneViewport on a template)
src/editor/texture/ TextureEditor (header + picker), TextureCanvas (picture + frames), SliceDialog, sheetView (the grid maths),
ImagePicker + imageGrid (the "+ → Texture" dialog), checker (the transparency board)
src/editor/animation/ AnimationEditor (header + picker), AnimationCanvas (preview + frame strip), strip (its geometry)
src/editor/animation/ AnimationEditor (header + picker, preview over timeline), PreviewCanvas (the root at the playhead, a WorldViewport
over animationPreview.ts), TimelineCanvas (rows, keys, ruler), timelineView (its geometry), RowDialog
src/editor/statemachine/ StateMachineEditor (header + picker, on the graph canvas)
src/editor/particles/ ParticlesEditor (graph canvas left of a preview panel, splitter between them),
ParticlePreview (the panel: the asset's own particles, wearing its own rows)
+1 -1
@@ -57,7 +57,7 @@ shown on the node and editable in the inspector.
not the list. Three kinds need saying, because JSON cannot tell them from the value beside them: a
**colour** is a number, and a **texture** or a **state machine** is an id, which is a text. Such a *tagged* kind wears its
name as the one key of a small object — `{"$color": 16711680}`, `{"$texture": "walk-up"}` (the
picker a sprite's `texture` property has — a whole picture, one frame of a sheet, or an animation),
picker a sprite's `texture` property has — a whole picture or one frame of a sheet),
`{"$statemachine": "doors"}`.
A field name cannot start with `$` (`isValidFieldName`), so a tag can never collide with a struct
item of one field. Either reads as what it wraps on a data
+2 -2
@@ -43,7 +43,7 @@ into issues #1-#50, closed ones included, each carrying the commits it was filed
Several scenes in one run (a `Load scene` node — screens are containers
today), auth, touch-optimised layout, graph comments/groups. Sound has no mixer, no fade and no
`Set volume` on a playing voice (see [Sounds](Sounds)). A frame is always stretched to
the sprite's `width` x `height`: no nine-slice. Animations have no state machine, no restart node
and no preview in the Scene tab. Slicing cuts a regular grid only — no detection of where the sprites actually
the sprite's `width` x `height`: no nine-slice. Animations have no easing, no events on the
timeline and no preview in the Scene tab. Slicing cuts a regular grid only — no detection of where the sprites actually
sit on the sheet. Unreferenced image files are never collected — deliberately, since the image
picker offers them again; nothing deletes a stored image except `DELETE ?r=textures/{file}`.
+1 -1
@@ -1,5 +1,5 @@
`Ctrl+Z` / `Ctrl+Shift+Z` / `Ctrl+Y` undo/redo (`Cmd` works wherever `Ctrl` does) · `Delete` or
`Backspace` remove the selection (objects, nodes, frames, a tile of the Animation tab, a graph group or comment — the box only, its nodes stay, and a band that caught boxes and nodes together removes both in one step; an asset asks first) · `Ctrl+G` create
`Backspace` remove the selection (objects, nodes, frames, keys or a row of the Animation tab, a graph group or comment — the box only, its nodes stay, and a band that caught boxes and nodes together removes both in one step; an asset asks first) · `Ctrl+G` create
container from the selection · `Ctrl+Enter` play, and pause/resume while playing · `Esc` stop ·
`Ctrl+V` import an image from
the clipboard as a texture (ignored while a text field has focus) · `Tab` / `Shift+Tab` cycle fields · lists: `↑`/`↓` move,