# Engine selection and portable recipes

[Guide](guide.md) · [Patterns](patterns.md) · [Authoring](authoring.md) · [Verification](verification.md) · [Capability catalog](capability-catalog.json)

**Choose a mechanism after choosing the primitive.** The recipes below are implementation guidance, not new approved effects or a promise that every feature of each library has been mastered. No vendor runtime is bundled by this chapter. Pin dependencies in the adopting project; record the source, version, license, assets and runtime evidence there.

## Decision ladder

| Need | Start with | Escalate when |
|---|---|---|
| Local state/hover/finite progress | CSS, SVG, WAAPI | A mutable sequence needs coordinated labels and cancellation |
| Persistent semantic reorder | Native FLIP | Nested layout complexity justifies GSAP Flip or an existing framework owner |
| React presence/state | Existing React state; Motion if needed | Exit retention/layout projection is genuinely required |
| Interrupted target following | A bounded scalar; React Spring in an existing React surface | The approved behavior actually needs a spring, not fixed duration |
| Document progress | Native scrolling and CSS timeline enhancement | Authored geometry needs ScrollTrigger measurement |
| Stateful vector asset | SVG/DOM state | Editable vector state machine repays Rive authoring/runtime cost |
| Fixed vector sequence | SVG/WAAPI | An approved export needs a Lottie/dotLottie player |
| Authored numeric tracks | A small controller | Theatre serialized state materially improves authoring |
| Dense 2D display list | Canvas | Measured draw cost justifies Pixi or a specialized GPU path |
| Original material/compute | Static illustration first | Approved design and actual device measurements justify WebGL/WebGPU |
| Collisions/constraints | No physics for a simple transition | The task is genuinely a simulation, with separately reviewed behavior |
| Sound/haptics | Silent, visual/DOM feedback | Explicit opt-in and actual output-device review justify augmentation |

“Escalate” does not waive registry or M13 approval. M15–M18 themselves can remain native. Existing exact-source M1/M8 implementations are not candidates for casual engine ports.

## Native browser toolkit

### CSS and WAAPI: finite presentation owned by one controller

Use CSS for state-derived styling and WAAPI for a cancellable finite job. `Element.animate()` returns an Animation object; keep it rather than losing the handle.[1] For M15:

1. Start with a readable result/status and a valid SVG pose.
2. Validate input; increment a generation counter; cancel the old owned animation.
3. Commit the host's semantic state independently. Start the presentation only if the API and live motion policy permit it.
4. Observe the owned `finished` promise. Treat cancellation as cancellation; report unexpected failures. Ignore stale generations.
5. Write the final style/attribute explicitly, then release the transient animation. On reset/destroy, invalidate the generation before cancellation.

Do not put business success in `onfinish`. Do not apply the controller to all animations on the page; it owns only its targets. Test a real intermediate value, reset midflight and blocked API, not merely a final value assigned by application code.

### Typed properties, SVG and scroll/view timelines

`@property` registers a typed custom property in CSS.[2] For a shared scalar, use an explicit syntax, inheritance policy and initial value; derive geometry from one owner. Registration enables interpolation, not a guarantee of GPU compositing. For M18, prefer direct input-to-state updates; a CSS property is not required if JavaScript owns the same bounded SVG value cleanly.

Use normalized SVG dash geometry for M15; path/mask geometry for M18. Define the viewBox, mask coordinate system and unique IDs; clamp values before rendering. Restrict filter regions. Keep text/control semantics outside masks and renderer surfaces. Inspect actual repaint cost for filters, masks and font changes.

For document progress, declare the visible static state first. Add scroll/view timeline behavior only inside exact support and no-reduced-motion conditions. Put `animation-timeline` **after** the `animation` shorthand, which otherwise resets it.[3] Test two actual scroll positions and reverse movement. Do not turn every entering section into an invisible reveal waiting for intersection.

### View Transitions: presentation around a semantic update

The update callback follows the old-page snapshot.[4] Keep a single state-update function that also runs directly when the API is unavailable or motion is reduced. Assign unique transition names; skip an older transition before a replacement. Handle rejected readiness without losing the state change. Restore/retain focus according to the application task, not the snapshot.

A same-document shared-element experiment is not proof of cross-document routing, history or framework integration. M16 authorizes bounded continuity around actual item order—not a default page-transition spectacle. Match the canonical specimen before selecting this alternative.

## GSAP: mutable authored sequences and plugins

Use a scoped context and owned timeline. Build from a stable semantic DOM; assign timeline labels to meaningful phases; read the applicable token values instead of substituting library-default easing. Kill/revert the owned work on release and restore the intended final state. No global selector/ticker cleanup in a shared application.

