Skip to content
MotionActor
Esc
↑↓navigate↵open⌘Jpreview
On this page

Rendering

Turning a compiled scene into pixels — SceneRenderer, defineSceneComposition, and the backend seam.

Rendering is deliberately dumb. A renderer resolves a frame, reads the state, and draws. It never owns a clock, never decides what comes next, and never writes authored state.

SceneRenderer

The general path. SceneRenderer resolves the frame, dispatches each resolved actor to its registered renderer, and memoizes by default.

<SceneRenderer compiled={compiled} frame={frame} width={1920} height={1080} />

Use it when actors have registered renderers — that is, for reusable families where the scene does not know in advance what it contains.

defineSceneComposition

The Remotion path. It supplies the render runtime, binds the Remotion host, derives duration from the compiled scene through calculateMetadata, and hands your render callback the resolved frame.

export const CardComposition = defineSceneComposition({
  id: "ActorCard",
  width: 1280,
  height: 720,
  fps: FPS,
  buildScene,
  render: ({ compiled, frame, width }) => {
    const { card, caption } = resolveScene(compiled, frame, width * 0.75).scene;
    if (!card || !caption) return <AbsoluteFill />;

    return (
      <AbsoluteFill className="items-center justify-center bg-slate-950 text-white">
        <div
          className="overflow-hidden rounded-xl bg-indigo-600 p-8 text-6xl whitespace-nowrap"
          style={{ width: card.revealWidth, opacity: card.progress }}
        >
          {caption.text}
        </div>
      </AbsoluteFill>
    );
  },
});

export function RemotionRoot() {
  return <Composition {...CardComposition.remotion} />;
}

registerRoot(RemotionRoot);

Options

PropType
id?string

Remotion composition id.

Typestring
width?number

Composition width in pixels.

Typenumber
height?number

Composition height in pixels.

Typenumber
fps?number

Must match the fps the scene was compiled with.

Typenumber
buildScene?(args) => CompiledScene

Builds the CompiledScene. Receives width, height and props.

Type(args) => CompiledScene
render?(args) => ReactElement

Draws one frame from the compiled scene, frame number and size.

Type(args) => ReactElement
defaultProps?P

Default Remotion props.

TypeP

Passing host values

A renderer knows things the scene cannot: the real width, a measured box, a decoded video frame. Those enter as channels — and the safest form is a per-resolution override, because it writes nothing:

export function resolveScene(compiled, frame: number, width: number) {
  const card = compiled.at(frame).scene.card;
  if (!card) return compiled.at(frame);

  return compiled.at(frame, {
    channelValues: new Map([[createCellKey(card.id, "availableWidth"), width]]),
  });
}

React may render the same frame twice. A publication would make the second render depend on the first; an override does not.

Backends

Package Surface
@motionactor/dom DOM/React — the release target
@motionactor/three Three.js
@motionactor/pixi Pixi layer host
@motionactor/skia Skia layer host

@motionactor/react currently re-exports the DOM backend, so importing either is equivalent.

What a renderer must not do

  • Hold a timer or a requestAnimationFrame loop. The frame number is an input.
  • Write authored state. Publish to a channel instead.
  • Compute presentation geometry the actor could have computed. Put it in a derived so tests and other backends get it too.

Was this page helpful?