---
title: Rendering
description: How a resolved frame becomes pixels, through renderers, backends, layers, compositions and the hosts that play them.
sidebar:
  hidden: true
mode: wide
---

A compiled scene holds no pictures. Rendering reads one resolved frame and draws it, and the
same code draws that frame in a browser preview, in an editor's scrubber and in an exported
video.

<DialsScene />

One scene and three layers. The scene spawns the same `Dial` class into each layer and tweens
all three with one call. Each layer belongs to a different backend: the left dial is HTML and
CSS, the middle one is Pixi vector strokes on a WebGL canvas, and the right one is a lit
Three.js mesh. `turn` is the only number any of them reads.

## The pieces

| Piece | What it does | Made with |
| --- | --- | --- |
| [Renderer](/docs/rendering/renderers) | Draws one actor class on one backend | `dom.render`, `pixi.render`, `three.render` |
| [Backend](/docs/rendering/backends-and-layers) | A drawing surface and the component that mounts it | `dom`, `pixi`, `three`, `createBackend` |
| Render runtime | The backends and renderers an app uses | `createRenderRuntime` |
| [Layer](/docs/rendering/backends-and-layers#layers) | A region of the stage painted by one backend | `layer(backend, { frame })` |
| [Effect](/docs/rendering/effects) | A treatment applied to an actor, drawn by its backend | `defineEffect`, `actor.fx`, `ctx.apply` |
| [Transition](/docs/rendering/transitions) | Morphs a view from before a change to after it | `transition`, `<Transition>`, `<Shared>` |
| [Composition](/docs/rendering/compositions) | Size, frame rate, runtime and scene, ready for a host | `defineComposition` |
| [Host](/docs/rendering/hosts) | Picks the frame and waits until it is ready to show | Remotion, HyperFrames, `mountComposition` |

## One frame, start to finish

1. A host picks a frame. Remotion reads its timeline, the native preview reads its clock, and
   the exporter takes the lowest frame no capture has claimed yet.
2. `SceneRenderer` resolves that frame from the compiled scene and sorts the actors into
   layers. Actors spawned with `ctx.spawn` go to the default layer.
3. Each layer mounts its backend's host component with the actors it paints: a tree of
   positioned elements for the DOM, a Pixi `Application`, a React Three Fiber canvas.
4. The host gives each actor to the first renderer that matches its class and draws what the
   renderer returns.
5. Fonts, images, video and GPU work hold the frame until they are ready. The host shows or
   captures the frame only after every hold is released.

## The dials

Each renderer names its backend and its class. The runtime routes each one to its backend's
layer, so the same `Dial` actor gets HTML in the DOM layer and a mesh in the Three layer.

```tsx twoslash title="dials.tsx"
// @jsx: react-jsx
// @filename: dials.scene.ts
import {
  createScene,
  layer,
  LayoutActor,
  signal,
  type Duration,
} from "@motionactor/core";
import { dom } from "@motionactor/dom";
import { pixi } from "@motionactor/pixi";
import { three } from "@motionactor/three";

export class Dial extends LayoutActor {
  turn = signal(0);

  *spin(to: number, duration: Duration) {
    yield* this.turn.tween(to, duration);
  }
}

const panel = (column: number) => ({
  frame: {
    x: 20 + column * 420,
    y: 20,
    width: 400,
    height: 440,
  },
});

const box = { x: 80, y: 100, width: 240, height: 240 };

export const dials = createScene(
  function* (ctx, _inputs, _viewport, layers) {
    const dials = [
      layers.dom.spawn(Dial, { layout: box }),
      layers.pixi.spawn(Dial, { layout: box }),
      layers.three.spawn(Dial, { layout: box }),
    ];

    yield* ctx.all(...dials.map((d) => d.spin(1, 1.5)));
    yield* ctx.wait(0.5);
    yield* ctx.all(...dials.map((d) => d.spin(0, 1)));

    return { dials };
  },
  {
    layers: {
      dom: layer(dom, panel(0)),
      pixi: layer(pixi, panel(1)),
      three: layer(three, panel(2)),
    },
  },
);
// @filename: dials.tsx
// ---cut---
import { createRenderRuntime, SceneRenderer } from "@motionactor/renderers/react";
import { dom } from "@motionactor/dom/react";
import { pixi } from "@motionactor/pixi/react";
import { three } from "@motionactor/three/react";
import { Dial, dials } from "./dials.scene";

const TAU = Math.PI * 2;

const domDial = dom.render(Dial, ({ actor }) => (
  <div className="ring">
    <div
      className="needle"
      style={{ rotate: `${actor.signals.turn}turn` }}
    />
  </div>
));

const pixiDial = pixi.render(Dial, ({ actor }) => {
  const r = actor.localLayout.width / 2;
  const angle = actor.signals.turn * TAU - Math.PI / 2;
  return (
    <pixiGraphics
      draw={(g) => {
        g.clear().circle(r, r, r - 3).stroke({ width: 6, color: 0x413c39 });
        g.moveTo(r, r)
          .lineTo(r + Math.cos(angle) * r, r + Math.sin(angle) * r)
          .stroke({ width: 6, color: 0xf4f1ef });
      }}
    />
  );
});

const threeDial = three.render(Dial, ({ actor }) => (
  <mesh rotation={[0, actor.signals.turn * TAU, 0]}>
    <torusKnotGeometry args={[1, 0.32, 160, 24]} />
    <meshStandardMaterial color="#f86e42" />
  </mesh>
));

const runtime = createRenderRuntime({
  backends: [dom, pixi, three],
  renderers: [domDial, pixiDial, threeDial],
});

const compiled = dials.build({ viewport: { width: 1280, height: 480 }, fps: 30 });

export const Frame = ({ frame }: { frame: number }) => (
  <SceneRenderer
    compiled={compiled}
    frame={frame}
    width={1280}
    height={480}
    runtime={runtime}
  />
);
```

The DOM and Pixi backends place each dial's 240 pixel box from its layout before the renderer
runs, so those renderers draw in local coordinates. The Three backend leaves placement to its
renderers, and the mesh sits at the origin of its layer's camera.

## What a renderer may do

A renderer reads. It gets the resolved actor (signals, layout, effects, slots, channel values)
along with the frame state, the frame number and the frame rate. Everything else is off limits:

- It keeps no clock. The frame number arrives as a prop, so timers, `requestAnimationFrame`
  and `Date.now()` have no place in render code. The same frame draws the same picture.
- It never writes authored state. A value only the renderer can know, such as a measured
  width, goes back into the scene through a [channel](/docs/reactivity/channel#publishing-from-a-renderer):
  the component the renderer returns calls
  `useChannelPublication(actor.channels.width, produce)`.
- It draws content, not placement, on the DOM and Pixi backends. Position, size, rotation,
  opacity and clipping come from the actor's state and are applied around what it returns.

**[Renderers](/docs/rendering/renderers)**

Matching, the render context, slots and bindings.

**[Backends and layers](/docs/rendering/backends-and-layers)**

DOM, Pixi, Three.js and Skia, and how a stage splits between them.

**[Compositions](/docs/rendering/compositions)**

Packaging a scene for any host.

**[Capture and export](/docs/rendering/capture-and-export)**

Stills, video files and headless renders.
