---
title: Quickstart
description: Write a scene, read a frame from it, draw it with a renderer and play it in a host.
mode: wide
---

MotionActor is not released yet.

1. ### Write a scene

    `LayoutActor` is the base class for an actor that occupies space; it comes with layout,
    transform and appearance state. `Card` adds a `progress` signal and a `reveal` command that
    tweens it. The scene spawns one card, reveals it over a second and holds it for another.

    <RevealCardScene />

    `yield*` is the only thing that spends time. The line beside the stage lights while its frames
    play; click a marked line to jump there. The second tab is the renderer that draws the card.

2. ### Read a frame

    `scene` is a definition: nothing compiles until a host builds it at a size and a frame rate.
    The build is a record of the scene, and nothing in it plays. Ask it for a frame and you get
    that frame's state.

    ```ts twoslash
    // @filename: reveal.scene.ts
    import {
      createScene,
      LayoutActor,
      signal,
      type Duration,
    } from "@motionactor/core";

    export class Card extends LayoutActor {
      progress = signal(0);

      *reveal({ duration }: { duration: Duration }) {
        yield* this.progress.tween(1, duration);
      }
    }

    export const scene = createScene(function* (ctx) {
      const card = ctx.spawn(Card, {
        layout: { x: 400, y: 240, width: 480, height: 240 },
      });

      yield* card.reveal({ duration: 1 });
      yield* ctx.wait(1);

      return { card };
    });
    // @filename: main.ts
    // ---cut---
    import { scene } from "./reveal.scene";

    using compiled = scene.build({ viewport: { width: 1280, height: 720 }, fps: 30 });
    compiled.totalDuration; // 60
    compiled.at(15).scene.card?.progress; // 0.5
    compiled.at(45).scene.card?.progress; // 1
    compiled.at(15).scene.card?.progress; // 0.5 again
    ```

    Returning `{ card }` from the generator makes `card` a scene export, a typed handle on that
    actor in every frame. The order of the calls doesn't matter: frame 15 reads the same after
    frame 45 as it did before.

3. ### Draw it

    A renderer pairs an actor class with a backend and returns what the backend should draw. A
    render runtime gathers backends and renderers, and `SceneRenderer` resolves one frame and hands
    each actor to its renderer.

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

    export class Card extends LayoutActor {
      progress = signal(0);

      *reveal({ duration }: { duration: Duration }) {
        yield* this.progress.tween(1, duration);
      }
    }

    export const scene = createScene(function* (ctx) {
      const card = ctx.spawn(Card, {
        layout: { x: 400, y: 240, width: 480, height: 240 },
      });

      yield* card.reveal({ duration: 1 });
      yield* ctx.wait(1);

      return { card };
    });
    // @filename: reveal.render.tsx
    // ---cut---
    import { createRenderRuntime } from "@motionactor/renderers/react";
    import { dom } from "@motionactor/dom/react";
    import { Card } from "./reveal.scene";

    // The backend places the card's box from its layout;
    // the renderer draws what goes inside it.
    const card = dom.render(Card, ({ actor }) => {
      const { progress } = actor.signals;
      return (
        <div
          className="grid size-full place-items-center rounded-2xl
            bg-[var(--stage-mark)] text-7xl font-medium text-[var(--stage-on-mark)]"
          style={{
            opacity: progress,
            transform: `scale(${0.9 + progress * 0.1})`,
          }}
        >
          MotionActor
        </div>
      );
    });

    export const runtime = createRenderRuntime({
      backends: [dom],
      renderers: [card],
    });
    // @filename: frame.tsx
    import { SceneRenderer } from "@motionactor/renderers/react";
    import { runtime } from "./reveal.render";
    import type { scene } from "./reveal.scene";

    // The host builds the scene and owns the build: `scene.build({ viewport, fps })`.
    type Build = ReturnType<typeof scene.build>;

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

    The DOM backend positions the card from its `layout` and `transform` state and applies its
    `appearance`. The renderer draws only what goes inside that box.

