---
title: Camera2D
description: The scene camera from @motionactor/primitives. It pans, zooms, rolls, frames a box, follows an actor and shakes.
mode: wide
---

`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.

```ts twoslash
import { createScene } from "@motionactor/core";
import { Camera2D } from "@motionactor/primitives/camera";

const compiled = createScene(function* (ctx) {
  const camera = ctx.spawn(Camera2D);
  yield* camera.moveTo({ x: 200, y: 0, zoom: 1.5 }, 0.8);
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

compiled.at(24).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.

<CameraFocusScene />

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:

```ts twoslash
import { createScene, LayoutActor } from "@motionactor/core";
import { Camera2D } from "@motionactor/primitives/camera";

class Card extends LayoutActor {}

const viewport = { width: 1280, height: 720 };

createScene(function* (ctx) {
  const camera = ctx.spawn(Camera2D);
  const card = ctx.spawn(Card, {
    layout: { x: 600, y: 300, width: 320, height: 200 },
  });

  yield* camera.focus(
    { rect: card.worldLayout(), viewport, padding: 80 },
    0.6,
  );
}).build({ viewport: { width: 1920, height: 1080 }, fps: 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](/docs/reactivity/derived) 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.

```ts twoslash
import { createScene } from "@motionactor/core";
import { Camera2D } from "@motionactor/primitives/camera";

createScene(function* (ctx) {
  const camera = ctx.spawn(Camera2D);
  yield* camera.shake({
    profile: "handheld-tele-strong",
    viewport: { width: 1280, height: 720 },
    duration: 1.5,
    intensity: 1.4,
  });
}).build({ viewport: { width: 1920, height: 1080 }, fps: 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](/docs/rendering/backends-and-layers#layers) and move that layer's actors at a different
rate, since the camera moves every layer the same way.
