# `tecs.gfx.animation` Fixed-step sprite-sheet playback. A [`Sheet`](tecs.gfx.sheet.Sheet) divides one image into frames and names frame spans with tags. An `Animation` selects a sheet and a tag and carries the speed, the loop flag, and where the cycle has got to. Playback advances on the fixed clock. The GPU resolves the shared frame table from the entity's [`Sprite`](tecs.gfx.Sprite) playback encoding, so clean animated crowds perform no per-entity CPU work or instance uploads as frames change. ```nupp const hero = tecs.gfx.sheet.grid({ name = "hero", imageWidth = 256, imageHeight = 32, frameWidth = 32, frameHeight = 32, tags = {idle = {from = 1, to = 4}, run = {from = 5, to = 8}}, }) hero:bind(tecs.gfx.images.id("sprites/hero")) tecs.gfx.animation.plugin(world) world:spawn( tecs.ecs.Transform2D(64, 64, 0, 1, 0, 32, 32), tecs.gfx.Tint(1, 1, 1, 1), hero:sprite(), tecs.gfx.animation.of(hero, "run"), tecs.gfx.Renderable2D ) ``` An entity needs a [`Sprite`](tecs.gfx.Sprite) as well, because that is what playback writes and what extraction reads. An `Animation` on its own draws nothing. # Events `Completed` fires once when a one-shot passes its last frame, and `Looped` fires when a looping tag wraps. Both reach only entities carrying `AnimationEvents`, so a game watches a handful of animations without walking the rest of an animated crowd. `Completed` also clears `playing` on watched entities. Unwatched one-shots clamp to the last frame entirely on the GPU. # Reloads Re-exporting a sheet under the same name and folding it in with `tecs.gfx.sheet.replace` keeps every entity's sheet id, tag, and phase, so playback carries on and shows the new frames from its next step. ## Types ### `Animation` _struct_ ```nupp struct Animation sheet: integer tag: integer speed: number time: number frame: integer loop: boolean playing: boolean crossed: integer end ``` `@derive(nupp.derive.Debug, nupp.derive.Serde)` Stores playback state for one entity. `time` is where the cycle has got to, in seconds, and stays inside the cycle, so it neither grows without bound nor loses precision to its own age. The cycle's duration is the sum of the durations of the frames the tag visits, which `tecs.gfx.sheet.Sheet.cycle` reports. #### Fields ##### `sheet` ```nupp sheet: integer ``` Caller-writable. Selects a sheet by its registration index. Zero plays nothing. ##### `tag` ```nupp tag: integer ``` Caller-writable. Selects a tag by its `Sheet.tagId` index. Zero plays the whole sheet in order. ##### `speed` ```nupp speed: number ``` Caller-writable. Multiplies the timing the sheet carries. One is the timing as authored and two is twice as fast. Zero or less holds the current frame and stops time advancing, which is a pause that leaves `playing` alone. How long each frame is held is the sheet's answer rather than an entity's, because that is where an artist sets it: a hold frame is a frame with a long duration, which no single rate can express. ##### `time` ```nupp time: number ``` Caller-writable. Supplies the phase used when frame is zero. To seek, write time and reset frame to zero. Read live phase with timeOf. ##### `frame` ```nupp frame: integer ``` Engine-owned. Holds -1 for encoded GPU playback, or zero to request re-encoding from time. Ordinary game code should call `frameOf`, which answers for an entity carrying no `Animation` too. Writing zero asks playback to rewrite the `Sprite`, which is what `play`, `restart`, `of`, and a restored snapshot do. ##### `loop` ```nupp loop: boolean ``` Caller-writable. Controls whether the tag restarts after its last frame. ##### `playing` ```nupp playing: boolean ``` Caller-writable. Controls whether time advances. ##### `crossed` ```nupp crossed: integer ``` Engine-owned. Retains the serialized component layout; GPU playback leaves this legacy event scratch field at zero. Ordinary game code should observe `Completed` and `Looped` instead. The reporting system clears it, and a snapshot drops it. ### `Completed` _record_ ```nupp record Completed entity: integer sheet: sheet.Sheet tag: string end ``` `@derive(events.Event)` `@event(name="Completed")` #### Fields ##### `entity` ```nupp entity: integer ``` Read-only. Identifies the entity whose animation ended a cycle. ##### `sheet` ```nupp sheet: sheet.Sheet ``` Read-only. Reports the sheet that was playing. ##### `tag` ```nupp tag: string ``` Read-only. Reports the tag that ended, or the empty string for a whole sheet. ### `Looped` _record_ ```nupp record Looped entity: integer sheet: sheet.Sheet tag: string end ``` `@derive(events.Event)` `@event(name="Looped")` #### Fields ##### `entity` ```nupp entity: integer ``` Read-only. Identifies the entity whose animation ended a cycle. ##### `sheet` ```nupp sheet: sheet.Sheet ``` Read-only. Reports the sheet that was playing. ##### `tag` ```nupp tag: string ``` Read-only. Reports the tag that ended, or the empty string for a whole sheet. ### `PlayOptions` _type_ ```nupp type PlayOptions = { speed: number?, loop: boolean?, playing: boolean? } ``` Configures how an animation plays. `speed` defaults to one, and `loop` and `playing` both default to true. ## Functions ### `frameOf` _function_ ```nupp function frameOf(borrows world: ecs.World, entity: integer): integer ``` Returns exactly the sheet frame selected by the shared GPU table. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the entity's world | | `entity` | `integer` | a live entity | #### Returns | Type | Description | | --- | --- | | `integer` | the one-based sheet frame, or zero without a known sheet | ### `of` _function_ ```nupp function of(source: sheet.Sheet, tag: string?, options: PlayOptions?): components.FFIInstance> ``` Creates an animation that plays a named sheet tag, ready to spawn. Omit the tag to play the whole sheet in order. #### Arguments | Name | Type | Description | | --- | --- | --- | | `source` | `sheet.Sheet` | the sheet to play | | `tag` | `string?` | a tag the sheet names, or nil for the whole sheet | | `options` | `PlayOptions?` | the speed, loop, and playing overrides, or nil for the sheet's own timing, looping, and playing | #### Returns | Type | Description | | --- | --- | | `components.FFIInstance\\>` | a component value ready for `world:spawn` or `world:set`, not an entity | #### Raises - when the sheet does not carry the named tag, because an author naming a tag here has one in mind and the sheet is already in hand to check ### `play` _function_ ```nupp function play(exclusive world: ecs.World, entity: integer, source: sheet.Sheet, tag: string?, options: PlayOptions?): nil ``` Points a live entity at a tag and restarts it there. Restarting is the point: the time and the frame both reset, so the next fixed step writes the tag's first frame whatever the entity was showing. An entity carrying no `Animation` is given one. #### Arguments | Name | Type | Description | | --- | --- | --- | | `exclusive world` | `ecs.World` | the world the entity lives in | | `entity` | `integer` | a live entity carrying a [`Sprite`](tecs.gfx.Sprite) | | `source` | `sheet.Sheet` | the sheet to play | | `tag` | `string?` | a tag the sheet names, or nil for the whole sheet | | `options` | `PlayOptions?` | the speed, loop, and playing overrides, or nil for the defaults | #### Returns | Type | Description | | --- | --- | | `nil` | | #### Raises - when the sheet does not carry the named tag ### `plugin` _function_ ```nupp function plugin(exclusive world: ecs.World): nil ``` Installs dirty-gated GPU playback and events for AnimationEvents entities. One-shots clamp on the GPU without a CPU visit; add AnimationEvents when game code needs Completed or the playing flag cleared. Installing twice is harmless. Snapshots save live phases and restore against a fresh clock. #### Arguments | Name | Type | Description | | --- | --- | --- | | `exclusive world` | `ecs.World` | the world receiving the animation systems | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `restart` _function_ ```nupp function restart(exclusive world: ecs.World, entity: integer): boolean ``` Plays an entity's animation again from the start of its tag. The sheet, tag, speed, and loop flag are left alone; what resets is where in the cycle playback has got to and whether it is running. For replaying a one-shot that has finished, and for rewinding one that has not. #### Arguments | Name | Type | Description | | --- | --- | --- | | `exclusive world` | `ecs.World` | the world the entity lives in | | `entity` | `integer` | a live entity | #### Returns | Type | Description | | --- | --- | | `boolean` | whether there was an animation to restart; false leaves the entity untouched, since nothing there says what it would play | ### `timeOf` _function_ ```nupp function timeOf(borrows world: ecs.World, entity: integer): number ``` Returns elapsed seconds within an entity's animation cycle. The answer is derived from its start and the fixed clock; no component needs advancing. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the entity's world | | `entity` | `integer` | a live entity | #### Returns | Type | Description | | --- | --- | | `number` | cycle seconds, or zero without an animation | ## Values ### `AnimationComponent` _variable_ ```nupp const AnimationComponent: ecs.ComponentDefinition ``` The process-wide `Animation` component definition. ### `AnimationEvents` _variable_ ```nupp const AnimationEvents: ecs.Component ``` ### `DEFAULT_SPEED` _variable_ ```nupp const DEFAULT_SPEED: number ``` The rate an animation runs at when nothing says otherwise, which is the timing the sheet's frames were authored with.