spanda 0.9.0

A general-purpose animation library for Rust — tweening, keyframes, timelines, and physics.
Documentation
# v0.8.0 Features

> DrawSVG, MorphPath, Inertia, DragState, Advanced Easings, and WASM-DOM Plugins

This page documents the features added in spanda 0.8.0. For the advanced parametric easings, see [easing.md](easing.md). For the WASM-DOM plugins (FLIP, SplitText, ScrollSmoother, Draggable, Observer), see [integrations.md](integrations.md).

---

## DrawSVG

Thin convenience helpers for the classic SVG stroke-dashoffset draw-on/draw-off effect. These return a `TweenBuilder<f32>` so you chain `.duration()`, `.easing()`, `.build()` as usual.

### API

| Function | Returns | Effect |
|----------|---------|--------|
| `draw_on(path_length)` | `TweenBuilder<f32>` | Tweens from `path_length` to `0.0` (draws the stroke on) |
| `draw_on_reverse(path_length)` | `TweenBuilder<f32>` | Tweens from `0.0` to `path_length` (erases the stroke) |

### Example

```rust
use spanda::svg_draw::{draw_on, draw_on_reverse};
use spanda::easing::Easing;
use spanda::traits::Update;

// Draw on a path with total length 300px
let mut tween = draw_on(300.0)
    .duration(1.5)
    .easing(Easing::EaseInOutCubic)
    .build();

// Each frame:
tween.update(dt);
let dash_offset = tween.value();
// Apply: element.style.strokeDashoffset = dash_offset

// Reverse: erase the stroke
let mut erase = draw_on_reverse(300.0)
    .duration(0.8)
    .easing(Easing::EaseInCubic)
    .build();
```

### How It Works

Set `stroke-dasharray` to the total path length, then animate `stroke-dashoffset` from `path_length` (fully hidden) to `0.0` (fully visible). This is the same technique used by CSS `stroke-dashoffset` animations and GSAP's DrawSVGPlugin.

---

## MorphPath

Point-by-point shape morphing with automatic resampling. Interpolates between two sets of 2D points, creating smooth shape transitions.

### API

| Method | Description |
|--------|-------------|
| `MorphPath::new(from, to)` | Create a `MorphPathBuilder`. Auto-resamples if point counts differ. |
| `.duration(seconds)` | Set morph duration (default: 1.0) |
| `.easing(easing)` | Set easing curve (default: Linear) |
| `.build()` | Build the `MorphPath` |
| `.value()` | Current interpolated `Vec<[f32; 2]>` |
| `.progress()` | Raw progress 0.0..=1.0 |
| `.is_complete()` | Whether the morph has completed |
| `.seek(t)` | Jump to a specific progress |
| `.reset()` | Reset to beginning |
| `resample(points, target_count)` | Utility: resample a polyline to evenly-spaced points |

### Example

```rust
use spanda::morph::{MorphPath, resample};
use spanda::easing::Easing;
use spanda::traits::Update;

// Triangle -> Square morph
let triangle = vec![[0.0, 0.0], [50.0, 100.0], [100.0, 0.0]];
let square = vec![[0.0, 0.0], [0.0, 100.0], [100.0, 100.0], [100.0, 0.0]];

// Point counts differ — the builder auto-resamples the shorter shape
let mut morph = MorphPath::new(triangle, square)
    .duration(1.0)
    .easing(Easing::EaseInOutCubic)
    .build();

// Each frame:
morph.update(dt);
let current_shape: Vec<[f32; 2]> = morph.value();
// Render current_shape as a polygon
```

### Resampling

When the source and target shapes have different point counts, the builder automatically resamples the shorter one using arc-length parameterisation. You can also resample manually:

```rust
use spanda::morph::resample;

let rough = vec![[0.0, 0.0], [100.0, 50.0], [200.0, 0.0]];
let smooth = resample(&rough, 20); // 20 evenly-spaced points along the polyline
```

---

## Inertia

Friction-based deceleration physics. An object coasts to a stop under exponential velocity decay — no target position, just gradual deceleration. Think scroll momentum or fling gestures.

### InertiaConfig

| Preset | Friction | Character |
|--------|----------|-----------|
| `InertiaConfig::default_flick()` | 0.05 | Moderate deceleration — general-purpose |
| `InertiaConfig::heavy()` | 0.02 | Slow deceleration, long coast |
| `InertiaConfig::snappy()` | 0.15 | Fast deceleration, quick stop |

### Inertia (1D)

Single-axis friction deceleration:

```rust
use spanda::inertia::{Inertia, InertiaConfig};
use spanda::traits::Update;

let mut inertia = Inertia::new(InertiaConfig::default_flick())
    .with_velocity(500.0)   // pixels per second
    .with_position(100.0);  // starting position

// Each frame:
while !inertia.is_settled() {
    inertia.update(1.0 / 60.0);
    let pos = inertia.position();
    let vel = inertia.velocity();
    // render at pos
}
```

