# `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.