In Depth

Projects

A Theatre project is like a save file that holds all the animations and tweaks that you design in @theatre/studio. It is ultimately consumed by @theatre/core to run those animations and tweaks.

  • All your work in Theatre is organized into projects.
  • A Theatre project is similar to a project in After Effects, Figma, and other tools – it is a way to organize related work.
  • You can create multiple projects in a single web page, but often one project is sufficient for a whole website.

core.getProject()

This is the main way to create a project or access an existing one.

import {getProject} from "@theatre/core"

// This will create a project with projectId="The 3 Bunnies".
// If the project is already created, it simply returns that same project.
const project = getProject("The 3 Bunnies")

Exporting

Learn how to export/import projects, and save them to a repository. Currently explained in the video tutorial (opens new window).

Project state

The project's state is the actual save file holding the data of your project.

  • All of the tweaks and animations that you create with Theatre are considered the project's state.
  • @theatre/core uses project state to run your tweaks and animations.
  • @theatre/studio is basically an editor for project state.
  • A project's state is a JSON object.
  • You can manually tweak your project's state if you want to. That's explained in the video tutorial (opens new window).

Sheets

A sheet is analogous to a component in React, or a composition in After Effects. It contains a number of Objects, and together, they comprise a self-contained unit of animation.

A project can have multiple sheets. Each sheet could have multiple instances running at the same time.

You can create mutilple instances from a single sheet

Here is an example of 3 instances of the same sheet running at the same time. Each white box on the screen is attached to a different instance of the same sheet. They all have the same sequence of animation, but the animation runs for each of them independently.

The video tutorial has a section (opens new window) on multiple sheet instances.

Why is it called a 'sheet?'

Conceptually, they're similar to 'sheets' in spreadsheets. A sheet in Theatre contains reactive values, including animated sequences, or scripted values written in code. Also (coming soon) these values can be driven by formulas, scripts, and constraints.

Sequences

Each sheet has a single sequence (multi-sequence sheets are coming). The sequence can be played or manually scrubbed in order to create time-driven animations, parallax effects, or more.

Accessing the sequence

const sequence = sheet.sequence

sequence.position

Each sequence has a position property that determines the amount of progress through the animation.

  • If your sequence is time-based, then you can consider position to be in the unit of seconds. So, position=60 would mean 1 minute of progression through the sequence.
  • If your sequence is controlled by a dimension other than time (such as parallax effects), then the unit of position is determined by you. From Theatre's perspective, sequence.position is unit-less.

Usage:

console.log(sequence.position) // logs the current position of the sequence
sequence.position = 0 // jumps to the beginning of the sequence
sequence.position = 3 // progresses the sequence to 3 (in time-based sequences, that would be 3 seconds)

sequence.play()

This method is used in time-based sequences. It basically plays the sequence.

// play the sequence from the current position to sequence.length
sequence.play().then((finished: boolean) => {
  // It returns a promise resolves when the playback is finished or interrupted
  if (finished) {
    console.log("Playback finished")
  } else {
    console.log("Playback interrupted")
  }
})

The playback is customizable:

// play at 2x speed
sequence.play({rate: 2})

// play 1/10 speed
sequence.play({rate: 0.1})

// play from 1s to 3s.
sequence.position = 0
sequence.play({range: [1, 3]})

// *actuall* play from 2s to 3s, because the previous position is inside the given range
sequence.position = 2
sequence.play({range: [1, 3]})

// play 10 times
sequence.play({iterationCount: 10})

// play 10 times, in the normal direction
sequence.play({iterationCount: 10, direction: "normal"})
// play 10 times, in the reverse direction
sequence.play({iterationCount: 10, direction: "reverse"})
// play 10 times, alternating the direction
sequence.play({iterationCount: 10, direction: "alternate"})
// play 10 times, alternating the direction, starting in the reverse direction
sequence.play({iterationCount: 10, direction: "alternateReverse"})

sequence.pause()

The counterpart to sequence.play(). It won't have an effect if no playback is running.

sequence.pause()

sequence.attachAudio()

Attaches an audio track to the sequence. Read more in Sound and Music.

Planned features

Multi-sequence sheets

As of Theatre.js 0.4, each sheet has a single sequence. We plan to support multiple sequences in the future. Until then, you can divide your sequence into multiple time ranges to simulate multiple sequences.

If your use-case requires multiple sequences, let us know in the Discord server (opens new window) or open an issue (opens new window) so we can prioritize it.

Listening to playback state changes

We're working on exposing the playback state through events. Feel free to chime in (opens new window) on this feature.

Sound and Music

You can attach audio tracks to sequences using sequence.attachAudio(). Theatre will then play the audio track every time the sequence is played with the timings in sync.

console.log("Loading audio...")
sheet.sequence.attachAudio({source: "https://localhost/audio.ogg"}).then(() => {
  console.log("Audio loaded!")
})

