Skip to main content
Declarative component for playing back recorded trajectories. Wraps the useTrajectoryPlayer hook for prop-driven control.

Usage

<MujocoCanvas config={config}>
  <TrajectoryPlayer
    trajectory={recorder.frames}
    fps={30}
    speed={1.5}
    loop
    playing={isPlaying}
    onFrame={(idx) => console.log("Frame:", idx)}
    onComplete={() => setIsPlaying(false)}
    onStateChange={(state) => console.log(state)}
  />
</MujocoCanvas>

Props

trajectory
TrajectoryFrame[] | number[][]
required
Trajectory data. Accepts TrajectoryFrame[] from useTrajectoryRecorder directly, or number[][] where each inner array is one frame of joint positions.
fps
number
default:"30"
Playback frame rate.
speed
number
default:"1.0"
Speed multiplier. 0.5 = half speed, 2 = double speed.
loop
boolean
default:"false"
Loop playback when reaching the end.
playing
boolean
Declarative play/pause control.
mode
"kinematic" | "physics"
default:"kinematic"
Playback mode. kinematic pauses physics and drives qpos directly. physics keeps the simulation running and applies ctrl values from the trajectory.
onFrame
(frameIdx: number) => void
Called each time a new frame is displayed. Synced to the R3F render loop via useFrame for precise timing.
onComplete
() => void
Called when playback reaches the end (non-looping only).
onStateChange
(state: PlaybackState) => void
Called on every state transition (idle, playing, paused, completed).

How It Works

  1. When playing becomes true, playback starts (in kinematic mode, the simulation is paused)
  2. Each render frame, the component advances the trajectory index based on elapsed time, fps, and speed
  3. The current frame’s qpos values are written to data.qpos (kinematic) or ctrl to data.ctrl (physics)
  4. mj_forward is called to update positions without stepping physics (kinematic mode only)
  5. When playback reaches the end, onComplete fires and state transitions to completed
  6. The onFrame callback is synced to R3F’s useFrame loop, ensuring it fires in lockstep with rendering

Notes

  • In kinematic mode, the simulation’s previous pause state is restored on completion
  • Accepts TrajectoryFrame[] directly from useTrajectoryRecorder — no format conversion needed
  • For hook-based control with play()/pause()/seek(), see useTrajectoryPlayer