Compositions
A composition packages a scene with its size, frame rate, render runtime and props, so any host can build and play it.
A composition is the unit a host plays. It names a scene, a size, a frame rate and a render runtime; its props are the scene’s inputs. Remotion, HyperFrames, the native preview and the exporter all take the same definition, so choosing a host is a decision made at the app’s entry point.
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 input: {
of: {
<T extends AnyType>(type: T): ValueInput<T, TypeValue<T>, true>;
<T extends AnyType>(type: T, defaultValue: null): ValueInput<T, TypeValue<T> | null, false>;
<T extends AnyType>(type: T, defaultValue: TypeValue<T>): ValueInput<T, TypeValue<T>, false>;
<TInstance extends object>(blueprint: ActorClass<TInstance>): ActorInput<TInstance>;
};
}
input, 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 text: DataType<"text", ZodString>text } from "@motionactor/schema";
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 defineComposition: DefineComposition<ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>A composition of a `createScene` definition (`scene`), painted by `render`,
or by default by `SceneRenderer` over `background`.defineComposition,
} from "@motionactor/renderers/react";
import { const dom: StagedBackend<React.FC<ReactBackendHostProps>, "dom", React.ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom } from "@motionactor/dom/react";
class class TitleTitle extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
Title.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("");
}
const const titleScene: SceneDefinition<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
heading: Title;
}>
titleScene = createScene<{
heading: Title;
}, {
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
heading: Title;
}>, options?: SceneDefinitionOptions<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}, Readonly<...>> | 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, { title: stringtitle }) {
const const heading: Titleheading = ctx: SceneContextctx.CompileContext.spawn<Title>(definition: ActorClass<Title>, overrides?: {
text?: SignalInputValue<string> | undefined;
transform?: {
scale?: SignalInputValue<number> | undefined;
rotation?: SignalInputValue<number> | undefined;
} | undefined;
id?: undefined;
localLayout?: undefined;
worldLayout?: undefined;
compositing?: {
blend?: SignalInputValue<"source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | ... 4 more ... | "plus-lighter"> | undefined;
isolate?: SignalInputValue<...> | undefined;
clip?: SignalInputValue<...> | undefined;
} | undefined;
... 17 more ...;
bounds?: undefined;
} | undefined, options?: {
...;
} | undefined): Title (+1 overload)
spawn(class TitleTitle, {
text?: SignalInputValue<string> | undefinedtext: title: stringtitle,
appearance?: {
opacity?: SignalInputValue<number> | undefined;
} | undefined
appearance: { opacity?: SignalInputValue<number> | undefinedopacity: 0 },
});
yield* const heading: Titleheading.LayoutActor<{}>.appearance: {
opacity: TypedFieldSignal<number, number>;
} & SignalGroupDefinition
appearance.opacity: TypedFieldSignal<number, number>opacity.function tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>Tween to target over authored seconds — must yield*tween(1, 0.5);
yield* ctx: SceneContextctx.CompileContext.wait(duration: Duration): Generator<Frame, void, unknown>wait(1.5);
return { heading: Titleheading };
},
{ SceneDefinitionOptions<{ readonly title: ValueInput<DataType<"text", ZodString>, string, false>; }, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>.inputs?: {
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
} | undefined
inputs: { title: ValueInput<DataType<"text", ZodString>, string, false>title: const input: {
of: {
<T extends AnyType>(type: T): ValueInput<T, TypeValue<T>, true>;
<T extends AnyType>(type: T, defaultValue: null): ValueInput<T, TypeValue<T> | null, false>;
<T extends AnyType>(type: T, defaultValue: TypeValue<T>): ValueInput<T, TypeValue<T>, false>;
<TInstance extends object>(blueprint: ActorClass<TInstance>): ActorInput<TInstance>;
};
}
input.of: <DataType<"text", ZodString>>(type: DataType<"text", ZodString>, defaultValue: string) => ValueInput<DataType<"text", ZodString>, string, false> (+3 overloads)Declare a scene input. Over a named type from `@motionactor/schema`,
`input.of(image)` is required; `input.of(text, "Launch day")` and
`input.of(image, null)` are not. Over an actor blueprint,
`input.of(Narrator)` takes a `Narrator` from the scene that stages it,
and has no default.of(const text: DataType<"text", ZodString>text, "Launch") } },
);
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, React.ReactNode>[]backends: [const dom: StagedBackend<React.FC<ReactBackendHostProps>, "dom", React.ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom],
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 dom: StagedBackend<React.FC<ReactBackendHostProps>, "dom", React.ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom.render: <typeof Title>(blueprint: typeof Title, render: ActorRendererForBlueprints<readonly [typeof Title], "dom">, options?: ActorRendererRegistrationOptions) => BackendRendererDefinition<readonly [typeof Title], "dom"> (+1 overload)render(class TitleTitle, ({ actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
text: string;
transform: {
scale: number;
rotation: number;
};
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 | ... 1 more ... | Readonly<...>;
x: LayoutValue;
y: LayoutValue;
};
... 4 more ...;
transitionState: {
...;
};
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor }) => (
<React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
text: string;
transform: {
scale: number;
rotation: number;
};
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 | ... 1 more ... | Readonly<...>;
x: LayoutValue;
y: LayoutValue;
};
... 4 more ...;
transitionState: {
...;
};
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor.signals: Readonly<Record<string, unknown>> & {
text: string;
transform: {
scale: number;
rotation: number;
};
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 | ... 1 more ... | Readonly<...>;
x: LayoutValue;
y: LayoutValue;
};
... 4 more ...;
transitionState: {
...;
};
}
signals.text: stringtext}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>
)),
],
});
export const const titleCard: CompositionDefinition<HostInputArgs<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}>, {
heading: Title;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>
titleCard = defineComposition<SceneDefinition<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
heading: Title;
}>>(definition: SceneCompositionInput<SceneDefinition<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
heading: Title;
}>, ReactBackendHost, React.ReactNode, React.ReactElement<...>>): CompositionDefinition<...> (+1 overload)
`scene`'s inputs are the props.defineComposition({
id: stringid: "TitleCard",
width: numberwidth: 1920,
height: numberheight: 1080,
fps: numberfps: 30,
runtime: RenderRuntime<ReactBackendHost, React.ReactNode>runtime,
scene: SceneDefinition<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
heading: Title;
}>
scene: const titleScene: SceneDefinition<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
heading: Title;
}>
titleScene,
background?: string | undefinedThe fill under the default `render`'s scene; unread with an authored `render`.background: "#0b0b0f",
});
The host builds scene at the composition’s size and frame rate, with the props as its inputs,
and draws each frame with the binding’s SceneRenderer over background.
The definition
| Field | Purpose |
|---|---|
id |
The name hosts list it under. Required |
width, height, fps |
The stage in pixels and the frame rate. Each must be a positive number |
scene |
A createScene definition, built at the composition’s size and frame rate |
defaultProps |
Props used when a host passes none, merged under a host’s. Required when the scene has a required input |
inputs |
(props: P) → inputs, when the props are not the scene’s inputs. P comes from defaultProps or the mapper’s parameter; the result is checked against the scene’s inputs. Without it the props are exactly the scene’s inputs |
scenes |
(props) → StagedScene[], the scenes the built scene lists as ctx.scenes |
runtime |
The render runtime: backends and renderers |
fonts |
Font faces to load before the first frame is shown. See Media, assets and fonts |
background |
The fill under the scene when render is left out |
render |
({ width, height, fps, props, compiled, frame }) → element, called for every frame. Defaults to SceneRenderer over background |
Props that pick or shape a scene go through scenes: a root that runs
for (const s of ctx.scenes) yield* ctx.stage(s) plays what scenes(props) returns. A build
that compiles to less than one whole frame throws. Hosts read the composition’s length from compiled.totalDuration,
so nothing else declares a duration.
render is where a composition can draw around the scene: a watermark, a debug overlay
reading props. SceneRenderer inside it needs no runtime prop: the composition provides
its runtime to everything it draws. Anything that moves belongs in the scene, where seeking
and export can reach it.
Building outside a host
buildComposition runs the same build a host does and returns the compiled scene, the media
plan and every asset the composition loads. A test or a command-line tool can call it with no
browser:
import { function buildComposition<P extends Record<string, unknown>, T, THost, TNode, TRender>(composition: CompositionDefinition<P, T, THost, TNode, TRender>, props: P, options?: CompositionBuildOptions): CompositionBuild<T>buildComposition } from "@motionactor/renderers";
import { const titleCard: CompositionDefinition<HostInputArgs<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}>, {
heading: Title;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>
titleCard } from "./title-card";
using using build: CompositionBuild<{
heading: Title;
}>
build = buildComposition<HostInputArgs<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}>, {
heading: Title;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>(composition: CompositionDefinition<HostInputArgs<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}>, {
heading: Title;
}, ReactBackendHost, React.ReactNode, React.ReactElement<...>>, props: HostInputArgs<...>, options?: CompositionBuildOptions): CompositionBuild<...>
buildComposition(const titleCard: CompositionDefinition<HostInputArgs<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}>, {
heading: Title;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>
titleCard, { title?: string | undefinedtitle: "Hello" });
using build: CompositionBuild<{
heading: Title;
}>
build.SceneBuild<{ heading: Title; }>.compiled: CompiledScene<{
heading: Title;
}, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>
compiled.CompiledScene<TSceneExports = void | SceneExportShape, TLayers extends CompiledLayers = Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>.totalDuration: numbertotalDuration; // 60
using build: CompositionBuild<{
heading: Title;
}>
build.SceneBuild<{ heading: Title; }>.compiled: CompiledScene<{
heading: Title;
}, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>
compiled.CompiledScene<{ heading: Title; }, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
heading: Title;
}>
at(0).FrameState<{ heading: Title; }>.scene: {
heading: ResolvedActorOfInstance<Title> | undefined;
}
scene.heading: ResolvedActorOfInstance<Title> | undefinedheading?.text: string | undefinedtext; // "Hello"
using build: CompositionBuild<{
heading: Title;
}>
build.assets: AssetRef[]Every ref the composition loads: typed scene fields, media and cues, fonts.assets; // []
A build owns the cells its scene compiled. using releases them when the build leaves scope;
outside a block, call build[Symbol.dispose](). Hosts do the same when props change or the
composition unmounts.
What a host does with it
- Merges its props over
defaultPropsand builds the scene, after React has committed, so a render React throws away never compiles a scene. - Collects the build’s assets: the scene’s asset fields, every video and audio clip its media renderers report, and the composition’s fonts.
- Holds the first frame until those assets, the fonts and every backend are ready.
- Calls
renderwith each frame it is asked for.
When a scene needs the length of a video or audio file to compile, the first build stops with the source it is missing. The host measures that file in the browser and builds again with the length known.
In Remotion
toRemotionComposition turns a definition into the props of a Remotion <Composition>. The
duration comes from a build in Remotion’s calculateMetadata, so it follows the props.
import { const Composition: <Schema extends AnyZodObject, Props extends Record<string, unknown>>(props: CompositionProps<Schema, Props>) => import("react/jsx-runtime").JSX.Element | nullComposition } from "remotion";
import { function toRemotionComposition<P extends Record<string, unknown>, T>(input: CompositionDefinition<P, T>): {
id: string;
width: number;
height: number;
fps: number;
defaultProps: P;
durationInFrames: number;
component: React.FC<P>;
calculateMetadata: CalculateMetadataFunction<P>;
}
A Remotion composition for `input`: like every host, it mounts the runtime media family after the runtime's own renderers.
The mounted instance builds at Remotion's resolved viewport, which `calculateMetadata` or a Player may set apart from the definition's.toRemotionComposition } from "@motionactor/remotion";
import { const titleCard: CompositionDefinition<HostInputArgs<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}>, {
heading: Title;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>
titleCard } from "./title-card";
export const const RemotionRoot: () => JSX.ElementRemotionRoot = () => (
<const Composition: <Schema extends AnyZodObject, Props extends Record<string, unknown>>(props: CompositionProps<Schema, Props>) => import("react/jsx-runtime").JSX.Element | nullComposition {...toRemotionComposition<HostInputArgs<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}>, {
heading: Title;
}>(input: CompositionDefinition<HostInputArgs<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}>, {
heading: Title;
}>): {
id: string;
width: number;
height: number;
fps: number;
defaultProps: HostInputArgs<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}>;
durationInFrames: number;
component: React.FC<...>;
calculateMetadata: CalculateMetadataFunction<...>;
}
A Remotion composition for `input`: like every host, it mounts the runtime media family after the runtime's own renderers.
The mounted instance builds at Remotion's resolved viewport, which `calculateMetadata` or a Player may set apart from the definition's.toRemotionComposition(const titleCard: CompositionDefinition<HostInputArgs<{
readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
}>, {
heading: Title;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>
titleCard)} />
);
The other hosts are on Hosts.