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

Backends and layers

The DOM, Pixi, Three.js and Skia backends, and how a scene splits its stage between them.

A backend is a drawing surface: the component that mounts it, the renderers built into it and the effect kinds it can paint. A layer is a rectangle of the stage that one backend paints. A scene declares its layers, and a render runtime supplies a backend for each one.

The backends

Backend Import Mounts Places each actor Effect kinds
DOM dom from @motionactor/dom/react A positioned element per actor Yes domStyle, glShader, canvasPaint
Pixi pixi from @motionactor/pixi/react A Pixi Application with a transparent background Yes pixiFilter
Three.js three from @motionactor/three/react A React Three Fiber canvas No threePass, threeMaterial
Skia createSkiaBackend from @motionactor/skia/react A Skia canvas you supply No None yet

“Places each actor” means the backend applies position, size, scale, rotation, opacity and clipping from the actor’s state, so its renderers draw in the actor’s local box. On Three.js and Skia a renderer places what it draws itself. For another drawing surface, see Writing a backend.

None of the backends runs a clock. Pixi is created with autoStart: false and React Three Fiber with frameloop="never". Each draws when the frame changes, then tells the host the frame is on screen. Three.js sets its clock to frame / fps before it advances, so seeking backwards or repeating a frame gives the same picture.

Each backend also has a ref, a small React-free value under the same name, exported from the package root beside the backend’s actor classes and effect kinds. The React backend, renderers and views live at /react. Scene files import the root to declare layers and effects, so a scene never pulls a renderer or a canvas into its module graph. A backend is accepted wherever its ref is, so renderer files hold just the backend:

import { const dom: BackendRef<"dom">
The DOM backend's identity, at its own framework-free entry (`@motionactor/renderers/dom-ref`). It lives here and not in `@motionactor/dom`: DOM renderers are written in packages the DOM host depends on (`@motionactor/primitives`, `@motionactor/media`), and the contract already treats DOM specially (only DOM renderers bind `refs`). It is not on the floor's root, so no binding entry re-exports a `dom` that is not the backend: `@motionactor/dom` re-exports it as the ref, and its `/react`, `/vue` and `/svelte` entries export their backends as `dom`.
dom
} from "@motionactor/dom";
import { const pixi: BackendRef<"pixi">
The Pixi backend's identity, and the only place its name is spelled. This entry pulls in no React, no Pixi and no host, so a scene file, an effect or a renderer package can name the backend without mounting it.
pixi
} from "@motionactor/pixi";
import { const three: BackendRef<"three">
The Three backend's identity, and the only place its name is spelled. This entry pulls in no React, no Three and no host, so a scene file, an effect or a renderer package can name the backend without mounting it.
three
} from "@motionactor/three";
import { const skia: BackendRef<"skia">
The Skia backend's identity, and the only place its name is spelled. This entry pulls in no React, no Skia and no host, so a scene file or a renderer package can name the backend without mounting it.
skia
} from "@motionactor/skia";
[const dom: BackendRef<"dom">
The DOM backend's identity, at its own framework-free entry (`@motionactor/renderers/dom-ref`). It lives here and not in `@motionactor/dom`: DOM renderers are written in packages the DOM host depends on (`@motionactor/primitives`, `@motionactor/media`), and the contract already treats DOM specially (only DOM renderers bind `refs`). It is not on the floor's root, so no binding entry re-exports a `dom` that is not the backend: `@motionactor/dom` re-exports it as the ref, and its `/react`, `/vue` and `/svelte` entries export their backends as `dom`.
dom
.BackendRef<"dom">.name: "dom"name, const pixi: BackendRef<"pixi">
The Pixi backend's identity, and the only place its name is spelled. This entry pulls in no React, no Pixi and no host, so a scene file, an effect or a renderer package can name the backend without mounting it.
pixi
.BackendRef<"pixi">.name: "pixi"name, const three: BackendRef<"three">
The Three backend's identity, and the only place its name is spelled. This entry pulls in no React, no Three and no host, so a scene file, an effect or a renderer package can name the backend without mounting it.
three
.BackendRef<"three">.name: "three"name, const skia: BackendRef<"skia">
The Skia backend's identity, and the only place its name is spelled. This entry pulls in no React, no Skia and no host, so a scene file or a renderer package can name the backend without mounting it.
skia
.BackendRef<"skia">.name: "skia"name];
// ["dom", "pixi", "three", "skia"]

The render runtime

createRenderRuntime takes the backends an app uses and the renderers it adds. The first backend is the default unless default names another.

