From 9370c8aacfc1b771a76a923ebca265e4980e5ef3 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Wed, 16 Sep 2026 21:00:09 +0200 Subject: [PATCH] Animations: the timeline model, the tab with preview over timeline, play/stop/pause (#97, #98, #99) --- Animations.md | 98 ++++++++++++++++++++++++++++++------------------- Architecture.md | 3 +- Behaviors.md | 2 +- Home.md | 4 +- Keyboard.md | 2 +- 5 files changed, 67 insertions(+), 42 deletions(-) diff --git a/Animations.md b/Animations.md index 2d1778a..a24a6dd 100644 --- a/Animations.md +++ b/Animations.md @@ -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. diff --git a/Architecture.md b/Architecture.md index fab2af3..d4b6990 100644 --- a/Architecture.md +++ b/Architecture.md @@ -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) diff --git a/Behaviors.md b/Behaviors.md index dd46a4f..c26ba9f 100644 --- a/Behaviors.md +++ b/Behaviors.md @@ -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 diff --git a/Home.md b/Home.md index c54f8f2..ff8e0d3 100644 --- a/Home.md +++ b/Home.md @@ -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}`. diff --git a/Keyboard.md b/Keyboard.md index f85631c..480a1de 100644 --- a/Keyboard.md +++ b/Keyboard.md @@ -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,