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

Camera2D

The scene camera from @motionactor/primitives. It pans, zooms, rolls, frames a box, follows an actor and shakes.

Camera2D is an actor with no picture. Spawn one and its values become the frame’s camera, which SceneRenderer applies to every layer as one transform: zoom, then rotation, then the pan, around an origin that defaults to the layer’s centre.

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
} from "@motionactor/core";
import { class Camera2D
2D scene camera — controls pan, zoom, and rotation of the entire scene. centerX/centerY is a **pan offset** in world-space pixels. (0, 0) = identity (no pan). Zoom/rotation pivot around the viewport anchor (originX, originY), defaulting to center. Does not render visually. It declares itself the scene camera, so its resolved signals become `FrameState.camera`.
Camera2D
} from "@motionactor/primitives/camera";
const const compiled: CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>compiled = createScene<void, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>, options?: SceneDefinitionOptions<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>> | 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) {
const const camera: Camera2Dcamera = ctx: SceneContextctx.
CompileContext.spawn<Camera2D>(definition: ActorClass<Camera2D>, overrides?: {
    centerX?: SignalInputValue<number> | undefined;
    centerY?: SignalInputValue<number> | undefined;
    zoom?: SignalInputValue<number> | undefined;
    rotation?: SignalInputValue<number> | undefined;
    shakeOffset?: {
        rotation?: SignalInputValue<number> | undefined;
        x?: SignalInputValue<number> | undefined;
        y?: SignalInputValue<number> | undefined;
    } | undefined;
    ... 19 more ...;
    parent?: undefined;
} | undefined, options?: {
    ...;
} | undefined): Camera2D (+1 overload)
spawn
(class Camera2D
2D scene camera — controls pan, zoom, and rotation of the entire scene. centerX/centerY is a **pan offset** in world-space pixels. (0, 0) = identity (no pan). Zoom/rotation pivot around the viewport anchor (originX, originY), defaulting to center. Does not render visually. It declares itself the scene camera, so its resolved signals become `FrameState.camera`.
Camera2D
);
yield* const camera: Camera2Dcamera.
Camera2D.moveTo(params: {
    x: number;
    y: number;
    zoom: number;
}, transition: TweenTransition): Generator<number, void, unknown>
Pan + zoom simultaneously (raw offset values).
moveTo
({ x: numberx: 200, y: numbery: 0, zoom: numberzoom: 1.5 }, 0.8);
}).SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 }); const compiled: CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>compiled.CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<void>at(24).FrameState<void>.camera: Camera2DState
2D scene camera. Identity when no spawned actor's class declares `SCENE_CAMERA`.
camera
;
// { centerX: 200, centerY: 0, zoom: 1.5, rotation: 0, // originX: 0.5, originY: 0.5 }

centerX and centerY are a pan in world pixels from where the scene sits untouched, so (0, 0) with zoom 1 is no camera at all. Import from @motionactor/primitives/camera, which depends on core alone. The package root exports the same classes alongside helpers that need Three.js.

The camera frames the left panel with 80 pixels to spare, then follows the right panel while it slides, so one tween moves both the panel and the view. A shake runs on top of the follow and dies out, and reset returns the view to identity.

Commands

Each command takes its transition last: seconds, or { duration, easing }. The easing defaults to Easing.inOutCubic.

Command Moves to
panTo({ x, y }, t) That pan
zoomTo({ zoom }, t) That zoom
moveTo({ x, y, zoom }, t) Both at once
centerOn({ rect, viewport, zoom? }, t) The pan that puts the rect’s centre at the origin
focus({ rect, viewport, padding? }, t) The pan and the largest zoom that fit the rect, with padding
follow({ target, viewport, zoom?, offsetX?, offsetY? }) A pan that tracks target from then on
shake({ profile, viewport, duration, ... }) A wobble on top of the pan and roll, for duration
reset(t) Pan 0, zoom 1, rotation 0

viewport is the size of the layer the camera looks through. focus also takes paddingX and paddingY for unequal margins.

Framing an actor