import { const createRenderRuntime: (input: CreateRenderRuntimeInput<ReactBackendHost, React.ReactNode>) => ReactRenderRuntime
The 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
} 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"; export const const runtime: ReactRenderRuntimeruntime = function createRenderRuntime(input: CreateRenderRuntimeInput<ReactBackendHost, React.ReactNode>): ReactRenderRuntime
The 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[] | undefined
Family 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 runtime: ReactRenderRuntimeruntime.defaultBackend: BackendRef<string>
`default`, or the first backend.
defaultBackend
.BackendRef<string>.name: stringname; // "dom"
const runtime: ReactRenderRuntimeruntime.function hasBackend(backend: BackendRef): boolean
Whether this runtime has `backend`.
hasBackend
(const three: Backend<ReactBackendHost, "three">three); // true

It throws when it is given no backends, two backends with one name, a default it does not have, or a renderer for a backend it does not have.

Layers

A scene without layers paints everything into the default layer, which covers the whole stage and uses the runtime’s default backend. Declare layers to split the stage. Each one is layer(backend, { frame }), with the frame in stage pixels; leave the frame out for the full stage.

import { function createScene<TSceneExports, const I extends SceneInputs = Readonly<Record<never, SceneInput>>, const L extends SceneLayers = Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<I, L, TSceneExports>, options?: SceneDefinitionOptions<I, L>): SceneDefinition<I, L, TSceneExports>
Declare a scene. Nothing compiles until a host builds it: `scene.build({ viewport, fps, inputs })` runs the script as `(ctx, inputs, viewport, layers)` and returns the compiled scene. `ctx.spawn` places a root actor in the default layer; `layers.<key>.spawn` places it in a declared one. `ctx.stage` stages another scene on this scene's clock, and a staged scene may stage again.
createScene
, const layer: <TBackend extends BackendRef>(backend: TBackend, options?: SceneLayerOptions) => SceneLayer<TBackend>
Declare a scene layer painted by `backend`: `layer(three, { frame })`, `layer(dom)`.
layer
, class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor,
function signal<T>(): RequiredSignal<T> (+1 overload)
module signal
A timeline value on an actor, and an input to every spawn of it. Without an initial value the signal is required: `ctx.spawn` must supply it.
signal
} from "@motionactor/core";
import { const dom: BackendRef<"dom">
The DOM backend's identity, at its own framework-free entry (`@motionactor/renderers/dom-ref`). It lives here and not in `@motionactor/dom`: DOM renderers are written in packages the DOM host depends on (`@motionactor/primitives`, `@motionactor/media`), and the contract already treats DOM specially (only DOM renderers bind `refs`). It is not on the floor's root, so no binding entry re-exports a `dom` that is not the backend: `@motionactor/dom` re-exports it as the ref, and its `/react`, `/vue` and `/svelte` entries export their backends as `dom`.
dom
} from "@motionactor/dom";
import { const three: BackendRef<"three">
The Three backend's identity, and the only place its name is spelled. This entry pulls in no React, no Three and no host, so a scene file, an effect or a renderer package can name the backend without mounting it.
three
} from "@motionactor/three";
class class GlobeGlobe extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor { Globe.spin: ActorFieldSignal<number>spin = signal<number>(initial: number): ActorFieldSignal<number> (+1 overload)
A timeline value on an actor, and an input to every spawn of it. Without an initial value the signal is required: `ctx.spawn` must supply it.
signal
(0);
} class class CaptionCaption extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor { Caption.text: ActorFieldSignal<string>text = signal<string>(initial: string): ActorFieldSignal<string> (+1 overload)
A timeline value on an actor, and an input to every spawn of it. Without an initial value the signal is required: `ctx.spawn` must supply it.
signal
("");
} export const
const titleCard: SceneDefinition<Readonly<Record<never, SceneInput>>, {
    readonly stage: SceneLayer<BackendRef<"three">>;
    readonly overlay: SceneLayer<BackendRef<"dom">>;
}, {
    globe: Globe;
}>
titleCard
=
createScene<{
    globe: Globe;
}, Readonly<Record<never, SceneInput>>, {
    readonly stage: SceneLayer<BackendRef<"three">>;
    readonly overlay: SceneLayer<BackendRef<"dom">>;
}>(script: SceneScript<Readonly<Record<never, SceneInput>>, {
    readonly stage: SceneLayer<BackendRef<"three">>;
    readonly overlay: SceneLayer<BackendRef<"dom">>;
}, {
    globe: Globe;
}>, options?: SceneDefinitionOptions<Readonly<Record<...>>, {
    readonly stage: SceneLayer<BackendRef<"three">>;
    readonly overlay: SceneLayer<BackendRef<"dom">>;
}> | undefined): SceneDefinition<...>
Declare a scene. Nothing compiles until a host builds it: `scene.build({ viewport, fps, inputs })` runs the script as `(ctx, inputs, viewport, layers)` and returns the compiled scene. `ctx.spawn` places a root actor in the default layer; `layers.<key>.spawn` places it in a declared one. `ctx.stage` stages another scene on this scene's clock, and a staged scene may stage again.
createScene
(
function* (ctx: SceneContextctx, _inputs: SceneInputValues<Readonly<Record<never, SceneInput>>>_inputs, viewport: SceneViewportviewport, { stage: LayerHandlestage, overlay: LayerHandleoverlay }) { const const globe: Globeglobe = stage: LayerHandlestage.
LayerHandle.spawn: <Globe>(definition: ActorClass<Globe>, overrides?: {
    measuredBox?: undefined;
    spin?: SignalInputValue<number> | undefined;
    layout?: {
        x?: SignalInputValue<LayoutValue> | undefined;
        y?: SignalInputValue<LayoutValue> | undefined;
        width?: Readonly<{
            kind: "layout.percent";
            value: number;
        }> | SignalInputValue<number | `${number}%` | Readonly<{
            kind: "layout.percent";
            value: number;
        }>> | undefined;
        height?: Readonly<...> | SignalInputValue<...> | undefined;
    } | undefined;
    ... 20 more ...;
    parent?: undefined;
} | undefined, options?: {
    ...;
} | undefined) => Globe (+1 overload)
spawn
(class GlobeGlobe);
overlay: LayerHandleoverlay.
LayerHandle.spawn: <Caption>(definition: ActorClass<Caption>, overrides?: {
    measuredBox?: undefined;
    layout?: {
        x?: SignalInputValue<LayoutValue> | undefined;
        y?: SignalInputValue<LayoutValue> | undefined;
        width?: Readonly<{
            kind: "layout.percent";
            value: number;
        }> | SignalInputValue<number | `${number}%` | Readonly<{
            kind: "layout.percent";
            value: number;
        }>> | undefined;
        height?: Readonly<{
            kind: "layout.percent";
            value: number;
        }> | SignalInputValue<...> | undefined;
    } | undefined;
    ... 21 more ...;
    text?: SignalInputValue<...> | undefined;
} | undefined, options?: {
    ...;
} | undefined) => Caption (+1 overload)
spawn
(class CaptionCaption, {
text?: SignalInputValue<string> | undefinedtext: "Launch day",
layout?: {
    x?: SignalInputValue<LayoutValue> | undefined;
    y?: SignalInputValue<LayoutValue> | undefined;
    width?: Readonly<{
        kind: "layout.percent";
        value: number;
    }> | SignalInputValue<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>> | undefined;
    height?: Readonly<{
        kind: "layout.percent";
        value: number;
    }> | SignalInputValue<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>> | undefined;
} | undefined
layout
: { x?: SignalInputValue<LayoutValue> | undefinedx: 0, y?: SignalInputValue<LayoutValue> | undefinedy: viewport: SceneViewportviewport.SceneViewport.height: numberheight - 160 },
}); yield* const globe: Globeglobe.Globe.spin: ActorFieldSignal<number>spin.LiveSignal<number>.tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>
Tween to target over authored seconds — must yield*
tween
(1, 3);
return { globe: Globeglobe }; }, {
SceneDefinitionOptions<Readonly<Record<never, SceneInput>>, { readonly stage: SceneLayer<BackendRef<"three">>; readonly overlay: SceneLayer<BackendRef<"dom">>; }>.layers?: {
    readonly stage: SceneLayer<BackendRef<"three">>;
    readonly overlay: SceneLayer<BackendRef<"dom">>;
} | undefined
The scene's layers, in paint order above the default layer. Each is painted by one backend; the script spawns into one through its handle.
layers
: {
stage: SceneLayer<BackendRef<"three">>stage: layer<BackendRef<"three">>(backend: BackendRef<"three">, options?: SceneLayerOptions): SceneLayer<BackendRef<"three">>
Declare a scene layer painted by `backend`: `layer(three, { frame })`, `layer(dom)`.
layer
(const three: BackendRef<"three">
The Three backend's identity, and the only place its name is spelled. This entry pulls in no React, no Three and no host, so a scene file, an effect or a renderer package can name the backend without mounting it.
three
, {
SceneLayerOptions.frame?: Rect | undefined
The layer's rectangle in stage pixels. Omit it for the full stage.
frame
: { Rect.x: numberx: 320, Rect.y: numbery: 0, Rect.width: numberwidth: 1280, Rect.height: numberheight: 1080 },
}), overlay: SceneLayer<BackendRef<"dom">>overlay: layer<BackendRef<"dom">>(backend: BackendRef<"dom">, options?: SceneLayerOptions): SceneLayer<BackendRef<"dom">>
Declare a scene layer painted by `backend`: `layer(three, { frame })`, `layer(dom)`.
layer
(const dom: BackendRef<"dom">
The DOM backend's identity, at its own framework-free entry (`@motionactor/renderers/dom-ref`). It lives here and not in `@motionactor/dom`: DOM renderers are written in packages the DOM host depends on (`@motionactor/primitives`, `@motionactor/media`), and the contract already treats DOM specially (only DOM renderers bind `refs`). It is not on the floor's root, so no binding entry re-exports a `dom` that is not the backend: `@motionactor/dom` re-exports it as the ref, and its `/react`, `/vue` and `/svelte` entries export their backends as `dom`.
dom
),
}, }, ); const
const compiled: CompiledScene<{
    globe: Globe;
}, CompiledLayersOf<{
    readonly stage: SceneLayer<BackendRef<"three">>;
    readonly overlay: SceneLayer<BackendRef<"dom">>;
}>>
compiled
=
const titleCard: SceneDefinition<Readonly<Record<never, SceneInput>>, {
    readonly stage: SceneLayer<BackendRef<"three">>;
    readonly overlay: SceneLayer<BackendRef<"dom">>;
}, {
    globe: Globe;
}>
titleCard
.
SceneDefinition<Readonly<Record<never, SceneInput>>, { readonly stage: SceneLayer<BackendRef<"three">>; readonly overlay: SceneLayer<BackendRef<"dom">>; }, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
    globe: Globe;
}, CompiledLayersOf<{
    readonly stage: SceneLayer<BackendRef<"three">>;
    readonly overlay: SceneLayer<BackendRef<"dom">>;
}>>
build
({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });
const compiled: CompiledScene<{
    globe: Globe;
}, CompiledLayersOf<{
    readonly stage: SceneLayer<BackendRef<"three">>;
    readonly overlay: SceneLayer<BackendRef<"dom">>;
}>>
compiled
.
CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
    globe: Globe;
}>
at
(0).FrameState<{ ...; }>.layers: readonly ResolvedLayer[]
The frame's layers in paint order: the default layer first, then the declared layers of each scene on stage, the deepest staged scene's first, so a stager paints above what it stages; one scene's in declaration order.
layers
.ReadonlyArray<ResolvedLayer>.map<"default" | "declared">(callbackfn: (value: ResolvedLayer, index: number, array: readonly ResolvedLayer[]) => "default" | "declared", thisArg?: any): ("default" | "declared")[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map
((layer: ResolvedLayerlayer) => layer: ResolvedLayerlayer.kind: "default" | "declared"
The layer `ctx.spawn` places into: the full stage, painted below the declared layers by the render runtime's default backend. Core does not know the runtime or the stage size, so it names neither. A layer a scene on stage declares in `createScene(…, { layers })`.
kind
);
// ["default", "declared", "declared"]

The script’s fourth parameter holds one handle per declared layer, typed from the declaration. handle.spawn takes the same arguments as ctx.spawn and places a root actor in that layer; children paint in their root’s layer. Coordinates are local to the layer, so x: 0 in stage is 320 pixels from the stage’s left edge.

A layer’s frame resolves against the viewport the scene is built with, so a new stage size is a new build.

Paint order

  1. Layers: the default layer first, then the declared layers in the order they are written. A layer key that is an integer would break that order, so it throws. While a scene is staged, its layers paint below its stager’s: the deepest staged scene’s first, the built scene’s last. A background therefore goes in the default layer. Each layer’s id is its key, or key#2, key#3 when an earlier scene in the build declared the key; an actor’s layer and the layer’s mount key are that id.
  2. Root actors within a layer, in spawn order.
  3. Children within an actor, in child order. place.reorder([...]) changes it from a given frame on.

There is no z-index. To bring an actor forward, reorder its slot. Adding a layer is the wrong fix, since a layer is a separate canvas or DOM root.

What a layer will paint

SceneRenderer checks every root before mounting any layer:

The root, or anything below it, has Result
A renderer on the layer’s backend, or a class render method on a DOM layer Painted
No renderer on any backend Skipped as logical, such as a clock or a camera
Renderers only on other backends Throws, naming the actor, the layer and the backend

A declared layer whose backend the runtime lacks also throws. An editor can ask the same question without rendering through classifyLayerPlacement from @motionactor/renderers/react.

Each layer renders as an absolutely positioned box at its frame with overflow: hidden and a data-layer attribute holding its id. The scene camera applies inside each layer, and the backend’s host mounts inside that box at the layer’s size.

Was this page helpful?