# Animation

SLT animations are standalone structs, not Context methods. Each animation computes an `f64` value from tick counts. You pass the computed value to style or layout methods yourself.

```rust
let mut fade = Tween::new(0.0, 1.0, 30).easing(ease_out_quad);
fade.reset(ui.tick());

// Each frame: compute the value, use it however you want
let opacity = fade.value(ui.tick());
ui.text("Hello").fg(Color::Rgb(255, 255, (255.0 * opacity) as u8));
```

## Tick-based model

Animations run on frame ticks, not wall-clock time.

- `ui.tick()` returns a `u64` frame counter that increments every render frame.
- `RunConfig::default().tick_rate(Duration::from_millis(16))` controls the polling interval (default 16ms / ~60fps).
- All animation types use `reset(tick)` to set a start time and `value(tick)` to sample.
- Lower `tick_rate` = smoother animations but more CPU usage.

## Animation types

### Tween

Linear interpolation from A to B over a fixed number of ticks.

```rust
use slt::{Tween, Context, Color};
use slt::anim::ease_out_quad;

let mut tween = Tween::new(0.0, 100.0, 60)
    .easing(ease_out_quad);

slt::run(|ui: &mut Context| {
    if ui.tick() == 0 {
        tween.reset(ui.tick());
    }
    let x = tween.value(ui.tick()) as u16;
    ui.text("sliding").ml(x);
});
```

Key methods:
- `Tween::new(from, to, duration_ticks)` — constructor, linear easing by default
- `.easing(fn)` — set easing function (builder)
- `.on_complete(fn)` — callback when done (builder)
- `.reset(tick)` — start/restart the tween
- `.value(tick) -> f64` — sample current value
- `.is_done() -> bool` — completion check

### Spring

Physics-based damped harmonic oscillator. Unlike Tween, Spring has no fixed duration -- it settles naturally based on stiffness and damping.

```rust
use slt::{Spring, Context};

let mut spring = Spring::new(0.0, 0.2, 0.85);

slt::run(|ui: &mut Context| {
    let hovered = ui.button("Hover me").hovered;
    spring.set_target(if hovered { 10.0 } else { 0.0 });
    spring.tick();

    let offset = spring.value() as u16;
    ui.text("bouncy").ml(offset);
});
```

Key methods:
- `Spring::new(initial, stiffness, damping)` — constructor
  - `stiffness`: acceleration per unit displacement (`0.1`..`0.5`)
  - `damping`: per-tick velocity multiplier, must satisfy `0.0 < damping < 1.0` (`0.8`..`0.95`). Both bounds are enforced via `debug_assert!` in `Spring::new` (v0.19.1); release builds do not panic but values outside this range conserve or amplify energy and never settle.
- `.on_settle(fn)` — callback when settled (builder)
- `.set_target(value)` — change the goal position (interactive use)
- `.tick()` — advance simulation by one frame (call once per frame)
- `.value() -> f64` — current position
- `.is_settled() -> bool` — true when velocity and distance are both < 0.01

Spring does not use `reset(tick)`. Call `.tick()` every frame and `.set_target()` to change direction.

> **Damping note**: This `damping` is *not* the standard ODE damping ratio ζ — it is a velocity multiplier applied each tick (`velocity *= damping` after the spring force). A value of `1.0` would conserve energy (eternal oscillation); `> 1.0` would amplify it. The recommended `0.80..=0.95` range covers fast-settle to slow-bouncy UI feel.

### Keyframes

Multi-stop timeline animation, like CSS `@keyframes`. Each segment between stops can use its own easing.

```rust
use slt::anim::{Keyframes, LoopMode, ease_out_quad, ease_in_cubic};

let mut kf = Keyframes::new(90)
    .stop(0.0, 0.0)       // start at 0
    .stop(0.3, 100.0)     // ramp up to 100 at 30%
    .stop(0.7, 100.0)     // hold at 100 until 70%
    .stop(1.0, 40.0)      // ease down to 40
    .segment_easing(0, ease_out_quad)
    .segment_easing(2, ease_in_cubic)
    .loop_mode(LoopMode::PingPong);

kf.reset(ui.tick());
let brightness = kf.value(ui.tick());
```