The captured standard license permits no-charge commercial use, including formerly members-only plugins, but it is a custom license, not MIT.[7] Its ordinary website/app/interface permission includes companies competing with Webflow in other business areas; the prohibited-use definition concerns competing no-code visual-animation builders.[7][22] Review the complete applicable terms before distributing a builder or repackaging the product. Downloadability is not unrestricted licensing.

| Mechanism | Portable implementation recipe | Ownership/failure trap and required proof |
|---|---|---|
| **GSAP core** | Bind one timeline to one region; choose explicit initial/settled values, labels and token-derived durations; replace the timeline on changed intent | Cancel during a phase and verify no stale callback commits state; do not mutate framework-owned properties from an unrelated timeline |
| **Flip** | Capture `Flip.getState(items)`; reorder persistent DOM items; animate with `Flip.from(state, options)`; clear temporary layout styles at settlement | Keep stable keys/identity, batch layout work, test changed count and resize. Nested absolute reparenting needs separate evidence. Flip is documented for changed DOM states.[8] |
| **ScrollTrigger** | Measure a stable trigger and animate its child; link progress to actual scroll; refresh after fonts/data/layout changes | Avoid animating measurement geometry. Kill owned triggers on teardown. Native progress is not proof of pin/snap, horizontal stories or proxy integration. `refresh()` recalculates geometry.[9] |
| **ScrollTo** | Choose a real destination; cancel the previous scroll; use an explicit nested/document target and user-interruption policy; manage hash/focus as separate semantics | Do not combine with CSS smooth scrolling on that target.[18] Test backward target, interruption, anchors, focus and native scrolling after release |
| **ScrollSmoother** | Use one deliberately chosen wrapper/content pair; wait for startup and fonts; keep fixed UI outside transformed content; provide a native-scroll mode | It transforms content to catch up with native scroll measured through ScrollTrigger.[19] Test sticky/fixed descendants and focus; the lab's frame-contained trial is not outer-document/touch proof |
| **Observer** | Listen in one region; normalize intent into a bounded host action; retain equivalent buttons/keys and an escape path | Observer supplies input deltas, not authoritative document position.[14] Do not infer physical touch coverage from a wheel test or prevent all page scrolling |
| **Draggable** | Give a dedicated handle bounded coordinates; map them to host state; provide equivalent native keyboard controls; refresh bounds on resize | Own the same position only once. Test actual pointer drag, keyboard range and cleanup; inertia coupling is a separate integration, not proved by independent tests |
| **Inertia** | Declare velocity units, resistance and endpoint bounds; feed a measured/explicit velocity into an owned tween; interrupt on new input | The plugin honors starting velocity and optional end restrictions.[15] Numeric-velocity fixtures do not prove pointer tracking. Elastic/inertial brand behavior still needs registry approval |
| **SplitText** | Apply only to suitable plain text or an isolated visual copy; preserve original semantics; measure after fonts; revert on end/cancel/content change | Its default approach does not preserve nested interactive semantics.[16] Test graphemes, wraps, re-splitting and screen-reader behavior; M17 is not permission to split a linked paragraph |
| **MorphSVG** | Use approved source/target geometry, fixed viewBox and explicit mapping; compare start/interior/end `d` and pixels; cancel/revert cleanly | It animates SVG path data.[17] Arbitrary path morphing is not M18: that primitive requires the specified shared-scalar aperture/path relation |

Only the named bounded mechanisms are covered here. DrawSVG, MotionPath, ScrambleText or other plugins appearing in vendor navigation are not thereby practiced or added to the original catalog. Choose the native primitive or perform a separate versioned lookup, review and test before using an additional plugin.

## Motion, Anime.js, React Spring and AutoAnimate

### Motion: React state and presence

Keep keys stable and property ownership inside the React subtree. `AnimatePresence` retains exiting motion descendants until their exit animations complete.[10] Use that retention only when the approved interaction needs it. Move focus before removing focused content; retain the same inspector node when policy changes. Test the exit interval, actual visual change and eventual removal, not just “closed” state.

**Version-specific caution:** the captured reduced-motion documentation says the hook responds live, while the inspected `framer-motion` 13.2.0 hook stores the preference with `useState`.[11][12] The historical lab records the corresponding failed live-update test and uses a native `matchMedia` change subscription via `useSyncExternalStore`. Recheck after upgrading; this is not a timeless claim about Motion. Subscribe even retained exit children to the current policy. Test SSR/hydration and StrictMode separately when the adopting app uses them.