centerOn and focus read the rect once, when they run, and tween to it. Give them actor.worldLayout(), the box on the stage with percentages and hosts resolved:

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
, class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor } from "@motionactor/core";
import { class Camera2D
2D scene camera — controls pan, zoom, and rotation of the entire scene. centerX/centerY is a **pan offset** in world-space pixels. (0, 0) = identity (no pan). Zoom/rotation pivot around the viewport anchor (originX, originY), defaulting to center. Does not render visually. It declares itself the scene camera, so its resolved signals become `FrameState.camera`.
Camera2D
} from "@motionactor/primitives/camera";
class class CardCard extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {} const
const viewport: {
    width: number;
    height: number;
}
viewport
= { width: numberwidth: 1280, height: numberheight: 720 };
createScene<void, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>, options?: SceneDefinitionOptions<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>> | 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) {
const const camera: Camera2Dcamera = ctx: SceneContextctx.
CompileContext.spawn<Camera2D>(definition: ActorClass<Camera2D>, overrides?: {
    centerX?: SignalInputValue<number> | undefined;
    centerY?: SignalInputValue<number> | undefined;
    zoom?: SignalInputValue<number> | undefined;
    rotation?: SignalInputValue<number> | undefined;
    shakeOffset?: {
        rotation?: SignalInputValue<number> | undefined;
        x?: SignalInputValue<number> | undefined;
        y?: SignalInputValue<number> | undefined;
    } | undefined;
    ... 19 more ...;
    parent?: undefined;
} | undefined, options?: {
    ...;
} | undefined): Camera2D (+1 overload)
spawn
(class Camera2D
2D scene camera — controls pan, zoom, and rotation of the entire scene. centerX/centerY is a **pan offset** in world-space pixels. (0, 0) = identity (no pan). Zoom/rotation pivot around the viewport anchor (originX, originY), defaulting to center. Does not render visually. It declares itself the scene camera, so its resolved signals become `FrameState.camera`.
Camera2D
);
const const card: Cardcard = ctx: SceneContextctx.
CompileContext.spawn<Card>(definition: ActorClass<Card>, overrides?: {
    id?: undefined;
    cursor?: undefined;
    cursorSeconds?: undefined;
    sceneFps?: undefined;
    onInit?: undefined;
    onEnter?: undefined;
    onExit?: undefined;
    enter?: undefined;
    exit?: undefined;
    parent?: 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<...> | ... 1 more ... | undefined;
    } | undefined;
    ... 11 more ...;
    bounds?: undefined;
} | undefined, options?: {
    ...;
} | undefined): Card (+1 overload)
spawn
(class CardCard, {
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: 600, y?: SignalInputValue<LayoutValue> | undefinedy: 300,
width?: Readonly<{
    kind: "layout.percent";
    value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
    kind: "layout.percent";
    value: number;
}>> | undefined
width
: 320,
height?: Readonly<{
    kind: "layout.percent";
    value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
    kind: "layout.percent";
    value: number;
}>> | undefined
height
: 200 },
}); yield* const camera: Camera2Dcamera.
Camera2D.focus(params: {
    rect: Rect;
    viewport: CameraViewport;
} & FocusOptions, transition: TweenTransition): Generator<number, void, unknown>
Focus on a rect — auto-compute zoom to fit it within the viewport with optional padding.
focus
(
{ rect: Rectrect: const card: Cardcard.LayoutActor<{}>.worldLayout(): ResolvedLayoutBoxworldLayout(), viewport: CameraViewportviewport, FocusOptions.padding?: number | undefinedpadding: 80 }, 0.6, ); }).SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });

A rect read this way is a snapshot. When the card moves later, the camera stays where it was.

Following

follow replaces the pan with a derived value of the target’s layout, so the camera keeps the target centred on every later frame without another command. It takes no time. A later panTo, focus or reset writes the pan again and ends the follow.

Shake

shake adds a seeded wobble on top of the pan and rotation the camera already has, including a live follow. The wobble is a function of the shake’s own progress, so a scrubbed frame matches a played one, and it starts and ends at exactly zero.

