Rendering
How a resolved frame becomes pixels, through renderers, backends, layers, compositions and the hosts that play them.
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.
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 | Draws one actor class on one backend | dom.render, pixi.render, three.render |
| Backend | A drawing surface and the component that mounts it | dom, pixi, three, createBackend |
| Render runtime | The backends and renderers an app uses | createRenderRuntime |
| Layer | A region of the stage painted by one backend | layer(backend, { frame }) |
| Effect | A treatment applied to an actor, drawn by its backend | defineEffect, actor.fx, ctx.apply |
| Transition | Morphs a view from before a change to after it | transition, <Transition>, <Shared> |
| Composition | Size, frame rate, runtime and scene, ready for a host | defineComposition |
| Host | Picks the frame and waits until it is ready to show | Remotion, HyperFrames, mountComposition |
One frame, start to finish
- 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.
SceneRendererresolves that frame from the compiled scene and sorts the actors into layers. Actors spawned withctx.spawngo to the default layer.- 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. - The host gives each actor to the first renderer that matches its class and draws what the renderer returns.
- 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.
import { const createRenderRuntime: (input: CreateRenderRuntimeInput<ReactBackendHost, React.ReactNode>) => ReactRenderRuntimeThe floor's `createRenderRuntime` for React: every backend's host is a
React component and its stages wrap React nodes, so mixed backends (each
with its own host props) compose without naming a type.createRenderRuntime, const SceneRenderer: <TSceneExports>({ compiled, frame, width, height, sceneMeta, style, children, runtime, }: {
compiled: CompiledScene<TSceneExports>;
frame: number;
width: number;
height: number;
sceneMeta?: unknown;
style?: React.CSSProperties;
children?: React.ReactNode;
runtime?: ReactRenderRuntime;
}) => JSX.Element
Frame-passive renderer for a CompiledScene: resolves
the frame and paints its layers (`SceneLayers`) on a stage of `width` by
`height`. Each layer is painted by the host its backend registers on the
runtime; the default layer by the runtime's default backend. `children` are
page markup: they render in the scope of the scene's DOM layer, where
`<actor.h1 of={compiled.scene.card.title}>` binds a node of it to an actor
(`SceneLayers`).SceneRenderer } from "@motionactor/renderers/react";
import { const dom: StagedBackend<FC<ReactBackendHostProps>, "dom", ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom } from "@motionactor/dom/react";
import { const pixi: StagedBackend<FC<StagedHostProps<ReactNode>>, "pixi", ReactNode>The Pixi backend. It pairs `pixiFilter` with its React stage. A
scene-applied effect carries its definition; a wire id resolves from the
effect registry, where importing an effect's module installs it.pixi } from "@motionactor/pixi/react";
import { const three: Backend<ReactBackendHost, "three">three } from "@motionactor/three/react";
import { class DialDial, const dials: SceneDefinition<Readonly<Record<never, SceneInput>>, {
readonly dom: SceneLayer<BackendRef<"dom">>;
readonly pixi: SceneLayer<BackendRef<"pixi">>;
readonly three: SceneLayer<BackendRef<"three">>;
}, {
dials: Dial[];
}>
dials } from "./dials.scene";
const const TAU: numberTAU = var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.Math.Math.PI: numberPi. This is the ratio of the circumference of a circle to its diameter.PI * 2;
const const domDial: BackendRendererDefinition<readonly [typeof Dial], "dom">domDial = const dom: StagedBackend<FC<ReactBackendHostProps>, "dom", ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom.render: <typeof Dial>(blueprint: typeof Dial, render: ActorRendererForBlueprints<readonly [typeof Dial], "dom">, options?: ActorRendererRegistrationOptions) => BackendRendererDefinition<readonly [typeof Dial], "dom"> (+1 overload)render(class DialDial, ({ actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | `${number}%` | Readonly<...>;
x: LayoutValue;
y: LayoutValue;
};
... 6 more ...;
turn: number;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor }) => (
<JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>div HTMLAttributes<HTMLDivElement>.className?: string | undefinedclassName="ring">
<JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>div
HTMLAttributes<HTMLDivElement>.className?: string | undefinedclassName="needle"
HTMLAttributes<HTMLDivElement>.style?: CSSProperties | undefinedstyle={{ StandardLonghandProperties<string | number, string & {}>.rotate?: Property.Rotate | undefinedThis feature is well established and works across many devices and browser versions. It’s been available across browsers since August 2022.
**Syntax**: `none | <angle> | [ x | y | z | <number>{3} ] && <angle>`
**Initial value**: `none`
| Chrome | Firefox | Safari | Edge | IE |
| :-----: | :-----: | :------: | :-----: | :-: |
| **104** | **72** | **14.1** | **104** | No |rotate: `${actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | `${number}%` | Readonly<...>;
x: LayoutValue;
y: LayoutValue;
};
... 6 more ...;
turn: number;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor.signals: Readonly<Record<string, unknown>> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | `${number}%` | Readonly<...>;
x: LayoutValue;
y: LayoutValue;
};
... 6 more ...;
turn: number;
}
signals.turn: numberturn}turn` }}
/>
</JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>
));
const const pixiDial: BackendRendererDefinition<readonly [typeof Dial], "pixi">pixiDial = const pixi: StagedBackend<FC<StagedHostProps<ReactNode>>, "pixi", ReactNode>The Pixi backend. It pairs `pixiFilter` with its React stage. A
scene-applied effect carries its definition; a wire id resolves from the
effect registry, where importing an effect's module installs it.pixi.render: <typeof Dial>(blueprint: typeof Dial, render: ActorRendererForBlueprints<readonly [typeof Dial], "pixi">, options?: ActorRendererRegistrationOptions) => BackendRendererDefinition<readonly [typeof Dial], "pixi"> (+1 overload)render(class DialDial, ({ actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | `${number}%` | Readonly<...>;
x: LayoutValue;
y: LayoutValue;
};
... 6 more ...;
turn: number;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor }) => {
const const r: numberr = actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | `${number}%` | Readonly<...>;
x: LayoutValue;
y: LayoutValue;
};
... 6 more ...;
turn: number;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor.ResolvedActor.localLayout: Readonly<Rect>localLayout.width: numberwidth / 2;
const const angle: numberangle = actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | `${number}%` | Readonly<...>;
x: LayoutValue;
y: LayoutValue;
};
... 6 more ...;
turn: number;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor.signals: Readonly<Record<string, unknown>> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | `${number}%` | Readonly<...>;
x: LayoutValue;
y: LayoutValue;
};
... 6 more ...;
turn: number;
}
signals.turn: numberturn * const TAU: numberTAU - var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.Math.Math.PI: numberPi. This is the ratio of the circumference of a circle to its diameter.PI / 2;
return (
<pixiGraphics: PixiReactElementProps<typeof Graphics>pixiGraphics
draw: DrawCallbackdraw={(g: Graphicsg) => {
g: Graphicsg.Graphics.clear(): GraphicsClears all drawing commands from the graphics context, effectively resetting it.
This includes clearing the current path, fill style, stroke style, and transformations.
> [!NOTE] Graphics objects are not designed to be continuously cleared and redrawn.
> Instead, they are intended to be used for static or semi-static graphics that
> can be redrawn as needed. Frequent clearing and redrawing may lead to performance issues.clear().Graphics.circle(x: number, y: number, radius: number): GraphicsDraws a circle shape at the specified location with the given radius.circle(const r: numberr, const r: numberr, const r: numberr - 3).Graphics.stroke(style?: StrokeInput | undefined): GraphicsStrokes the current path with the current stroke style or specified style.
Outlines the shape using the stroke settings.stroke({ StrokeAttributes.width?: number | undefinedThe width of the stroke in pixels.width: 6, FillStyle.color?: ColorSource | undefinedThe color to use for the fill.
This can be any valid color source, such as a hex value, a Color object, or a string.color: 0x413c39 });
g: Graphicsg.Graphics.moveTo(x: number, y: number): GraphicsSets the starting point for a new sub-path.
Moves the "pen" to a new location without drawing a line.
Any subsequent drawing commands will start from this point.moveTo(const r: numberr, const r: numberr)
.Graphics.lineTo(x: number, y: number): GraphicsConnects the current point to a new point with a straight line.
Any subsequent drawing commands will start from this new point.lineTo(const r: numberr + var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.Math.Math.cos(x: number): numberReturns the cosine of a number.cos(const angle: numberangle) * const r: numberr, const r: numberr + var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.Math.Math.sin(x: number): numberReturns the sine of a number.sin(const angle: numberangle) * const r: numberr)
.Graphics.stroke(style?: StrokeInput | undefined): GraphicsStrokes the current path with the current stroke style or specified style.
Outlines the shape using the stroke settings.stroke({ StrokeAttributes.width?: number | undefinedThe width of the stroke in pixels.width: 6, FillStyle.color?: ColorSource | undefinedThe color to use for the fill.
This can be any valid color source, such as a hex value, a Color object, or a string.color: 0xf4f1ef });
}}
/>
);
});
const const threeDial: BackendRendererDefinition<readonly [typeof Dial], "three">threeDial = const three: Backend<ReactBackendHost, "three">three.render: <typeof Dial>(blueprint: typeof Dial, render: ActorRendererForBlueprints<readonly [typeof Dial], "three">, options?: ActorRendererRegistrationOptions) => BackendRendererDefinition<readonly [typeof Dial], "three"> (+1 overload)render(class DialDial, ({ actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | `${number}%` | Readonly<...>;
x: LayoutValue;
y: LayoutValue;
};
... 6 more ...;
turn: number;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor }) => (
<mesh: Mutable<Overwrite<Partial<Overwrite<Mesh<BufferGeometry<NormalBufferAttributes, BufferGeometryEventMap>, Material<MaterialEventMap> | Material<MaterialEventMap>[], Object3DEventMap>, MathProps<Mesh<BufferGeometry<NormalBufferAttributes, BufferGeometryEventMap>, Material<MaterialEventMap> | Material<MaterialEventMap>[], Object3DEventMap>> & ReactProps<...> & Partial<...>>>, Omit<...>>>mesh rotation?: Euler | [x: number, y: number, z: number, order?: EulerOrder | undefined] | Readonly<number | Euler | [x: number, y: number, z: number, order?: EulerOrder | undefined] | undefined>Object's local rotation (
{@link
https://en.wikipedia.org/wiki/Euler_angles Euler angles
}
), in radians.rotation={[0, actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | `${number}%` | Readonly<...>;
x: LayoutValue;
y: LayoutValue;
};
... 6 more ...;
turn: number;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor.signals: Readonly<Record<string, unknown>> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | `${number}%` | Readonly<...>;
x: LayoutValue;
y: LayoutValue;
};
... 6 more ...;
turn: number;
}
signals.turn: numberturn * const TAU: numberTAU, 0]}>
<torusKnotGeometry: Mutable<Overwrite<Partial<Overwrite<TorusKnotGeometry, MathProps<TorusKnotGeometry> & ReactProps<TorusKnotGeometry>>>, Omit<InstanceProps<TorusKnotGeometry, typeof TorusKnotGeometry>, "object">>>torusKnotGeometry args?: [radius?: number | undefined, tube?: number | undefined, tubularSegments?: number | undefined, radialSegments?: number | undefined, p?: number | undefined, q?: number | undefined] | Readonly<[radius?: number | undefined, tube?: number | undefined, tubularSegments?: number | undefined, radialSegments?: number | undefined, p?: number | undefined, q?: number | undefined] | undefined>args={[1, 0.32, 160, 24]} />
<meshStandardMaterial: Mutable<Overwrite<Partial<Overwrite<MeshStandardMaterial, MathProps<MeshStandardMaterial> & ReactProps<MeshStandardMaterial>>>, Omit<InstanceProps<MeshStandardMaterial, typeof MeshStandardMaterial>, "object">>>meshStandardMaterial color?: string | number | Color | [r: number, g: number, b: number] | [color: ColorRepresentation] | Readonly<Color> | readonly [r: number, g: number, b: number] | readonly [color: ColorRepresentation] | undefinedColor of the material.color="#f86e42" />
</mesh: Mutable<Overwrite<Partial<Overwrite<Mesh<BufferGeometry<NormalBufferAttributes, BufferGeometryEventMap>, Material<MaterialEventMap> | Material<MaterialEventMap>[], Object3DEventMap>, MathProps<Mesh<BufferGeometry<NormalBufferAttributes, BufferGeometryEventMap>, Material<MaterialEventMap> | Material<MaterialEventMap>[], Object3DEventMap>> & ReactProps<...> & Partial<...>>>, Omit<...>>>mesh>
));
const const runtime: ReactRenderRuntimeruntime = function createRenderRuntime(input: CreateRenderRuntimeInput<ReactBackendHost, React.ReactNode>): ReactRenderRuntimeThe floor's `createRenderRuntime` for React: every backend's host is a
React component and its stages wrap React nodes, so mixed backends (each
with its own host props) compose without naming a type.createRenderRuntime({
backends: readonly AnyBackend<ReactBackendHost, ReactNode>[]backends: [const dom: StagedBackend<FC<ReactBackendHostProps>, "dom", ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom, const pixi: StagedBackend<FC<StagedHostProps<ReactNode>>, "pixi", ReactNode>The Pixi backend. It pairs `pixiFilter` with its React stage. A
scene-applied effect carries its definition; a wire id resolves from the
effect registry, where importing an effect's module installs it.pixi, const three: Backend<ReactBackendHost, "three">three],
renderers?: readonly RendererRegistryEntry[] | undefinedFamily and project renderers, for any backend. Each routes to the backend it
names (`three.render(…)`); one naming a backend missing from
`backends` throws.renderers: [const domDial: BackendRendererDefinition<readonly [typeof Dial], "dom">domDial, const pixiDial: BackendRendererDefinition<readonly [typeof Dial], "pixi">pixiDial, const threeDial: BackendRendererDefinition<readonly [typeof Dial], "three">threeDial],
});
const const compiled: CompiledScene<{
dials: Dial[];
}, CompiledLayersOf<{
readonly dom: SceneLayer<BackendRef<"dom">>;
readonly pixi: SceneLayer<BackendRef<"pixi">>;
readonly three: SceneLayer<BackendRef<"three">>;
}>>
compiled = const dials: SceneDefinition<Readonly<Record<never, SceneInput>>, {
readonly dom: SceneLayer<BackendRef<"dom">>;
readonly pixi: SceneLayer<BackendRef<"pixi">>;
readonly three: SceneLayer<BackendRef<"three">>;
}, {
dials: Dial[];
}>
dials.SceneDefinition<Readonly<Record<never, SceneInput>>, { readonly dom: SceneLayer<BackendRef<"dom">>; readonly pixi: SceneLayer<BackendRef<"pixi">>; readonly three: SceneLayer<...>; }, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
dials: Dial[];
}, CompiledLayersOf<{
readonly dom: SceneLayer<BackendRef<"dom">>;
readonly pixi: SceneLayer<BackendRef<"pixi">>;
readonly three: SceneLayer<BackendRef<"three">>;
}>>
build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1280, SceneViewport.height: numberheight: 480 }, fps: numberfps: 30 });
export const const Frame: ({ frame }: {
frame: number;
}) => JSX.Element
Frame = ({ frame: numberframe }: { frame: numberframe: number }) => (
<const SceneRenderer: <TSceneExports>({ compiled, frame, width, height, sceneMeta, style, children, runtime, }: {
compiled: CompiledScene<TSceneExports>;
frame: number;
width: number;
height: number;
sceneMeta?: unknown;
style?: React.CSSProperties;
children?: React.ReactNode;
runtime?: ReactRenderRuntime;
}) => JSX.Element
Frame-passive renderer for a CompiledScene: resolves
the frame and paints its layers (`SceneLayers`) on a stage of `width` by
`height`. Each layer is painted by the host its backend registers on the
runtime; the default layer by the runtime's default backend. `children` are
page markup: they render in the scope of the scene's DOM layer, where
`<actor.h1 of={compiled.scene.card.title}>` binds a node of it to an actor
(`SceneLayers`).SceneRenderer
compiled: CompiledScene<{
dials: Dial[];
}, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>
compiled={const compiled: CompiledScene<{
dials: Dial[];
}, CompiledLayersOf<{
readonly dom: SceneLayer<BackendRef<"dom">>;
readonly pixi: SceneLayer<BackendRef<"pixi">>;
readonly three: SceneLayer<BackendRef<"three">>;
}>>
compiled}
frame: numberframe={frame: numberframe}
width: numberwidth={1280}
height: numberheight={480}
runtime?: ReactRenderRuntime | undefinedruntime={const runtime: ReactRenderRuntimeruntime}
/>
);
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,
requestAnimationFrameandDate.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:
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.