@threlte/theatre
<Sequence>
Sequences are the heart of the Theatre.js animation system. The sequence represents the animation timeline and provides an API for controlling its playback.
In Threlte 7, you can reactively control animations through the <Sequence>
component, which you place inside a <Sheet>
.
Currently, you can only have one sequence in each sheet. Future versions of Theatre.js are expected to support multisequence sheets.
Theatre.js Docs
Sequence | Sequence Manual | Sequence API Reference |
Usage
The following example shows how <Sequence>
can be used to build a simple playback controller.
<script lang="ts">
import { Canvas } from '@threlte/core'
import { Project, Sequence, Sheet, type SequenceController } from '@threlte/theatre'
import Controller from './Controller.svelte'
import Scene from './Scene.svelte'
import state from './state.json'
let sequence: SequenceController
let position: number = 0
let playing: boolean = false
let play: (opts?: {}) => Promise<boolean>
let pause: (opts?: {}) => Promise<boolean>
let rate: number = 1
</script>
<div>
<Canvas>
<Project config={{ state }}>
<Sheet>
<Scene />
<Sequence
bind:sequence
bind:playing
bind:position
bind:play
bind:pause
iterationCount={3}
direction="alternate"
autoplay
delay={1000}
{rate}
/>
</Sheet>
</Project>
</Canvas>
<Controller
bind:position
bind:playing
bind:rate
{play}
{pause}
/>
</div>
<style>
div {
height: 100%;
}
</style>
<script lang="ts">
export let position: number
export let playing: boolean
export let play: (opts?: {}) => Promise<boolean>
export let pause: (opts?: {}) => Promise<boolean>
export let rate: number = 1
import { PauseIcon, PlayIcon } from './icons'
const fmt = (n: number) =>
n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
const toggleRate = () => {
if (rate == 1) {
rate = 0.5
} else if (rate == 0.5) {
rate = 2
} else {
rate = 1
}
}
</script>
<menu>
{#if !playing}
<button on:click={() => play()}>
<PlayIcon />
</button>
{:else}
<button on:click={() => pause()}>
<PauseIcon />
</button>
{/if}
<button on:click={() => toggleRate()}>
x{rate.toFixed(1)}
</button>
<input
type="range"
min={0}
max={1}
step={0.01}
bind:value={position}
/>
<div>{fmt(position)}</div>
</menu>
<style>
menu {
position: absolute;
top: 0;
width: 100%;
display: flex;
flex-flow: row;
align-items: center;
gap: 1rem;
padding: 1rem;
color: white;
font-weight: bold;
}
menu :global(svg) {
fill: white;
width: 24px;
height: 24px;
}
menu div {
width: 50px;
min-width: 50px;
}
menu input {
flex-grow: 1;
}
</style>
<script lang="ts">
import { T } from '@threlte/core'
import { SheetObject } from '@threlte/theatre'
</script>
<!-- Box -->
<SheetObject key="Box">
{#snippet children({ Transform, Sync })}
<Transform>
<T.Mesh
receiveShadow
castShadow
>
<T.BoxGeometry args={[1, 1, 1]} />
<T.MeshStandardMaterial>
<Sync
color
emissive
/>
</T.MeshStandardMaterial>
</T.Mesh>
</Transform>
{/snippet}
</SheetObject>
<T.DirectionalLight
position={[0.5, 2, 1]}
castShadow
/>
<T.AmbientLight intensity={0.2} />
<T.PerspectiveCamera
position={[4, 5, 10]}
makeDefault
oncreate={(ref) => {
ref.lookAt(0, 0.5, 0)
}}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
>
<rect
width="256"
height="256"
fill="none"
/>
<path
d="M216,48V208a16,16,0,0,1-16,16H160a16,16,0,0,1-16-16V48a16,16,0,0,1,16-16h40A16,16,0,0,1,216,48ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Z"
/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
>
<rect
width="256"
height="256"
fill="none"
/>
<path
d="M240,128a15.74,15.74,0,0,1-7.6,13.51L88.32,229.65a16,16,0,0,1-16.2.3A15.86,15.86,0,0,1,64,216.13V39.87a15.86,15.86,0,0,1,8.12-13.82,16,16,0,0,1,16.2.3L232.4,114.49A15.74,15.74,0,0,1,240,128Z"
/>
</svg>
export { default as PlayIcon } from './PlayIcon.svelte'
export { default as PauseIcon } from './PauseIcon.svelte'
{
"sheetsById": {
"default": {
"staticOverrides": {
"byObject": {
"Box": {
"scale": {
"x": 1,
"y": 1,
"z": 1
},
"rotation": {
"x": 0,
"y": 0,
"z": 0
},
"color": {
"r": 1,
"g": 1,
"b": 1,
"a": 1
}
}
}
},
"sequence": {
"subUnitsPerUnit": 30,
"length": 1,
"type": "PositionalSequence",
"tracksByObject": {
"Box": {
"trackData": {
"6jxcIVb5VE": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"position\",\"x\"]",
"keyframes": [
{
"id": "2znog1Hqt9",
"position": 0,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": 0
},
{
"id": "izysi9xIZV",
"position": 0.467,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": 0
},
{
"id": "1R7ALr_y7I",
"position": 1,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": 0
}
]
},
"CL6ilYk5F0": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"position\",\"y\"]",
"keyframes": [
{
"id": "ILuids3SS2",
"position": 0,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": 0
},
{
"id": "5Fx9QwZog4",
"position": 0.467,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": 2.884894266415041
},
{
"id": "QdxaWbUKTO",
"position": 1,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": 0
}
]
},
"k17VolLHaa": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"position\",\"z\"]",
"keyframes": [
{
"id": "ensBkfE4zJ",
"position": 0,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": 0
},
{
"id": "qGYRgJFQt0",
"position": 0.467,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": 0
},
{
"id": "acTy6onWqA",
"position": 1,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": 0
}
]
},
"8ozF3Q9pr9": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"rotation\",\"x\"]",
"keyframes": [
{
"id": "zemdW99tC7",
"position": 0.3,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": 0
},
{
"id": "FKeku-J_jh",
"position": 0.7,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": 90
}
]
},
"Yz64RYOUhH": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"rotation\",\"y\"]",
"keyframes": [
{
"id": "cO8d4gOyP7",
"position": 0.3,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": 0
},
{
"id": "GEPzCyTP8m",
"position": 0.7,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": 90
}
]
},
"aWuU3nv7t2": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"rotation\",\"z\"]",
"keyframes": []
},
"825UyDpz6a": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"emissive\"]",
"keyframes": [
{
"id": "2u6ASUPCLN",
"position": 0,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": {
"r": 0,
"g": 0,
"b": 0,
"a": 1
}
},
{
"id": "NuH1fhxmZy",
"position": 0.467,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": {
"r": 0.996078431372549,
"g": 0.23921568627450981,
"b": 0,
"a": 1
}
},
{
"id": "VmWNCbzBhT",
"position": 1,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": {
"r": 0,
"g": 0,
"b": 0,
"a": 1
}
}
]
},
"UP-UOQISBD": {
"type": "BasicKeyframedTrack",
"__debugName": "Box:[\"color\"]",
"keyframes": [
{
"id": "A-rHMw_1mz",
"position": 0,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": {
"r": 1,
"g": 1,
"b": 1,
"a": 1
}
},
{
"id": "jrpD3O8bLK",
"position": 0.467,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": {
"r": 0.996078431372549,
"g": 0.23921568627450981,
"b": 0,
"a": 1
}
},
{
"id": "3B_Qi4_QFS",
"position": 1,
"connectedRight": true,
"handles": [0.5, 1, 0.5, 0],
"type": "bezier",
"value": {
"r": 1,
"g": 1,
"b": 1,
"a": 1
}
}
]
}
},
"trackIdByPropPath": {
"[\"position\",\"x\"]": "6jxcIVb5VE",
"[\"position\",\"y\"]": "CL6ilYk5F0",
"[\"position\",\"z\"]": "k17VolLHaa",
"[\"rotation\",\"x\"]": "8ozF3Q9pr9",
"[\"rotation\",\"y\"]": "Yz64RYOUhH",
"[\"rotation\",\"z\"]": "aWuU3nv7t2",
"[\"emissive\"]": "825UyDpz6a",
"[\"color\"]": "UP-UOQISBD"
}
}
}
}
}
},
"definitionVersion": "0.4.0",
"revisionHistory": ["VSRORcDV9_yJ8fbi", "4poCDvFG3GZwc5nU", "gR-SIAqfZWUtx-q_"]
}
Lifecycle
Threlte provides lifecycle props to allow you to configure how the sequence playback is connected to the Svelte component lifecycle. See the autoplay
, autoreset
and autopause
props below.
Note that the underlying Theatre.js sheets are persisted even when unmounting a <Sheet>
component. That’s why the sequence doesn’t reset automatically when unmounting a <Sheet>
, and why the autoreset
options is required.
Audio
The audio options allow you to attach a soundtrack to your animation sequence. Theatre.js achieves this using the Web Audio API. For more details, see audio manual and attach audio API reference
Snippet Prop
When using the sequence in a child component, a snippet prop can come in handy.
<script lang="ts">
import { T } from '@threlte/core'
import { Sheet, Sequence, SheetObject } from '@threlte/theatre'
</script>
<Sheet>
<Sequence>
{#snippet children({ play })}
<SheetObject key="Cube">
{#snippet children({ Transform })}
<Transform>
<T.Mesh onclick={play}>
<T.BoxGeometry />
<T.MeshStandardMaterial />
</T.Mesh>
</Transform>
{/snippet}
</SheetObject>
{/snippet}
</Sequence>
</Sheet>