For M16 alternatives, prove DOM order and stable identity as well as layout projection. A passing presence example is not evidence for shared-layout, gesture or router mastery.

### Anime.js: a bounded tween or local sequence

Create one explicit animation of an owned scalar/property. On changed intent, stop/revert or retarget according to the specified interruption contract; update semantic state independently. Map durations/units from the pinned API and tokens. Use `revert()` when the desired cleanup is full stop/destruction, as the documentation specifies.[23] Verify that inline state and callbacks are gone; then render the correct settled state.

The lab exercised an interrupted transform tween in 4.5.0, not every timeline, Scope or WAAPI-backend feature. Do not mix v3 examples with v4 imports/options. A timeline sequence needs its own phase/interruption tests.

### React Spring: interrupted target following

Use one hook/controller per owned subtree and bind its output to an `animated` element. Separate the target from its presentation; on a new target retain the current trajectory only when that is the approved behavior. Stop the controller on unmount and render a static target for reduced motion. Springs do not have a predetermined curve or duration by default.[20] Therefore a spring is not automatically equivalent to a canonical fixed-duration transition.

Specify mass, tension/damping, overshoot limits and a settling criterion in the project brief. Prove a changed target while moving, stop/unmount, numerical bounds and settled result. List/transition hooks and StrictMode require separate tests; the historical scalar exercise does not establish them.

### AutoAnimate: immediate-child layout changes

Attach one owner to a container; let the application add/move/remove persistent immediate children. AutoAnimate observes those immediate-child changes.[21] Set explicit timing/policy and disable it when motion is reduced. Before container removal, disable the controller and cancel any owned retained animations. Verify no detached work continues; disabling is not automatically proof of complete resource disposal.

Test the actual interior and later phases of add/move/remove, not just an early zero-opacity frame. Flex growth, virtualization and nested observers can change geometry and need explicit transfer tests. Do not use AutoAnimate alongside Flip/Motion layout ownership on the same children. Its implicit effects must match M16 rather than silently adding a new entry/exit vocabulary.

## Lenis and shared clocks

Native scroll remains the baseline. Use Lenis only when designed momentum materially benefits an approved experience. Start with an opt-in bounded reader; preserve wheel/touch, keyboard, anchors, selection and focused-element visibility before and after setup/failure/teardown.

Manual Lenis `raf` takes milliseconds; its documented destroy method removes instance events, not the application's independent clock.[13] If integrating with GSAP, use one owner callback that converts ticker seconds to milliseconds, pass `autoRaf: false`, remove that exact callback at release, then destroy the instance. Never run automatic RAF plus an external driver, or Lenis plus ScrollSmoother on the same target. Verify the time conversion by measured progress, not by apparent smoothness.

Pause owned work offscreen/hidden without overriding explicit Pause. Test a real cached Back navigation and restoration of manual scroll position. The historical lab used nested Lenis; pagewide smoothing, Safari, physical touch and shared-clock ScrollTrigger integration remain separate proof gates.

## Vector runtimes and authored tracks

### Rive

Load the asset and matching local WASM/renderer under a generation guard. Resolve exact artboard, state-machine and binding names/types after load; clamp values; let HTML own state and essential labels. In the captured canvas 2.42.0 types, singular `stateMachine` supersedes the deprecated plural option.[42] Dispose late loads and owned runtime/listeners.

Configure both primary and fallback WASM paths: the captured loader types document a CDN fallback.[43] For a local-only contract, explicitly disable that fallback with the pinned supported API and test missing WASM under external-request denial. A successful primary load does not exercise this failure path. A repository test fixture is not original ProDyn artwork or Rive Editor practice.

### Lottie and dotLottie

Choose the exact player and renderer before asset approval. Load one contained illustration with autoplay/loop explicitly off unless the brief says otherwise; seek named frames; compare decoded pixels; destroy on release. Preserve editable source and referenced images separately. A code-authored JSON fixture is not an AE export.

The dotLottie 2.0 format supports optional packaged state machines.[44] Define animation/state IDs, event and numeric guards, legal transitions, reset and paused rendering. Test reported state **and** visible output. The historical 0.80.0 trial needed an explicit refresh on a paused-state change; isolate and retest such workarounds instead of making them universal policy. Conversion/package success does not prove equivalent frames or event behavior.

### Theatre core and Studio

Export authored project state, load it through `getProject(name, { state })`, await readiness, bind the exact sheet/object names, and subscribe only owned presentation values. Seek reference times before playing. Pause/unsubscribe/detach owned objects at teardown; avoid reusing a process-cached project identity when comparing independent fixtures. Studio exposes state serialization, but localStorage alone is not a handoff.[47]

