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);Direct render or dispatched render
A small composition can render its typed scene exports directly, as above. Reusable
visual families should register renderers and let SceneRenderer dispatch instead —
otherwise every composition has to know every actor's markup.
Options
Prop
Type
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
requestAnimationFrameloop. 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
derivedso tests and other backends get it too.