| name | theatrejs-cinematics |
| description | Guide for implementing Theatre.js animation timeline and instant replay system. Use this when working with cutscenes, instant replays, cinematic camera animations, or timeline-based animations. |
Theatre.js Cinematics and Replay System
This skill provides guidance for implementing Theatre.js for cinematic animations, instant replays, and timeline-based camera movements in browser games.
Technology Stack
- Theatre.js @theatre/core - Animation engine
- Theatre.js @theatre/r3f - React Three Fiber integration
- Theatre.js @theatre/studio - Visual editor (development only)
Installation
npm install @theatre/core @theatre/r3f @theatre/studio
Basic Setup
import { getProject } from '@theatre/core';
import studio from '@theatre/studio';
if (process.env.NODE_ENV === 'development') {
studio.initialize();
}
const project = getProject('ChadPowersGame');
const sheet = project.sheet('ThrowReplay');
React Three Fiber Integration
import { Canvas } from '@react-three/fiber';
import { SheetProvider, editable as e, PerspectiveCamera } from '@theatre/r3f';
import { getProject } from '@theatre/core';
const project = getProject('ChadPowersGame');
const sheet = project.sheet('Scene');
function App() {
return (
<Canvas>
<SheetProvider sheet={sheet}>
<PerspectiveCamera
theatreKey="Camera"
makeDefault
position={[0, 5, 10]}
fov={75}
/>
<e.mesh theatreKey="Ball">
<sphereGeometry args={[0.143, 32, 32]} />
<meshStandardMaterial color="brown" />
</e.mesh>
</SheetProvider>
</Canvas>
);
}
Cinematic Camera System
Define Camera Animation Objects
import { types } from '@theatre/core';
const project = getProject('ChadPowersReplays');
const sheet = project.sheet('ThrowReplay');
const cameraObj = sheet.object('Camera', {
position: types.compound({
x: types.number(0, { range: [-50, 50] }),
y: types.number(5, { range: [0, 20] }),
z: types.number(10, { range: [-50, 50] })
}),
lookAt: types.compound({
x: types.number(0, { range: [-50, 50] }),
y: types.number(1, { range: [0, 10] }),
z: types.number(-20, { range: [-100, 0] })
}),
fov: types.number(75, { range: [30, 120] })
});
cameraObj.onValuesChange((values) => {
camera.position.set(values.position.x, values.position.y, values.position.z);
camera.lookAt(values.lookAt.x, values.lookAt.y, values.lookAt.z);
camera.fov = values.fov;
camera.updateProjectionMatrix();
});
Instant Replay System
Recording Gameplay State
Record physics state at 20Hz during gameplay for replay:
interface ReplayFrame {
timestamp: number;
ball: {
position: [number, number, number];
rotation: [number, number, number, number];
velocity: [number, number, number];
};
camera: {
position: [number, number, number];
target: [number, number, number];
};
}
class ReplayRecorder {
private frames: ReplayFrame[] = [];
private maxDuration = 5000;
private recordInterval = 50;
private lastRecordTime = 0;
record(state: GameState, timestamp: number) {
if (timestamp - this.lastRecordTime < this.recordInterval) return;
this.frames.push({
timestamp,
ball: {
position: [...state.ball.position],
rotation: [...state.ball.rotation],
velocity: [...state.ball.velocity]
},
camera: {
position: [...state.camera.position],
target: [...state.camera.target]
}
});
const cutoff = timestamp - this.maxDuration;
this.frames = this.frames.filter(f => f.timestamp > cutoff);
this.lastRecordTime = timestamp;
}
getFrames(): ReplayFrame[] {
return [...this.frames];
}
}
Playing Back Replays with Theatre.js
async function playReplay(frames: ReplayFrame[], playbackSpeed: number = 0.25) {
const project = getProject('ChadPowersReplays');
const replaySheet = project.sheet('InstantReplay');
const ballObj = replaySheet.object('Ball', {
position: types.compound({
x: types.number(0),
y: types.number(0),
z: types.number(0)
}),
rotation: types.compound({
x: types.number(0),
y: types.number(0),
z: types.number(0),
w: types.number(1)
})
});
const replayCameraObj = replaySheet.object('ReplayCamera', {
position: types.compound({
x: types.number(5),
y: types.number(3),
z: types.number(5)
}),
lookAt: types.compound({
x: types.number(0),
y: types.number(1),
z: types.number(-10)
})
});
ballObj.onValuesChange((values) => {
ballMesh.position.set(values.position.x, values.position.y, values.position.z);
ballMesh.quaternion.set(
values.rotation.x,
values.rotation.y,
values.rotation.z,
values.rotation.w
);
});
const sequence = replaySheet.sequence;
await sequence.play({
iterationCount: 1,
range: [0, 3],
rate: playbackSpeed,
direction: 'normal'
});
}
Cinematic Camera Paths
Pre-defined Camera Angles
const CAMERA_ANGLES = {
behindQB: {
position: { x: 0, y: 3, z: 5 },
lookAt: { x: 0, y: 1, z: -20 },
fov: 60
},
sideView: {
position: { x: 15, y: 2, z: -10 },
lookAt: { x: 0, y: 1, z: -20 },
fov: 50
},
endZone: {
position: { x: 0, y: 5, z: -45 },
lookAt: { x: 0, y: 1, z: 0 },
fov: 70
},
aerial: {
position: { x: 0, y: 30, z: -20 },
lookAt: { x: 0, y: 0, z: -20 },
fov: 45
}
};
function transitionToAngle(
angleName: keyof typeof CAMERA_ANGLES,
duration: number = 1
) {
const angle = CAMERA_ANGLES[angleName];
const cameraSheet = project.sheet('CameraTransition');
cameraSheet.sequence.play({
iterationCount: 1,
range: [0, duration]
});
}
Slow Motion Effects
async function playSlowMotionReplay(
startTime: number,
endTime: number,
speed: number = 0.25
) {
const sheet = project.sheet('SlowMoReplay');
await sheet.sequence.play({
iterationCount: 1,
range: [startTime, endTime],
rate: speed,
direction: 'normal'
});
}
async function playReverseSlowMo(startTime: number, endTime: number) {
const sheet = project.sheet('SlowMoReplay');
await sheet.sequence.play({
iterationCount: 1,
range: [startTime, endTime],
rate: 0.25,
direction: 'reverse'
});
}
Sequence Control API
const sequence = sheet.sequence;
await sequence.play({
iterationCount: 1,
range: [0, 3],
rate: 1,
direction: 'normal'
});
sequence.pause();
sequence.position = 1.5;
if (sequence.playing) {
}
const currentTime = sequence.position;
Attaching Audio to Sequences
await sheet.sequence.attachAudio({
source: '/audio/crowd-cheer.mp3'
});
const audioContext = new AudioContext();
const audioBuffer = await loadAudioBuffer('/audio/commentary.mp3');
await sheet.sequence.attachAudio({
source: audioBuffer,
audioContext,
destinationNode: audioContext.destination
});
Studio Mode for Development
if (process.env.NODE_ENV === 'development') {
import('@theatre/studio').then((studioModule) => {
studioModule.default.initialize();
import('@theatre/r3f/dist/extension').then((extensionModule) => {
studioModule.default.extend(extensionModule.default);
});
});
}
Exporting Animation State
const state = project.exportState();
localStorage.setItem('theatreState', JSON.stringify(state));
const savedState = localStorage.getItem('theatreState');
if (savedState) {
const project = getProject('ChadPowersGame', {
state: JSON.parse(savedState)
});
}
Performance Tips
- Use RAF Drivers for custom frame rates
- Batch value changes to minimize updates
- Dispose sheets when not needed
- Use static state in production (no studio)
import { createRafDriver } from '@theatre/core';
const replayDriver = createRafDriver({ name: 'replayDriver' });
sequence.play({
rafDriver: replayDriver
});
References