### InertiaN (Multi-Dimensional)

For 2D/3D fling gestures, `InertiaN<T>` uses the `SpringAnimatable` trait:

```rust
use spanda::inertia::{InertiaN, InertiaConfig};
use spanda::traits::Update;

let mut inertia = InertiaN::new(InertiaConfig::default_flick(), [100.0_f32, 200.0])
    .with_velocity([300.0, -150.0]);

inertia.update(1.0 / 60.0);
let pos: [f32; 2] = inertia.position();
// pos has moved in the direction of the initial velocity
```

### Physics

Velocity decays via frame-rate independent exponential formula:

```
velocity *= (1.0 - friction).powf(dt * 60.0)
```

This ensures identical behaviour whether running at 30fps, 60fps, or 144fps. The animation settles when velocity drops below `epsilon` (default: 0.1).

### Kick

Re-apply velocity to a settled inertia (e.g., a second fling gesture):

```rust
inertia.kick(800.0); // new velocity impulse, restarts settling
```

---

## DragState

Pure-math pointer drag tracker. No DOM dependency — works everywhere. Tracks position, computes smoothed velocity via exponential moving average, and applies constraints.

### API

| Method | Description |
|--------|-------------|
| `DragState::new()` | Create at origin, no constraints |
| `.with_position(pos)` | Builder: set initial position |
| `.with_constraints(c)` | Builder: set constraints |
| `.on_pointer_down(x, y)` | Start drag from pointer position |
| `.on_pointer_move(x, y, dt)` | Move during drag (provide dt for velocity) |
| `.on_pointer_up()` | End drag, returns `InertiaN<[f32; 2]>` for momentum |
| `.position()` | Current `[f32; 2]` position |
| `.velocity()` | Smoothed `[f32; 2]` velocity |
| `.is_dragging()` | Whether the pointer is held |

### DragConstraints

| Field | Type | Description |
|-------|------|-------------|
| `bounds` | `Option<[f32; 4]>` | Bounding rect: `[min_x, min_y, max_x, max_y]` |
| `axis_lock` | `Option<DragAxis>` | Lock to `DragAxis::X` or `DragAxis::Y` |
| `snap_to_grid` | `Option<[f32; 2]>` | Snap position to grid: `[grid_x, grid_y]` |

### Example

```rust
use spanda::drag::{DragState, DragConstraints, DragAxis};
use spanda::traits::Update;

let mut drag = DragState::new()
    .with_position([100.0, 100.0])
    .with_constraints(DragConstraints {
        bounds: Some([0.0, 0.0, 500.0, 500.0]),
        axis_lock: None,
        snap_to_grid: Some([20.0, 20.0]),
        ..Default::default()
    });

// On pointer down:
drag.on_pointer_down(150.0, 150.0);

// On pointer move (each frame):
drag.on_pointer_move(170.0, 160.0, 1.0 / 60.0);
let pos = drag.position(); // snapped to [160.0, 160.0]

// On pointer up — get momentum for fling:
let mut inertia = drag.on_pointer_up();

// The inertia carries the drag's velocity:
inertia.update(1.0 / 60.0);
let fling_pos = inertia.position();
```

### Velocity Smoothing

Velocity is tracked using an exponential moving average (EMA):

```
velocity = 0.8 * instantaneous_velocity + 0.2 * previous_velocity
```

This prevents velocity spikes from noisy pointer events while still being responsive.

### DOM Binding

For web apps, use `Draggable` (requires `feature = "wasm-dom"`) which wraps `DragState` with DOM pointer event listeners. See [integrations.md](integrations.md#draggable).

---

## Advanced Easings

Five new parametric easing variants added in v0.8.0:

| Variant | Character |
|---------|-----------|
| `RoughEase { strength, points, seed }` | Deterministic noise overlay — hand-drawn feel |
| `SlowMo { ratio, power, yoyo_mode }` | Slow-fast-slow — cinematic movements |
| `ExpoScale { start_scale, end_scale }` | Perceptual exponential correction — zoom animations |
| `Wiggle { frequency, amplitude }` | Sinusoidal oscillation — vibration / shake |
| `CustomBounce { strength, squash }` | Parametric bounce with decay and squash |

See the full [Easing documentation](easing.md#advanced-parametric-easings) for parameters, examples, and usage recommendations.

---

## WASM-DOM Plugins

Five DOM interaction plugins requiring `feature = "wasm-dom"`:

| Plugin | Module | Description |
|--------|--------|-------------|
| Observer | `integrations::observer` | Unified pointer/touch/mouse event normaliser |
| FLIP | `integrations::flip` | First-Last-Invert-Play layout animations |
| SplitText | `integrations::split_text` | Character/word splitting + staggered timelines |
| ScrollSmoother | `integrations::scroll_smoother` | Spring-driven smooth scroll |
| Draggable | `integrations::draggable` | DOM pointer binding for DragState |

See the full [Integrations Guide](integrations.md#wasm-dom-plugins) for API details and examples.