4. ### Play it

    A composition is what a host plays: a size, a frame rate, a render runtime, the scene it builds
    at that size and rate, and a way to draw a frame. `toRemotionComposition` turns one into a
    Remotion `<Composition>`, which reads the duration from the build.

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

    export class Card extends LayoutActor {
      progress = signal(0);

      *reveal({ duration }: { duration: Duration }) {
        yield* this.progress.tween(1, duration);
      }
    }

    export const scene = createScene(function* (ctx) {
      const card = ctx.spawn(Card, {
        layout: { x: 400, y: 240, width: 480, height: 240 },
      });

      yield* card.reveal({ duration: 1 });
      yield* ctx.wait(1);

      return { card };
    });
    // @filename: reveal.render.tsx
    import { createRenderRuntime } from "@motionactor/renderers/react";
    import { dom } from "@motionactor/dom/react";
    import { Card } from "./reveal.scene";

    // The backend places the card's box from its layout;
    // the renderer draws what goes inside it.
    const card = dom.render(Card, ({ actor }) => {
      const { progress } = actor.signals;
      return (
        <div
          className="grid size-full place-items-center rounded-2xl
            bg-[var(--stage-mark)] text-7xl font-medium text-[var(--stage-on-mark)]"
          style={{
            opacity: progress,
            transform: `scale(${0.9 + progress * 0.1})`,
          }}
        >
          MotionActor
        </div>
      );
    });

    export const runtime = createRenderRuntime({
      backends: [dom],
      renderers: [card],
    });
    // @filename: composition.tsx
    // ---cut---
    import { Composition } from "remotion";
    import { defineComposition, SceneRenderer } from "@motionactor/renderers/react";
    import { toRemotionComposition } from "@motionactor/remotion";
    import { scene } from "./reveal.scene";
    import { runtime } from "./reveal.render";

    export const reveal = defineComposition({
      id: "Reveal",
      width: 1280,
      height: 720,
      fps: 30,
      runtime,
      scene,
      render: ({ compiled, frame, width, height }) => (
        <SceneRenderer
          compiled={compiled}
          frame={frame}
          width={width}
          height={height}
        />
      ),
    });

    export const RemotionRoot = () => (
      <Composition {...toRemotionComposition(reveal)} />
    );
    ```

    The same definition plays in MotionActor's own preview, which waits for fonts and media to be
    ready before it shows a frame:

    ```ts twoslash title="preview.ts"
    // @jsx: react-jsx
    // @filename: reveal.scene.ts
    import {
      createScene,
      LayoutActor,
      signal,
      type Duration,
    } from "@motionactor/core";

    export class Card extends LayoutActor {
      progress = signal(0);

      *reveal({ duration }: { duration: Duration }) {
        yield* this.progress.tween(1, duration);
      }
    }

    export const scene = createScene(function* (ctx) {
      const card = ctx.spawn(Card, {
        layout: { x: 400, y: 240, width: 480, height: 240 },
      });

      yield* card.reveal({ duration: 1 });
      yield* ctx.wait(1);

      return { card };
    });
    // @filename: composition.tsx
    import {
      createRenderRuntime,
      defineComposition,
      SceneRenderer,
    } from "@motionactor/renderers/react";
    import { dom } from "@motionactor/dom/react";
    import { scene } from "./reveal.scene";
    export const reveal = defineComposition({
      id: "Reveal", width: 1280, height: 720, fps: 30,
      runtime: createRenderRuntime({ backends: [dom] }),
      scene,
      render: ({ compiled, frame, width, height }) => <SceneRenderer compiled={compiled} frame={frame} width={width} height={height} />,
    });
    // @filename: preview.ts
    // ---cut---
    import { mountComposition } from "@motionactor/engine/react";
    import { reveal } from "./composition";

    const preview = await mountComposition({
      composition: reveal,
      element: document.querySelector("#stage")!,
      props: {},
    });

    await preview.seek(45);
    preview.play();
    ```

**[How it works](/docs/mental-model)**

Compile time, frame time, and what may happen in each.

**[Rendering](/docs/rendering)**

Renderers, backends and hosts in depth.

**[Creating actors](/docs/guides/creating-actors)**

State layout, commands and renderers for reusable actors.

**[Remotion example](/docs/examples/remotion-card)**

A complete composition with a child, a derived value and typed exports.