Exclude Studio imports from production, not merely its initialize call. The captured 0.7.2 core license is Apache-2.0; Studio's is AGPL, with the exact package recorded as AGPL-3.0-only.[45][46] Review the applicable distribution obligations. The historical Studio exercise edited a code-seeded midpoint and exported it; it did not author a complete visual timeline from scratch.

## Graphics: prove pixels, then justify cost

Canvas supplies a bitmap, not the document's semantic structure.[24] Keep headings, controls and outcomes in HTML. Use one bounded renderer; validate dimensions, seed, finite inputs and sample counts before replacing a good frame. Switch renderers serially with generation guards. Record the actual selected backend.

| Mechanism | Portable recipe | Negative test / release |
|---|---|---|
| **Canvas 2D** | Size CSS and drawing buffer deliberately; cap pixel area/DPR per brief; draw a deterministic frame from a bounded input | Invalid input preserves the last valid frame; a one-shot figure has no persistent RAF |
| **OffscreenCanvas** | Transfer before obtaining a main-thread context; send typed bounded messages; ignore stale generations | OffscreenCanvas is transferable.[24] On transfer/worker failure create a new main-thread canvas; terminate the worker on release |
| **PixiJS** | Construct then await `app.init`; use an owned stopped ticker for one-shot work; record actual renderer and texture ownership | Async initialization is required by v8 guidance.[25] Destroy owned resources after pending-init races; do not claim WebGL/Canvas feature parity |
| **Raw WebGL / GLSL** | Compile/link with diagnostics; define uniforms, precision, viewport, blending and framebuffer ownership; draw known input | Read actual pixels; induce context loss; show a static fallback; delete owned programs/buffers/textures and restore with a fresh context |
| **OGL** | Use its renderer/program/geometry abstractions around original shader code; set the same coordinate/input contract as the baseline | Track who owns the context and resources; shader portability is not a performance result. Registry metadata declares Unlicense but the audited package lacked a root notice: review before redistribution.[31] |
| **regl** | Define a draw command with explicit uniforms/resources; invoke on state changes rather than starting an unnecessary frame loop | Cancel owned frame handles; `regl.destroy()` releases context-associated resources.[28] Do not destroy a shared context |
| **p5.js** | Use instance mode and a dedicated container; `noLoop()` for one-shot work; redraw after valid input | `remove()` stops the sketch loop and removes sketch-created elements.[30] The researched 2.3.2 package declares LGPL-2.1, not MIT.[32] |
| **WebGPU compute** | Request a real adapter/device; bound storage inputs; guard workgroup tails; dispatch; copy to MAP_READ; compare all output with a CPU oracle | API presence alone is not usable hardware.[29] Label missing adapter/device as fallback; unmap/destroy resources; test device loss and denied capability |

**Resolve evidence conflicts narrowly.** The Pixi 8.16 announcement describes an experimental Canvas renderer, while the captured general renderer guide still said “Coming-soon.”[26][27] The lab's 8.20.1 Canvas pixels support existence in that setup, not full parity or universal support. Do not mirror the stale guide or extrapolate from one contour rendering to heavy continuous throughput.

A shader/material or compute study does not grant a new ProDyn effect. Keep exact M8/M1 sources unchanged; M13 approval still applies to its 3D use. Benchmark the actual composition rather than selecting an engine from an invented universal particle-count threshold.

## Physics, audio and haptics

For simple input acknowledgment, use the canonical finite transition; do not introduce a solver. If a separately approved task truly requires force or collision:

- **Custom springs/forces:** specify units, mass, restoring force, damping and integration method. Use fixed steps, clamp elapsed accumulation and cap catch-up. Record discarded wall time, settling tolerance and overshoot. Test retarget/reset, not only a pretty bounce.
- **Matter.js:** update with milliseconds and explicit fixed steps; clear owned Composite contents as well as engine caches.[33][34] Keep the render clock separate from the solver.
- **Planck.js:** use a fixed world step; avoid mutating/destroying bodies inside a locked step.[35] Define world-to-screen scale and test contact against a known fixture.
- **Rapier:** await WASM initialization; create an owned world with explicit timestep; dispose stale initialization; call `world.free()` once and never read freed handles.[36] Test failed initialization and rapid backend switching.
- **cannon-es:** step with the specified fixed interval; remove owned bodies on release and cancel the external loop independently.[37] Do not claim cross-solver numerical equivalence or cross-machine determinism from similar final poses.

