# Motion Kit

Hands-on walkthrough: [Tutorials — First motion](/docs/tutorials/first-motion.md).

Shared motion system for **runtime HUD**, **editor chrome**, and general gameplay values. Implementation lives under `src/hikari/sdk/src/motion/`. Exports: `hi.motion` (full kit) plus root alias `hi.Tween` (and `hi.easing` for the easing module). Other types are `hi.motion.Spring` / `Driver` / `Motions` / `Transition` / `Transaction` / `Preset` / `Easing`.

## Goal

Modern UI motion (tween, spring, enter/exit, stagger, interruptible retarget, reduced motion) under hot-path constraints: plain values, no per-frame heap, no dual APIs for game vs editor, one draw path unchanged.

## Non-goals

- Skeletal / clip animation (Shinra / model pipeline).
- GPU particle systems or shader timeline graphs.
- Auto-magic layout morphing without caller-stored previous geometry.
- Per-widget heap timelines, string-keyed animators, or callback graphs on the hot path.
- A second compositor / transform stack. Motion outputs **numbers**; UI paints them.

## Where it sits

```text
gameplay / GameSubsystem / entity data
editor host (spinners, panel open, toast)
        │
        ▼
   motion.*   (easing, tween, spring, driver, presets, Motions bus)
        │
        ├─► world.ui styles / colors / Length / progress   (immediate)
        └─► RetainedUi setStyle / setIconRotation / alpha   (retained)
                    │
                    ▼
              ui.Compositor → one UI draw
```

| Layer | Owns | Must not own |
|-------|------|--------------|
| `motion/` | Curves, drivers, springs, presets, Motions bus | Widgets, documents, GPU |
| Immediate `ui/` | Rebuild + sample caller-owned drivers into styles | A second tween registry |
| Retained `editor/ui/` | Dirty invalidation when driven values change | Duplicate easing math |

## Core model

### Curves (`easing.zig`)

`Easing` + `apply(t)`, plus `lerp`, `clamp01`, `repeat`, `pingpong`, `wave`, `spin` / `spinMs`.

### Drivers

Plain structs — store next to animated state. Poll `done()`; no hot-path closures.

| Type | Role |
|------|------|
| `Tween` / `TweenVec2` / `TweenVec3` / `TweenColor` | Duration + curve; delay, plays, yoyo |
| `Spring` / `SpringVec2` | Interruptible; `setTarget` keeps velocity |
| `Driver` | Union of tween / spring / hold |
| `Hold` (via `Driver.hold`) | Constant |
| `Transaction` | Interruptible from/to presentation (`begin` / `retarget` / `update`) |

`Driver.make(from, to, Spec)` builds from a `Spec` (`.tween`, `.spring`, or `.snap`).

### Spring

SwiftUI-style `(response, damping_fraction)` → internal stiffness/damping. Settles when position and velocity are within eps, then snaps.

`Spring.update` clamps a delayed frame to a bounded elapsed interval and integrates it in at most 1/60-second steps. This keeps the semi-implicit solver stable after debugger pauses, blocking native work, or idle-frame gaps instead of flinging UI surfaces past their authored travel.

### Presets (`Preset`)

`snappy` / `gentle` / `bouncy` springs; `fade_fast` / `fade_panel` / `slide_panel` / `pop` tweens. Themes do not redefine physics.

### Transitions

`Transition` recipes (`fade_in`, `panel_in`, `pop_in`, …) seed a `TransitionState`. Caller owns show/hide lifecycle; sample opacity/offset/scale each frame.

Stagger: `tweenSpecStagger(base, index, slot)` or `staggerDelay`.

### Motions bus

Dense `Motions` table keyed by `u64` + `Channel` (no string IDs). `drive` / `retarget` / `tick` / `get` / `active`. Respects `setReduceMotion`. Use `Motions.keyWidget(index, gen)` for retained `WidgetId`s.

Immediate UI usually skips the bus — store `Spring`/`Tween` on entity/subsystem state.

### FLIP

`motion.flip.invert(prev_origin, curr_origin)` — caller stores previous rect.

### Reduced motion

`ui.AccessibilityPrefs.reduce_motion` (editor Settings + `prefs.json`). `motion.resolve(spec, reduce_motion)` → `.snap`. Exposed on `UiContext.reduce_motion` / `RetainedUi.reduce_motion` after `applyAccessibility`. Editor spinners freeze when set.

## Integration

### Immediate UI

```zig
self.panel_x.setTarget(open_x); // Spring
const x = self.panel_x.update(ctx.dt);
// Host code: world.ui.vStack(...); game modules:
const panel = ui.vStack(.{ .position = .{ x, 16 } });
defer ui.end(panel);
```

Do not put drivers inside `UiContext`.

### Retained

Mutate properties (`setIconRotation`, `setOpacity`, `setShift`) + invalidate. Spinners use `motion.spinMs(now_ms, period)` unless `reduce_motion`.

Editor overlays use `editor/ui/overlay_motion.zig` (`OverlayMotion`): settings / confirm / asset picker run `panel_in`/`panel_out`; compile splash uses fade; status/recompile loaders pop opacity with `Preset.fade_fast`.

### Frame timing

Drive from sim/UI `dt`. Wall-clock only via `spin` / `spinMs`. `Motions.active()` is a pacing hint for hosts.

## Performance contract

1. No heap on `update` / `tick` / `lerpColor`. Reserve `Motions` capacity up front.
2. Settled drivers are free (`tick` early-outs; `active_count`).
3. One curve table — controls never copy easing formulas.
4. Compositor unchanged: one atlas, one stream, one UI draw.
5. No string IDs in the bus.
6. Color RGB lerps in linear light; alpha straight (`lerpColor` / `withOpacity`).

## Package layout

```text
src/hikari/sdk/src/motion/
  easing.zig  tween.zig  spring.zig  driver.zig
  color.zig   preset.zig transition.zig  reduce.zig
  bus.zig     flip.zig   transaction.zig  motion.zig
```

## Forbidden patterns

- Second copy of ease-out-cubic in a control file.
- Growth inside `update` / `tick`.
- Putting `Motions` inside `UiContext` frame state.
- Driving motion from the render thread.
- Default-on animation for every stock control — opt-in at the host.

## See also

- [UI and editor](/docs/systems/ui-and-editor.md)
- [User interface](/docs/guides/user-interface.md)
- [Application lifecycle](/docs/systems/lifecycle.md)
- [Scenes and gameplay](/docs/systems/scenes-and-gameplay.md)