Key methods:
- `Keyframes::new(duration_ticks)` — constructor
- `.stop(position, value)` — add a stop at normalized position `[0.0, 1.0]`. Stops are kept sorted by `position` after every call, so the order in which you append them does not matter — a `.stop(0.7, 100.0)` after `.stop(1.0, 40.0)` lands in the right slot. Code that relied on insertion order to identify segments will see the sorted-by-time order instead.
- `.easing(fn)` — default easing for all segments
- `.segment_easing(index, fn)` — override easing for segment `index` (0 = first-to-second stop). Out-of-range indices are silently ignored in release builds (preserving the panic-free guarantee for runtime code) and trigger a `debug_assert!` panic in debug builds (v0.19.1) so builder-order mistakes — calling `segment_easing(2, ...)` before three stops have been added — surface during development.
- `.loop_mode(mode)` — set loop behavior
- `.on_complete(fn)` — callback when done
- `.reset(tick)` — start/restart
- `.value(tick) -> f64` — sample current value
- `.is_done() -> bool` — completion check (always false for looping modes)

### Sequence

Chain multiple tween segments end-to-end into a single timeline.

```rust
use slt::anim::{Sequence, LoopMode, ease_linear, ease_out_quad, ease_in_cubic};

let mut seq = Sequence::new()
    .then(0.0, 100.0, 30, ease_out_quad)   // slide right
    .then(100.0, 100.0, 10, ease_linear)   // pause
    .then(100.0, 0.0, 20, ease_in_cubic)   // slide back
    .loop_mode(LoopMode::Repeat);

seq.reset(ui.tick());
let x = seq.value(ui.tick());
```

Key methods:
- `Sequence::new()` — constructor
- `.then(from, to, duration_ticks, easing)` — append a segment
- `.loop_mode(mode)` — set loop behavior
- `.on_complete(fn)` — callback when done
- `.reset(tick)` — start/restart
- `.value(tick) -> f64` — sample current value
- `.is_done() -> bool` — completion check

### Stagger

Apply the same tween to N items with a fixed delay between each start.

```rust
use slt::anim::{Stagger, LoopMode, ease_out_quad};

let items = vec!["Alpha", "Beta", "Gamma", "Delta"];
let mut stagger = Stagger::new(0.0, 1.0, 20)
    .easing(ease_out_quad)
    .delay(5)
    .items(items.len())
    .loop_mode(LoopMode::Once);

stagger.reset(ui.tick());

for (i, label) in items.iter().enumerate() {
    let opacity = stagger.value(ui.tick(), i);
    let gray = (255.0 * opacity) as u8;
    ui.text(*label).fg(Color::Rgb(gray, gray, gray));
}
```

Key methods:
- `Stagger::new(from, to, duration_ticks)` — constructor
- `.easing(fn)` — easing for each item's tween
- `.delay(ticks)` — ticks between consecutive item starts
- `.items(count)` — set item count (inferred from usage if not set)
- `.loop_mode(mode)` — set loop behavior
- `.on_complete(fn)` — callback when done
- `.reset(tick)` — start/restart
- `.value(tick, item_index) -> f64` — sample value for a specific item
- `.is_done() -> bool` — true if the most recently sampled item finished
- `.is_all_done(tick, item_count) -> bool` — true once **every** item has finished, computed from pure tick arithmetic (v0.19.1). Use this when you need a single completion signal that does not depend on which item happened to be sampled last. With `LoopMode::Repeat` / `PingPong` it only reports `true` for the first cycle (loops re-enter after completion).

## Easing functions

All easing functions have signature `fn(f64) -> f64`, mapping `[0, 1]` to `[0, 1]`.

| Function | Curve | Use case |
|----------|-------|----------|
| `ease_linear` | Constant rate | Default, mechanical motion |
| `ease_in_quad` | Slow start | Accelerating elements |
| `ease_out_quad` | Slow end | Decelerating, natural stops |
| `ease_in_out_quad` | Slow both ends | Smooth transitions |
| `ease_in_cubic` | Slower start | Stronger acceleration |
| `ease_out_cubic` | Slower end | Stronger deceleration |
| `ease_in_out_cubic` | Slower both ends | Emphasis on middle speed |
| `ease_out_elastic` | Overshoot + oscillate | Attention-grabbing, playful |
| `ease_out_bounce` | Bouncing ball | Landing, drop effects |

Helper: `lerp(a, b, t)` — linear interpolation, not clamped. Apply easing to `t` before calling.

```rust
use slt::anim::{lerp, ease_out_quad};

let t = ease_out_quad(0.5);
let value = lerp(0.0, 100.0, t); // ~75.0
```

## LoopMode

Controls what happens when an animation reaches its end.

| Mode | Behavior |
|------|----------|
| `LoopMode::Once` | Play once, hold at final value. `is_done()` returns `true`. |
| `LoopMode::Repeat` | Restart from the beginning each cycle. |
| `LoopMode::PingPong` | Alternate forward and backward each cycle. |