**Sound starts silent.** A user gesture opts into audio; consent is distinct from autoplay technically succeeding. Keep a visual/text alternative, explicit mute/stop and a context owner. Never restart sound on history restoration.

- **Web Audio:** create/resume an owned context after activation; connect a bounded envelope and stop/disconnect owned nodes. Use OfflineAudioContext for sample analysis, not proof of audible output—it does not render to device hardware.[38]
- **Tone.js:** select a pinned integration that does not create live global audio before opt-in; assign the owned context before starting/constructing instruments. The inspected `start()` resumes the global context, so ownership matters.[39] Avoid copying update-sensitive private deep imports without tests. Dispose transport events, instruments and only an exclusively owned context.
- **Howler.js:** set explicit autoplay, loop, gain, rate and activation policy. Unload the owned Howl on release; the documented method stops its sounds and removes it from cache.[40] Do not close a context shared with the rest of the application.
- **Haptics:** request only a bounded opt-in event; cancel outstanding patterns on stop/hide. API presence or a successful return is not actuator proof; unsupported devices can do nothing.[41] Require actual hardware review, or report unavailable without blocking the task.

Listening quality, touch latency, vibration and mobile energy use require human/physical-device evidence. The catalog preserves these gates rather than converting sample analysis or emulated input into certification.

## Sources

[1] https://developer.mozilla.org/en-US/docs/Web/API/Element/animate
[2] https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@property
[3] https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/animation-timeline
[4] https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition
[7] https://gsap.com/standard-license
[8] https://gsap.com/docs/v3/Plugins/Flip
[9] https://gsap.com/docs/v3/Plugins/ScrollTrigger
[10] https://motion.dev/docs/react-animate-presence
[11] https://motion.dev/docs/react-use-reduced-motion
[12] https://registry.npmjs.org/framer-motion/-/framer-motion-13.2.0.tgz
[13] https://raw.githubusercontent.com/darkroomengineering/lenis/main/README.md
[14] https://gsap.com/docs/v3/Plugins/Observer
[15] https://gsap.com/docs/v3/Plugins/InertiaPlugin
[16] https://gsap.com/docs/v3/Plugins/SplitText
[17] https://gsap.com/docs/v3/Plugins/MorphSVGPlugin
[18] https://gsap.com/docs/v3/Plugins/ScrollToPlugin
[19] https://gsap.com/docs/v3/Plugins/ScrollSmoother
[20] https://react-spring.dev/docs
[21] https://auto-animate.formkit.com
[22] https://gsap.com/community/standard-license
[23] https://animejs.com/documentation/animation/animation-methods/revert
[24] https://html.spec.whatwg.org/multipage/canvas.html
[25] https://pixijs.com/8.x/guides/components/application
[26] https://pixijs.com/blog/8.16.0
[27] https://pixijs.com/8.x/guides/components/renderers
[28] https://raw.githubusercontent.com/regl-project/regl/5689d9746a45cf2f8e3ce95a3edeb3d8376bd1ad/API.md
[29] https://www.w3.org/TR/webgpu
[30] https://p5js.org/reference/p5/remove
[31] https://registry.npmjs.org/ogl/1.0.11
[32] https://registry.npmjs.org/p5/2.3.2
[33] https://raw.githubusercontent.com/liabru/matter-js/0.20.0/src/core/Engine.js
[34] https://brm.io/matter-js/docs/classes/Composite.html
[35] https://raw.githubusercontent.com/piqnt/planck.js/93dd64df0fd2e5388551b159bebc6306e7af580a/src/dynamics/World.ts
[36] https://raw.githubusercontent.com/dimforge/rapier.js/master/src.ts/pipeline/world.ts
[37] https://raw.githubusercontent.com/pmndrs/cannon-es/master/src/world/World.ts
[38] https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext
[39] https://raw.githubusercontent.com/Tonejs/Tone.js/d2d52ffa8803b35debd9f19f2da08ad1c3540de0/Tone/core/Global.ts
[40] https://raw.githubusercontent.com/goldfire/howler.js/master/README.md
[41] https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API
[42] https://unpkg.com/@rive-app/canvas@2.42.0/rive.d.ts
[43] https://unpkg.com/@rive-app/canvas@2.42.0/runtimeLoader.d.ts
[44] https://dotlottie.io/spec/2.0
[45] https://unpkg.com/@theatre/core@0.7.2/LICENSE
[46] https://unpkg.com/@theatre/studio@0.7.2/LICENSE
[47] https://www.theatrejs.com/docs/latest/api/studio
