Building Graph Timelines
Model behavior as states with transitions driven by time or conditions.
Graph timelines let you model states and transitions. We'll switch between two states (red
and blue
) depending on the mouse position, using promise-based triggers instead of React state.
In the following example, you can move the mouse to the left or right of the canvas to trigger a state transition.
import { Canvas, useThree } from '@react-three/fiber' import { useRef } from 'react' import { Mesh } from 'three' import { useTimelineGraph, action, lookAt, spring } from '@react-three/timeline' function mouseOnSide(side: 'left' | 'right'): Promise<void> { 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) }) } function Scene() { const camera = useThree((s) => s.camera) const red = useRef<Mesh>(null) const blue = useRef<Mesh>(null) useTimelineGraph( '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], ) return ( <> <mesh ref={red} position-x={-2} position-y={-1} rotation-y={(-30 / 180) * Math.PI} scale={[0.2, 0.2, 0.4]}> <sphereGeometry /> <meshPhysicalMaterial emissive="red" emissiveIntensity={1.5} color="red" /> </mesh> <mesh ref={blue} position-x={2} position-y={-1} rotation-y={(20 / 180) * Math.PI} scale={[0.2, 0.2, 0.4]}> <sphereGeometry /> <meshPhysicalMaterial emissive="blue" emissiveIntensity={5} color="blue" /> </mesh> <ambientLight intensity={0.5} /> <directionalLight position={[3, 3, 3]} intensity={1} /> </> ) } export default function App() { return ( <Canvas style={{ position: "absolute", inset: "0", touchAction: "none" }}> <Scene /> </Canvas> ) }
Key ideas
- Define a
stateMap
withtimeline
for each state andtransitionTo
conditions. - Transitions can be driven by promises (e.g., “resolve when mouse is on the right side”).
Extras:
- You can return a state name from a state's
timeline
to jump programmatically. - You can set
transitionTo: { finally: 'someState' }
to always transition after a state's timeline completes.
Build it in steps
- Create a reusable promise trigger for either side
function mouseOnSide(side: 'left' | 'right'): Promise<void> {
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)
})
}
- Start the graph with inline states (no separate variable) and render the scene. The state shape is inferred by
useTimelineGraph
.
useTimelineGraph(
'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],
)