Applies to `Keyframes`, `Sequence`, and `Stagger`. `Tween` always plays once (use `Sequence` with `LoopMode::Repeat` for looping tweens). `Spring` has no loop mode -- it settles naturally.

## Common patterns

### Fade-in on mount

```rust
let mut fade = Tween::new(0.0, 1.0, 30).easing(ease_out_quad);
let mut started = false;

slt::run(|ui: &mut Context| {
    if !started {
        fade.reset(ui.tick());
        started = true;
    }
    let alpha = fade.value(ui.tick());
    let g = (255.0 * alpha) as u8;
    ui.text("Welcome").fg(Color::Rgb(g, g, g));
});
```

### Button hover spring

```rust
let mut spring = Spring::new(0.0, 0.3, 0.8);

slt::run(|ui: &mut Context| {
    let btn = ui.button("Click me");
    spring.set_target(if btn.hovered { 2.0 } else { 0.0 });
    spring.tick();

    let pad = spring.value() as u16;
    ui.text(">>").ml(pad);
});
```

### Staggered list entry

```rust
let items = vec!["One", "Two", "Three", "Four", "Five"];
let mut stagger = Stagger::new(0.0, 1.0, 15)
    .easing(ease_out_cubic)
    .delay(3)
    .items(items.len());

let mut started = false;

slt::run(|ui: &mut Context| {
    if !started {
        stagger.reset(ui.tick());
        started = true;
    }
    for (i, item) in items.iter().enumerate() {
        let t = stagger.value(ui.tick(), i);
        let brightness = (255.0 * t) as u8;
        ui.text(*item).fg(Color::Rgb(brightness, brightness, brightness));
    }
});
```

### Loading sequence (multi-phase)

```rust
let mut loading = Keyframes::new(120)
    .stop(0.0, 0.0)     // idle
    .stop(0.25, 100.0)  // fill bar
    .stop(0.5, 100.0)   // hold
    .stop(0.75, 0.0)    // reset
    .stop(1.0, 0.0)     // idle
    .easing(ease_in_out_quad)
    .loop_mode(LoopMode::Repeat);

loading.reset(ui.tick());
let pct = loading.value(ui.tick());
ui.text(format!("[{:>3.0}%]", pct));
```

### Chained transitions

```rust
let mut chain = Sequence::new()
    .then(0.0, 50.0, 20, ease_out_quad)    // move to center
    .then(50.0, 50.0, 30, ease_linear)     // hold
    .then(50.0, 100.0, 20, ease_in_cubic); // move to end

chain.reset(ui.tick());
let pos = chain.value(ui.tick()) as u16;
ui.text("->").ml(pos);
```

## API quick reference

### Tween

| | |
|---|---|
| Constructor | `Tween::new(from, to, duration_ticks)` |
| Builder | `.easing(fn)`, `.on_complete(fn)` |
| Control | `.reset(tick)` |
| Sample | `.value(tick) -> f64` |
| Done | `.is_done() -> bool` |

### Spring

| | |
|---|---|
| Constructor | `Spring::new(initial, stiffness, damping)` |
| Builder | `.on_settle(fn)` |
| Control | `.set_target(value)`, `.tick()` |
| Sample | `.value() -> f64` |
| Done | `.is_settled() -> bool` |

### Keyframes

| | |
|---|---|
| Constructor | `Keyframes::new(duration_ticks)` |
| Builder | `.stop(pos, val)`, `.easing(fn)`, `.segment_easing(idx, fn)`, `.loop_mode(mode)`, `.on_complete(fn)` |
| Control | `.reset(tick)` |
| Sample | `.value(tick) -> f64` |
| Done | `.is_done() -> bool` |

### Sequence

| | |
|---|---|
| Constructor | `Sequence::new()` |
| Builder | `.then(from, to, ticks, easing)`, `.loop_mode(mode)`, `.on_complete(fn)` |
| Control | `.reset(tick)` |
| Sample | `.value(tick) -> f64` |
| Done | `.is_done() -> bool` |

### Stagger

| | |
|---|---|
| Constructor | `Stagger::new(from, to, duration_ticks)` |
| Builder | `.easing(fn)`, `.delay(ticks)`, `.items(count)`, `.loop_mode(mode)`, `.on_complete(fn)` |
| Control | `.reset(tick)` |
| Sample | `.value(tick, item_index) -> f64` |
| Done | `.is_done() -> bool` (last-sampled item), `.is_all_done(tick, item_count) -> bool` (every item) |

## Related docs

- [PATTERNS.md](PATTERNS.md) -- common composition patterns
- [WIDGETS.md](WIDGETS.md) -- widget catalog and usage