In the above example, Theatre will:

  1. fetch() (opens new window) the audio file.

  2. Create a Web Audio API (opens new window) contex.

    👉 If the browser is blocking (opens new window) the audio context, Theatre will wait for a user gesture (eg. a click/touch/keydown) to initiate it. It's best to prompt the user to initiate audio playback by, for example, showing a <button>Start</button>. Once the user clicks on that button (or anywhere else), Theatre will initiate the audio context.

  3. Decode (opens new window) the audio.

  4. Resolve the returned Promise. After this, when you call sequence.play(), the audio track will play simultaneously and in sync.

If you would like to have more control over loading or the audio setup, you could provide your own audio graph.

const audioContext = new AudioContext() // create an AudioContext using the Audio API
const audioBuffer: AudioBuffer = someAudioBuffer // create an AudioBuffer from your audio file or generate one on the fly
const destinationNode = audioContext.destination // the audio output.

sheet.sequence
  .attachAudio({
    source: audioBuffer,
    audioContext,
    destinationNode,
  })
  // this promise resolves immediately as everything is already provided
  .then(() => {
    sequence.play()
  })

Or you re-use the sequence's audio graph.


sheet.sequence
  .attachAudio({
    source: "/music.mp3",
  })
  .then((graph) => {
    // this is the audioContext that the sequence created.
    const audioContext = graph.audioContext
    // this is the main gainNode that the sequence will feed its audio into
    const sequenceGain = graph.gainNode
    // let's disconnect it from graph.destinationNode so we can feed it into our own graph.
    // at this point, audio will be inaudible
    sequenceGain.disconnect()
    // create our own GainNode
    const loweredGain = audioContext.createGain()
    // lower its volume to 10%
    loweredGain.gain.setValueAtTime(0.1, audioContext.currentTime)
    // connect the sequence's gain to our lowered gain
    sequenceGain.connect(loweredGain)
    // and connect the lower gain to the audioContext's destination
    loweredGain.connect(audioContext.destination)
    // now sequence's audio will be audible at 10% volume
  })

Objects

Objects in Theatre are usually made to represent actual visual elements on the screen, such as a <div>, or a THREE.js Object3D.

  • Calling sheet.object("box", {prop1: 0}) returns an object with:
    • a key of "box"
    • a single prop called prop1
      • whose type is a number (ie. cannot be a string or boolean, etc.)
      • whose default value is 0.

Reading the values of objects

There are multiple ways to read the values of an object

Static reads

You can read the latest value of an object through obj.value:

const obj = sheet.object("box", {x: 0})
console.log(obj.value.x) // prints 0 or the current overridden value of x

Using obj.onValuesChange()

The method obj.onValuesChange() allows you to react to the values of the object changing:

const obj = sheet.object("box", {x: 0, y: 0})
// calls the callback every time any of the values change
const unsubscribe = obj.onValuesChange((newValues) => {
  // note that this will print the value of `x` even if only `y` has changed
  console.log(newValues.x)
})

// stop listening after 5 seconds
setTimeout(unsubscribe, 5000)

Using obj.props

If you wish to listen to only a certain set of values changing, you can use obj.props as a pointer. More documentation on this coming soon.

Prop types

Each object has one or more props (just like a React component). Once you define your props, you can start tweaking them in the studio.

Read more about props the getting started guide or see how to use them in the video tutorial (opens new window).

In the examples below, the variable t comes from import {types as t} from '@theatre/core'.

  • Numbers
    • A floating point number with a default value of 0

      // shorthand notation
      const obj = sheet.object("box", {
        x: 0
      })
      
      // non-shorthand notation
      const obj = sheet.object("box", {
        x: t.number(0)
      })
      
    • A number with a range

      const obj = sheet.object("box", {
        x: t.number(0, {
          range: [0, 20]
        })
      })
      
    • A custom nudgeMultiplier is useful if you want to control how fast the number jumps as the user nudges it

      const obj = sheet.object("box", {
          x: t.number(0, {
            nudgeMultiplier: 0.25
          })
        })
      
    • Using a nudgeFn is useful if you want to fully customize the nudging behavior

      const x = t.number({
        nudgeFn: (
          // the mouse movement (in pixels)
          deltaX: number,
          // the movement as a fraction of the width of the number editor's input
          deltaFraction: number,
          // A multiplier that's usually 1, but might be another number if user wants to nudge slower/faster
          magnitude: number,
          // the configuration of the number
          config: {nudgeMultiplier?: number; range?: [number, number]},
        ): number => {
          // use any of the arguments to determine the correct nudging amount
          return deltaX * magnitude
        },
      })
      
      const obj = sheet.object("box", {x})
      
    • Strings

      TODO

    • Booleans

      TODO

    • Compounds

      TODO

    • String Literals

      TODO

Extensions

Read about extensions here.

Editor intellisense

The libraries come bundled with typescript definitions with TSDoc (opens new window) comments, so whether you're using JavaScript or TypeScript, you should be able to use autocomplete and API comments. If you're using JavaScript, consider adding some JSDoc (opens new window) comments here and there to further enhance the editing experience.

Note that Typescript is not required to use Theatre.js or see its API docs.


This documentation is a work in progress. In the meantime, check the video tutorial (opens new window) which covers much of Theatre.js.