What is a Timeline?
Learn the core concepts—actions, updates, until, orchestration, and parallelism.
Timelines let you write 3D behaviors like a story using async generator functions. You yield actions, wait for conditions, and compose sequences, loops, and parallel flows in a readable way.
At a high level, timelines are composed of actions and sub‑timelines. You chain them sequentially or run them in parallel to build rich behaviors.
Actions
An action is a unit of work you yield* from a timeline. It can:
- provide an
update(state, clock)called every frame; returnfalseto finish - provide an
untilpromise to decide when it finishes - or both to run something every frame until another event happens
Use an action when you want to perform work over time, wait for something to happen, or both.
What does an action look like?
yield *
action({
update: (state, clock) => {
// apply changes every frame
return false // finish immediately
},
})
Waiting with until
yield * action({ until: timePassed(1, 'seconds') })
Never use await ... inside timelines. Awaiting a promise pauses the generator in a way that cannot be canceled by the timeline runtime. Instead, wrap waits in an action and yield* it so cancellation can propagate correctly.
Bad:
await someAsyncThing()
Good:
yield* action({ until: someAsyncThing() })
How long does an action run?
An action runs until either:
- its
updatereturnsfalse, or - its
untilpromise resolves.
If both are provided, the action keeps updating until the until resolves.
The action clock
The clock passed to update(state, clock) contains:
delta: seconds since last frame (internally clamped to ~1/30s)prevDelta: previousdeltaorundefinedon the first frame (used byspring/velocityto infer velocity)actionTime: time since the current action started (resets between actions)
Timelines
A timeline is an async 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.
async function* reusableFadeIn() {
yield* action({ until: timePassed(0.5, 'seconds') })
}
// later inside another timeline
yield * reusableFadeIn()
Running timelines
- React: use
useRunTimelinereusableTimeline, deps); it wires intouseFrameand restarts ondepschange. - Vanilla: use
runTimeline(reusableTimeline)to get an update function you call each frame withdeltaSeconds.
// 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.
// Sequential
yield * action(A)
yield * action(B)
Parallel composition
- All:
yield* parallel('all', A, B)continues when both finish - Race:
yield* parallel('race', A, B)continues when the first finishes
yield * parallel('all', A, B)
yield * parallel('race', A, B)
Sub-timelines
Timelines compose: you can yield* another reusable timeline anywhere. This keeps complex behaviors modular and readable.
// Reusable timeline
async function* someReusableTimeline() {
yield* action({ until: timePassed(0.5, 'seconds') })
}
// Compose inside another timeline
yield * someReusableTimeline()
Graph timelines
Model behaviors as named states and transitions. graph(initial, stateMap) drives the flow; useTimelineGraph is the React wrapper.
yield* graph('Idle', {
Idle: {
timeline: () => action({ update: ... }),
transitionTo: { Run: { whenPromise: () => timePassed(1, "seconds") } }
},
Run: {/* ... */},
})
TimelineQueue
Use a queue to sequence reusable timelines that are added dynamically. The queue drains in order until empty each time you run it.
const queue = new TimelineQueue()
// Manager timeline: repeatedly drain the queue, then wait for new work
async 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.add(async function* () {
yield* action({ until: timePassed(0.5, 'seconds') })
})
// Insert at a specific position (e.g. front of the queue)
queue.add(async function* () {
/* ... */
}, 0)
// Clear pending items
queue.clear()
- add(timeline, index?): enqueue at optional index (default appends). If the queue was empty, any waiters from
waitUntilNotEmpty()resolve. - run(): async generator that yields each enqueued timeline in FIFO order until empty.
- waitUntilNotEmpty(): promise that resolves immediately if there are entries, or when the next item is added.
- 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.
TimelineGraph (class API)
Beyond the graph(initial, map) helper, you can control a state machine via the TimelineGraph class—add/remove states at runtime, or force transitions.
const machine = new TimelineGraph('Idle', {
Idle: {
timeline: () => action({ update: () => true }), // keep updating until a transition fires
transitionTo: {
Run: { whenPromise: () => 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.setState('Run')
machine.addState(
'Attack',
() => action({ until: timePassed(0.2, 'seconds') }),
{ finally: 'Idle' },
)
machine.removeState('Attack')
Transitions can be expressed with:
- whenUpdate(state, clock) => boolean: transition when this returns true
- whenPromise: () => Promise: transition when the promise resolves
- finally: 'StateName': default next state after the state's
timelinecompletes
A state's timeline may also return a state name to transition to:
async 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 liketimePassed,mediaFinished,animationFinished, plusdoUntil/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