timeline Full documentation content. s.camera) const redPill = useRef(null) const bluePill = useRef(null) useRunTimeline(function* () { while (true) { //transition to look at the red pill yield* action({ update: lookAt(camera, redPill.current!, spring()) }) //transition to look at the blue pill yield* action({ update: lookAt(camera, bluePill.current!, spring()) }) } }, []) return ( <> Remember: all I'm offering is the truth. Nothing more. ) ``` ## Documentation - [What is a timeline?](./1-what-is-a-timeline.mdx) - [Building your First Timeline](./2-first-timeline.mdx) - [Overview of All Functions](./3-overview.mdx) - [Tutorial: Building Complex Timelines using Parallel](../tutorials/5-parallel.mdx) - [Tutorial: Building Graph Timelines](../tutorials/6-graph.mdx) - [Tutorial: Use pmndrs/timeline in vanilla three.js](../tutorials/7-vanilla.mdx) ## Sponsors This project is supported by a few companies and individuals building cutting edge 3D Web & XR experiences. Check them out! ![Sponsors Overview](https://bbohlender.github.io/sponsors/screenshot.png) ## Roadmap \[ ] record, useRecordedTimeline, ... => allows to bind the timeline progress to any controls (e.g. scrolling); allowing using markers to attach scroll positions (e.g. button center screen) to the time of the timeline\ \[ ] yield\* cleanup(() => ...)\ \[ ] add external methods for the graph (set/get state externally)\ \[ ] queue with optional priority sorting and cancellation (configure what happens with the current timeline when canceled)\ \[ ] timeline visualization and `yield* marker(...)` to appear in the visualization (e.g. log values or complete json objects) ]]> (model = value)) })`. - provide an `update(state, clock)` called every frame; return `false` to finish - provide an `until` condition or promise to decide when it finishes - or any combination of the above Use an action when you want to perform work over time, wait for something to happen, or both. #### What does an action look like? ```ts yield * action({ update: (state, clock) => { // apply changes every frame return false // finish immediately }, }) ``` #### Waiting with `until` ```ts yield * action({ until: timePassed(1, 'seconds') }) ``` `until` accepts either - a synchronous condition `(state, clock, actionTime) => boolean`, checked at the start of every frame (`timePassed`, `forever` and `queue.waitUntilNotEmpty()` return one), or - a promise for external events (`mediaFinished`, `animationFinished`, a click, a network request). Timelines only advance inside the per-frame `update` call, so everything is stepped by frames and simulation time: `timePassed` counts the sum of frame deltas, not wall clock time. A promise is noticed on the next frame after it resolves (at most one frame of latency), so prefer conditions for sequencing that happens inside your simulation and promises for events from the outside world. > \[!IMPORTANT] Timelines are synchronous generators, so `await` is not available inside them. Wrap waits in an action and `yield*` it: > > ```ts > yield* action({ until: someAsyncThing() }) > ``` #### How long does an action run? An action runs until either: - its `update` returns `false`, or - its `until` condition is met or its `until` promise resolved. If both are provided, the action keeps updating until the `until` is met. When an action finishes, the timeline immediately continues to its next action within the same frame. The next action receives its first update in that same frame only if the finished action did not use it (its `until` was already met at the start of the frame); otherwise it starts updating on the next frame. Three actions whose `update` returns `false` therefore run on three consecutive frames. #### The action clock The `clock` passed to `update(state, clock, actionTime, memo)` contains: - `delta`: seconds since the last frame, as passed to the update function returned by `runTimeline` - `prevDelta`: the previous `delta`, `0` on the first frame (used by `spring`/`velocity` to infer velocity) - `frame`: index of the current update, starting at 0 (lets a finished `transition`/`lookAt`/`offset*` hand its motion to the next action on the same target in the following frame) The third argument, `actionTime`, is the time since the current action started (it resets between actions). ### Timelines A timeline is a generator that yields actions (and can yield other timelines). You can loop, wait, and branch—writing behavior like a story. #### Reusable timelines A reusable timeline is a function that returns a timeline so it can be started many times. ```ts function* reusableFadeIn() { yield* action({ until: timePassed(0.5, 'seconds') }) } // later inside another timeline yield * reusableFadeIn() ``` ### Running timelines - **React**: use `useRunTimeline(reusableTimeline, deps)`; it wires into `useFrame` and restarts on `deps` change. - **Vanilla**: use `runTimeline(reusableTimeline)` to get an update function you call each frame with `deltaSeconds`. ```ts // React useRunTimeline(() => reusableFadeIn(), [someDep]) // Vanilla const update = runTimeline(reusableFadeIn) // in your render loop update(state, deltaSeconds) ``` ### Orchestration You can chain actions/timelines sequentially or run them concurrently. #### Sequential chaining - **Sequential**: `yield* action(A); yield* action(B);` runs A, then B. ```ts // Sequential yield * action(A) yield * action(B) ``` #### Parallel composition - **All**: `yield* parallel('all', A, B)` continues when both finish and returns `false` if one of them returned `false`, otherwise `true` - **Race**: `yield* parallel('race', A, B)` continues when the first finishes and returns what that one returned ```ts const bothSucceeded = yield * parallel('all', A, B) const firstOutcome = yield * parallel('race', A, B) ``` ### Sub-timelines Timelines compose: you can `yield*` another reusable timeline anywhere. This keeps complex behaviors modular and readable. ```ts // Reusable timeline function* someReusableTimeline() { yield* action({ until: timePassed(0.5, 'seconds') }) } // Compose inside another timeline yield * someReusableTimeline() ``` ### Resources and cancellation When a timeline is canceled (a race is lost, a graph state is left, the abort signal passed to `runTimeline` aborts), its `finally` blocks run synchronously. Use `try`/`finally` to bind a resource to a timeline's lifetime: ```ts function* withSound() { const audio = new Audio('./sound.mp3') try { yield* action({ until: audio.play() }) yield* action({ until: mediaFinished(audio) }) } finally { audio.pause() } } ``` ### Graph timelines Model behaviors as named states and transitions. `graph(initial, stateMap)` drives the flow; `` and `` are the React components. ```ts yield* graph('Idle', { Idle: { timeline: () => action({ update: ... }), transitionTo: { Run: { whenUpdate: timePassed(1, 'seconds') } } }, Run: {/* ... */}, }) ``` ### QueueTimeline Use a queue to sequence reusable timelines that are added dynamically. The queue drains in order until empty each time you run it. ```ts const queue = new QueueTimeline() // Manager timeline: repeatedly drain the queue, then wait for new work function* queueManager() { while (true) { // runs each enqueued timeline sequentially until the queue is empty yield* queue.run() // pause until someone enqueues again yield* action({ until: queue.waitUntilNotEmpty() }) } } // Somewhere else: enqueue work queue.attach(function* () { yield* action({ until: timePassed(0.5, 'seconds') }) }) // Insert at a specific position (e.g. front of the queue) queue.attach(function* () { /* ... */ }, 0) // Clear pending items queue.clear() ``` - **attach(timeline, index?)**: enqueue at optional index (default appends). - **run()**: timeline that runs each enqueued timeline in FIFO order until empty (throws when run concurrently). - **waitUntilNotEmpty()**: `until` condition that is met while the queue has entries. - **length**: current number of queued timelines. This is useful for input-driven behaviors (clicks, network events) where you want actions to play back-to-back without interleaving. ### GraphTimeline (class API) Beyond the `graph(initial, map)` helper, you can control a state machine via the `GraphTimeline` class—attach/unattach states at runtime, read the current state, or force transitions. ```ts const machine = new GraphTimeline('Idle', { Idle: { timeline: () => action({ update: () => true }), // keep updating until a transition fires transitionTo: { Run: { whenUpdate: timePassed(1, 'seconds') }, }, }, Run: { timeline: () => action({ until: timePassed(0.5, 'seconds') }), transitionTo: { finally: 'Idle' }, // jump here after timeline completes }, }) // Start the graph inside a timeline yield* machine.run() // At runtime, you can manipulate it: machine.getState() // 'Idle' machine.setState('Run') // cancels the current state; 'Run' starts on the next frame machine.attach('Attack', () => action({ until: timePassed(0.2, 'seconds') }), { finally: 'Idle' }) machine.unattach('Attack') ``` Transitions can be expressed with: - **whenUpdate(state, clock, actionTime) => boolean**: transition when this returns true; checked at the start of every frame with `actionTime` being the time the state ran before this frame, so the next state receives that frame (e.g. `timePassed(1, 'seconds')`) - **whenPromise: () => Promise**: transition on the next frame after the promise resolved - **finally: 'StateName'**: default next state after the state's `timeline` completes A state's `timeline` may also return a state name to transition to: ```ts function* Run() { // ... do work ... return 'Idle' // explicit next state } ``` ### Built‑ins for motion - **Action updates**: `transition`, `lookAt`, `offsetDistance`, `offsetRotation` - **Eases**: `spring(preset|config)`, `time(durationSec)`, `velocity(speed, maxAccel?)` - **Helpers**: `worldSpace(type, object)`, `property(object, key)`, waits like `timePassed`, `mediaFinished`, `animationFinished`, plus `doUntil`/`doWhile` Explore full, runnable examples in the tutorials: - Building your First Timeline - Building Complex Timelines using Parallel - Building Graph Timelines - Use pmndrs/timeline in vanilla three.js ]]> {/* Add in the next step */} ) } ``` 4. Add meshes and references ```tsx import { useRef } from 'react' import { Mesh } from 'three' import { Text } from '@react-three/drei' function Scene() { const red = useRef(null) const blue = useRef(null) return ( <> Your first timeline ) } ``` 5. Add the timeline We use `useTimeline` to run a `while (true)` loop, which keeps the sequence going, and each `yield* action({ update: lookAt(...) })` eases the camera toward a target using a spring; once the ease completes, the next action starts. ```tsx import { useThree } from '@react-three/fiber' import { useTimeline, action, lookAt, spring, springPresets } from '@react-three/timeline' function Scene() { const camera = useThree((s) => s.camera) const red = useRef(null) const blue = useRef(null) useRunTimeline(function* () { while (true) { yield* action({ update: lookAt(camera, red.current!, spring(springPresets.stiff)) }) yield* action({ update: lookAt(camera, blue.current!, spring(springPresets.stiff)) }) } }, []) // ...return meshes as above } ``` 7. Mount the scene ```tsx export default function App() { return ( ) } ``` ]]> ` ### time action update easing function for a linear ease based on the provided duration in seconds #### Parameters - \`duration\` (number) #### Returns `EaseFunction` ### velocity action update ease function for easing values according to the target velocity may overshoot the goal; finishing does not guarantee exact arrival at a waypoint #### Parameters - \`velocity\` (number) - \`maxAcceleration\` (number): (optional) can be used to limit the change in velocity #### Returns `EaseFunction` ## Action Update ### lookAt action update function for making a src object or quaternion look from its position towards the `toPosition` #### Parameters - \`from\` (`Object3D | { position: Vector3 | () => Vector3; rotation: Quaternion | (newValue?: Quaternion) => Quaternion }`) - \`toPosition\` (`Object3D | Vector3 | number[] | () => Vector3 | number[]`) - \`ease\` (`EaseFunction`): allows to ease the rotation from the current state to the target position > \[!NOTE] World forward is −Z with up = `Object3D.DEFAULT_UP`. If source and target positions are identical. #### Returns `ActionUpdate` ### offsetDistance action update function for making a src object or position move away/towards the to position to be at the specified distance combined with offsetRotation, this action update describes an exact target position in orbital coordinates #### Parameters - \`from\` (`Object3D | Vector3 | (newValue?: Vector3) => Vector3`) - \`to\` (`Object3D | Vector3 | () => Vector3`) - \`distance\` (number) - \`ease\` (`EaseFunction`): allows to ease the position from the current state to the position with the target distance > \[!NOTE] If `from` and `to` coincide (zero distance), direction is undefined; the action no‑ops and finishes. #### Returns `ActionUpdate` ### offsetRotation action update function for making a src object or position move to a position with the specified rotation offset combined with offsetDistance, this action update describes an exact target position in orbital coordinates #### Parameters - \`from\` (`Object3D | Vector3 | (newValue?: Vector3) => Vector3`) - \`to\` (`Object3D | Vector3 | () => Vector3`) - \`rotation\` (`Quaternion | Euler | number[] | () => Quaternion | Euler | number[]`): the offset rotation in world space > \[!NOTE] Applies a world‑space rotation around `to` using a forward vector derived from the world up axis. Pair with `offsetDistance` to define orbital targets. - \`ease\` (`EaseFunction`): allows to ease the position from the current state to the position with the target rotation offset #### Returns `ActionUpdate` ### transition action update function for making a src object, position, rotation, scale, number change towards the to value #### Parameters - \`from\` (`TransitionFrom | (newValue?: Vector3 | Quaternion) => number | TransitionFrom`) - \`to\` (`TransitionTo | () => TransitionTo`) - \`ease\` (`EaseFunction`): allows to ease the value from the current value to the target value Without an ease, vector and numeric cuts carry the preceding frame's velocity into the next action. An intervening idle frame clears that momentum; rotation cuts do not carry angular velocity. #### Returns `ActionUpdate` ## Action Until ### animationFinished action until function for executing an action until a animation has finished playing #### Parameters - \`animation\` (AnimationAction) #### Returns `Promise` ### forever action until condition for executing an action until forever #### Returns `UntilCondition` ### mediaFinished action until function for executing an action until a html media has finished playing #### Parameters - \`media\` (`HTMLAudioElement | HTMLVideoElement`) #### Returns `Promise` ### promiseConcat action until function for concatenating multiple action until functions #### Parameters - \`promises\` (`Promise[]`) #### Returns `Promise` ### timePassed action until condition for executing an action until a certain amount of simulation time (the sum of frame deltas) has passed #### Parameters - \`time\` (number) - \`unit\` (`"seconds" | "milliseconds"`) #### Returns `UntilCondition` ## Structural ### abortable a timeline wrapper that allows to make any timeline externally cancelable via an abort signal aborting cancels the wrapped timeline immediately; the wrapper finishes on the next frame without using it #### Parameters - \`timeline\` (`Timeline`) - \`abortSignal\` (AbortSignal) #### Returns `NonReuseableTimeline` ### action core function for yielding an action used via `yield* action({...})` allows to run this action `until` a certain event and execute an `update` function until the action is finished #### Parameters - \`params\` (`ActionParams`) #### Returns `NonReuseableTimeline` ### doUntil function for generating a timeline that repeats the inner timeline until the `until` condition or promise is met #### Parameters - \`until\` (`Until`) - \`timeline\` (`ReusableTimeline`) #### Returns `NonReuseableTimeline` ### doWhile function for generating a timeline that repeats the inner timeline while the condition holds the condition is checked at the start of every frame; once it fails, the frame passes on to what follows #### Parameters - \`condition\` (`UntilCondition`) - \`timeline\` (`ReusableTimeline`) #### Returns `NonReuseableTimeline` ### graph timeline function for building a state graph with transitions as edges #### Parameters - \`enterStateName\` (string): is the name of the initial state - \`stateMap\` (`GraphTimelineStateMap`): is the map of states including their transitions to other states - \`exitStateName\` (string) #### Returns `NonReuseableTimeline` ### parallel function for executing multiple timelines in parallel each timeline receives every frame in the given order; `false` and `true` entries are skipped without any timelines (e.g. all entries are `false`), it returns immediately in a `"race"`, the first timeline that finishes ends the race before later timelines receive the frame; put conditions (e.g. `action({ until })`) first so that a met condition hands the frame on to what follows the race the outcome (what `yield* parallel(...)` returns): - `"race"` returns what the timeline that ended first returned (never what a cancelled timeline returns); `undefined` without timelines - `"all"` returns `false` if any timeline returned `false`, otherwise `true` (a timeline returning nothing counts as succeeded); `true` without timelines the race's outcome is typed from its timelines; with an explicit state type argument (`parallel('race', ...)`) it is `unknown` #### Parameters - \`type\` (`"all" | "race"`): when to stop all timelines - either wait for `"all"` or cancel all timelines once the first timeline is done via `"race"` - \`timelines\` (`(boolean | Timeline)[]`) #### Returns `NonReuseableTimeline>` for `"race"`, `NonReuseableTimeline` for `"all"` ### runTimeline function for starting a timeline starts the generator up to its first action immediately, then advances it inside the returned update function errors propagate to the caller; deltas are passed through without clamping or substeps #### Parameters - \`timeline\` (`Timeline`) - \`abortSignal\` (AbortSignal) #### Returns `Update` ## Misc ### Action attaches an action to the enclosing timeline; until is a factory evaluated when the action starts without dependencies, the latest props are read on the next start; changing dependencies reattaches the action #### Parameters - \`\_\_namedParameters\` (`{ dependencies?: unknown[]; until?: () => Until } & Omit, "until">`) #### Returns `null` ### property helper function for building a write/read function of any property in an object reuse the same accessor across actions to preserve momentum; transition goals for numeric properties are plain numbers #### Parameters - \`object\` (`{ [Key in string]: number }`): the object from which to read/write a property - \`key\` (K): the key of the property from which to read/write inside of the object #### Returns `(newValue?: Vector3 | Quaternion) => number` ### useRunTimeline hook for running the specified timeline every frame errors thrown by the timeline propagate out of the frame callback #### Parameters - \`timeline\` (`ReusableTimeline`) - \`deps\` (`any[]`): tells the hook when to stop the previous timeline and restart the currently provided timeline #### Returns `void` ### worldSpace helper function for building a write/read function of the position/euler/quaternion/scale for an object in world space useful when combined with other action update functions such as lookAt #### Parameters - \`type\` (Type) - \`forObject\` (Object3D) - \`offset\` (`number[]`) #### Returns `(newValue?: any) => WorldSpaceResults[Type]` ]]> \[!NOTE] you can pass conditionally included timelines by using booleans; falsey entries are ignored. On `'race'`, non‑winning timelines are canceled and their cleanups run. The following demo shows how the left cube constantly moves since it has to cover a larger distance while the right cube waits for the cube since both move at the same speed while the right cube has a shorter distance to travel. Interactive code example (available on the documentation website). ### Key ideas - **parallel('all', ...)** waits for all child timelines to finish. It returns `false` if a child returned `false`, otherwise `true`. - **parallel('race', ...)** continues as soon as the first child finishes and cancels the rest. It returns what the first child returned. ### Build it in steps 1. Define a reusable `moveTo` sub-timeline (what it is): a small timeline that transitions a mesh position to a target using `transition(..., spring())`. ```tsx export function* moveTo(ref: React.RefObject, to: Vector3) { if (!ref.current) return yield* action({ update: transition(ref.current.position, to, velocity(1, 5)) }) } ``` 2. Create a `Scene` with two cubes and lights ```tsx import { useRef } from 'react' import { Mesh } from 'three' function Scene() { const cubeA = useRef(null) const cubeB = useRef(null) return ( <> ) } ``` 3. Add the timeline using `parallel('all')` then `parallel('race')` ```tsx import { useTimeline, parallel } from '@react-three/timeline' import { Vector3 } from 'three' function Scene() { const cubeA = useRef(null) const cubeB = useRef(null) useRunTimeline(function* () { while (true) { yield* parallel('all', moveTo(cubeA, new Vector3(-3.5, 0.5, 0)), moveTo(cubeB, new Vector3(1.5, -0.5, 0))) yield* parallel('all', moveTo(cubeA, new Vector3(0, 0, 0)), moveTo(cubeB, new Vector3(0, 0, 0))) } }, []) // ...return cubes and lights from step 2 } ``` 4. Mount the scene in a Canvas ```tsx import { Canvas } from '@react-three/fiber' export default function App() { return ( ) } ``` ]]> { return new Promise((resolve) => { function onMove(e: MouseEvent) { const onRight = e.clientX / window.innerWidth > 0.5 if ((side === 'right' && onRight) || (side === 'left' && !onRight)) { window.removeEventListener('mousemove', onMove) resolve() } } window.addEventListener('mousemove', onMove) }) } ``` 2. Start the graph with inline states (no separate variable) and render the scene. `graph` builds the state machine and `useRunTimeline` runs it. ```tsx useRunTimeline( () => graph('red', { red: { timeline: () => action({ update: lookAt(camera, red.current!, spring()) }), transitionTo: { blue: { whenPromise: () => mouseOnSide('right') } }, }, blue: { timeline: () => action({ update: lookAt(camera, blue.current!, spring()) }), transitionTo: { red: { whenPromise: () => mouseOnSide('left') } }, }, }), [camera], ) ``` ]]> Document
``` ]]>