The wobble lives in the camera’s shakeOffset group, which the camera adds to centerX, centerY and rotation when it becomes the frame’s camera. The actor’s own centerX stays where the pan or follow put it.

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
} from "@motionactor/core";
import { class Camera2D
2D scene camera — controls pan, zoom, and rotation of the entire scene. centerX/centerY is a **pan offset** in world-space pixels. (0, 0) = identity (no pan). Zoom/rotation pivot around the viewport anchor (originX, originY), defaulting to center. Does not render visually. It declares itself the scene camera, so its resolved signals become `FrameState.camera`.
Camera2D
} from "@motionactor/primitives/camera";
createScene<void, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>, options?: SceneDefinitionOptions<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>> | 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) {
const const camera: Camera2Dcamera = ctx: SceneContextctx.
CompileContext.spawn<Camera2D>(definition: ActorClass<Camera2D>, overrides?: {
    centerX?: SignalInputValue<number> | undefined;
    centerY?: SignalInputValue<number> | undefined;
    zoom?: SignalInputValue<number> | undefined;
    rotation?: SignalInputValue<number> | undefined;
    shakeOffset?: {
        rotation?: SignalInputValue<number> | undefined;
        x?: SignalInputValue<number> | undefined;
        y?: SignalInputValue<number> | undefined;
    } | undefined;
    ... 19 more ...;
    parent?: undefined;
} | undefined, options?: {
    ...;
} | undefined): Camera2D (+1 overload)
spawn
(class Camera2D
2D scene camera — controls pan, zoom, and rotation of the entire scene. centerX/centerY is a **pan offset** in world-space pixels. (0, 0) = identity (no pan). Zoom/rotation pivot around the viewport anchor (originX, originY), defaulting to center. Does not render visually. It declares itself the scene camera, so its resolved signals become `FrameState.camera`.
Camera2D
);
yield* const camera: Camera2Dcamera.
Camera2D.shake(params: {
    profile: CameraShakeProfileName;
    viewport: CameraViewport;
    duration: Duration;
    intensity?: number;
    frequency?: number;
    rotation?: boolean;
    seed?: number;
} & CameraShakeEnvelopeOptions): Generator<...>
Procedural camera shake, added on top of whatever the camera is already doing. Runs for `duration` seconds and settles to exactly zero, leaving no residual offset on the base pan. Deterministic: the offset is a pure function of progress, so scrubbing to a frame gives the same result as playing to it.
shake
({
profile: CameraShakeProfileNameprofile: "handheld-tele-strong", viewport: CameraViewportviewport: { width: numberwidth: 1280, height: numberheight: 720 }, duration: numberduration: 1.5, intensity?: number | undefined
Amplitude multiplier. Default 1.
intensity
: 1.4,
}); }).SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });
Option Default
profile The kind of camera, below
duration Seconds
intensity 1 Multiplies the amplitude
frequency 1 Multiplies the speed
rotation true Includes roll. false reads as a gimbal
attack 0.08 Fraction of the run spent ramping in
settle 0.25 Fraction spent ramping out
seed Shifts the noise, so two cameras shake differently

The profiles are measured from real cameras. The handheld ones are tuned to move the frame by about the same distance on screen, but a wide lens drifts in slow, large swings and a telephoto jitters in small, fast ones, so they read as different lenses.

Profile Lens Roll
handheld-normal-mild, -strong, -extreme 50 mm None
handheld-wideangle-mild 28 mm None
handheld-wideangle-strong 28 mm Slight
handheld-tele-mild 85 mm None
handheld-tele-strong 85 mm Slight
rig-6d-shake 50 mm Fine vibration, tens of hertz, on every axis
rig-6d-wobble 50 mm A sway of one or two degrees, a few times a second

A profile without roll never changes rotation, whatever rotation is set to.

Not for

Moving one actor: tween its layout. Parallax: put the background in its own layer and move that layer’s actors at a different rate, since the camera moves every layer the same way.

Was this page helpful?