# motion — animation runtime `EngineMotion` is the runtime engine for the UIX motion system: it registers motion **presets** and **runs** them. It is an art (a pure runtime artifact, public methods over private state, no reactive `$state`) so that **both** UIX layers can consume it via `uix.motion` without a cross-layer dependency: - **soma** (`Presence`) calls `motion.run(node, phase)` to run + await a JS preset (a `spring`) before unmounting — the gating soma can't get from `getAnimations()` alone. - **eidos** generates the CSS (keyframes + preset rules), registers its visual presets into the engine, and drives wrappers. The engine owns the **execution**; eidos owns the **CSS generation** and the visual presets **data**. Neither imports the other — they meet at `uix.motion`. ## The two moments Motion occurs in two moments, each animable (one, the other, or both): | | `--event` | `--state` | |---|---|---| | Attr | `data-event-*` (sema) | `data-state` (soma) | | What | the perceptual **firma** during a signal's hold | the transition to/from a persistent condition | | Registry | `signatures` (eidos generates CSS) | `presets` (named, per-component) | `EngineMotion` runs the **`--state`** presets (CSS → settled; JS → driver). The `--event` firma is CSS that eidos generates from `signatures`; the engine does not run it. ## API ```ts const motion = createEngineMotion({ dom }) // dom: a MotionDom port motion.register('scale-fade', { driver: 'css', … }) // eidos registers presets motion.register('pop', { driver: 'spring', enter: spring({ … }) }) motion.run(node, 'enter') // resolve node's data-animation-style + run motion.enter(el, 'pop') // run a preset by name motion.cancel(el) // cancel active JS motion on el await motion.pending(el) // combined finished, for Presence motion.dispose() // idempotent: cancel all + clear registry ``` - **CSS preset** → declarative: a settled handle (the generated CSS + soma's `Presence` via `getAnimations()` do the work). - **JS preset** → runs its `MotionRun` driver, normalises the result (`Animation` | `Animation[]` | `MotionHandle`) to one handle, tracks it per element. `pending(el)` is what Presence awaits for drivers (`spring`) that `getAnimations()` can't see. ## Drivers (`./drivers`) `MotionRun` factories for JS presets: - **`spring`** — self-contained physics: an independent semi-implicit-Euler spring per property, stepped via `ctx.dom.requestFrame`. Overshoot + settle — the curve no cubic-bezier expresses. Cancellable via `ctx.signal`. - **`waapi`** — `el.animate(...)`. The `Animation` is in `getAnimations()`, so Presence awaits it for free. - **`rect`** — FLIP: measures first/last rects, animates the inverse delta via WAAPI (layout / shared-element transitions). ## The DOM port This art imports no other art. The DOM dependency arrives **injected** and is typed by the structural `MotionDom` port (`requestFrame` / `cancelFrame` / `prefersReducedMotion`); the real `ActiveDom` satisfies it. ## Composition ```ts // active-app (attach path) const App = createActiveApp({ services: { dom: defineActiveDom(), motion: defineEngineMotion() } }); // active-uix (standalone) creates it directly and exposes uix.motion. ``` Without a `dom` service the engine degrades (JS drivers settle; CSS is declarative regardless). `duration` / `ease` are `string` (token keys or raw values); eidos validates the keys — this art stays token-agnostic.