# CameraHandle

> Talos 0.7 · Since 0.6; changed in 0.7

The active camera rig.

While a script drives it — it follows something, or a `"fixed"` rig was given a `position` — the rig is the rendered view and the listener; otherwise the scene's primary `camera` entity is.

Reached in a behaviour as `ctx.camera`.

## Signature

```ts
export interface CameraHandle {
  /** Switch rigs; `params` are the rig's settings, per kind: see {@link CameraParams}. */
  set(kind: CameraKind, params?: CameraParams): void
  /**
   * What the rig tracks. `null` returns the view to the scene's primary camera and forgets every
   * `set` param, `fov` included.
   */
  follow(target: EntityHandle | string | null): void
  /**
   * Jolt the view up to `amplitude` m, decaying over `seconds` (0.4) at `frequency` Hz (25).
   */
  shake(amplitude: number, seconds?: number, frequency?: number): void
  /** The engine rig's yaw now, radians: the last `set`'s, or where a `recenterDelay` swing took it. */
  readonly yaw: number
  /** The engine rig's pitch now, radians; positive looks up. */
  readonly pitch: number
  /**
   * The heading of the view being drawn, radians about +Y with 0 facing -Z (the rig, else the scene camera;
   * 0 with none), measured at the top of the tick. The frame an `inputBasis: "camera"` controller reads its
   * stick in: forward is `[-sin(viewYaw), 0, -cos(viewYaw)]`.
   */
  readonly viewYaw: number
  /**
   * The vertical field of view being drawn, degrees: what a script set with `set(kind, { fov })`,
   * else the scene's primary camera's `fov`, else 60. A rig that names no fov of its own (the
   * built-in first-person rig by default) draws at this.
   */
  readonly fov: number
  /**
   * The box an orthographic lens frames, measured in the reference view (1920 x 1080), never the window
   * ({@link ViewSize}); `null` while the lens is perspective. The lens is read live. Because it is the same at
   * any window size, gameplay may use it (a clamp, "keep the ship on screen") and a replay or a co-op peer at
   * another size plays the same game. The drawn view always contains the box; a window that is not 16:9
   * shows more of the world beyond it on one axis.
   */
  readonly viewSize: ViewSize | null
  /**
   * The world ray through pixel `(x, y)` of view `view`'s cell (default 1, the view `ctx.camera` is): `origin`
   * on the near plane and a unit `dir`. Pixels are the cell's own, top-left 0,0, in the viewport
   * {@link ViewSize} reports (`pixelWidth` x `pixelHeight`, divided among the cells of a split-screen layout
   * as `ctx.views` draws them). It goes through the matrices the renderer draws that view with, as of the top of
   * the tick (like `viewYaw`), so the pixel you aim at is the pixel you see. `null` when that view has no
   * camera or `x`/`y` is not a number; a pixel outside the cell extrapolates. Throws `ViewsError`
   * `views-out-of-range` for a view outside 1 to 4.
   *
   * Under the mouse: `const p = ctx.input.pointer; if (p) ctx.camera.screenToRay(p.x, p.y, p.view)`. The
   * pointer is simulation input and is recorded; the ray is cast through the camera the run draws, so a
   * replay at another window size aims through that size's view of the same pointer pixel.
   */
  screenToRay(x: number, y: number, view?: number): Ray | null
  /**
   * The first collider along `screenToRay(x, y, opts.view)`, as `ctx.world.raycast` answers it. Triggers are
   * seen through unless `opts.triggers` is true. With `opts.ground` (a height), a ray that hits no collider
   * meets the plane y = ground instead, answered with `entity: null` and normal `[0, 1, 0]`. `null` for a miss,
   * no ray, or a ray parallel to or pointing away from the plane.
   */
  pick(x: number, y: number, opts?: CameraPickOptions): PickHit | null
  /**
   * The pixel of view `view`'s cell (default 1) that world `point` is drawn at: `screenToRay`'s inverse, in the
   * same pixels and through the same matrices. `null` when that view has no camera or the point is at or behind
   * the eye. A point in front of the camera but off screen is **not** null: it answers pixels outside the
   * cell (negative, or beyond `pixelWidth` / `pixelHeight`), so test the range yourself to know whether it is visible.
   */
  worldToScreen(point: Vec3, view?: number): { x: number; y: number } | null
  /** The engine rig's boom this frame: its length, whether a wall collapsed it, what did, and the character fade. */
  readonly arm: CameraArm
}
```

Import: `import type { CameraHandle } from "@argus/api"`

## Members

| Member | Signature | Description |
|---|---|---|
| `set` | `set(kind: CameraKind, params?: CameraParams): void` | Switch rigs; `params` are the rig's settings, per kind: see {@link CameraParams}. |
| `follow` | `follow(target: EntityHandle \| string \| null): void` | What the rig tracks. `null` returns the view to the scene's primary camera and forgets every `set` param, `fov` included. |
| `shake` | `shake(amplitude: number, seconds?: number, frequency?: number): void` | Jolt the view up to `amplitude` m, decaying over `seconds` (0.4) at `frequency` Hz (25). |
| `yaw` | `readonly yaw: number` | The engine rig's yaw now, radians: the last `set`'s, or where a `recenterDelay` swing took it. |
| `pitch` | `readonly pitch: number` | The engine rig's pitch now, radians; positive looks up. |
| `viewYaw` | `readonly viewYaw: number` | The heading of the view being drawn, radians about +Y with 0 facing -Z (the rig, else the scene camera; 0 with none), measured at the top of the tick. The frame an `inputBasis: "camera"` controller reads its stick in: forward is `[-sin(viewYaw), 0, -cos(viewYaw)]`. |
| `fov` | `readonly fov: number` | The vertical field of view being drawn, degrees: what a script set with `set(kind, { fov })`, else the scene's primary camera's `fov`, else 60. A rig that names no fov of its own (the built-in first-person rig by default) draws at this. |
| `viewSize` | `readonly viewSize: ViewSize \| null` | The box an orthographic lens frames, measured in the reference view (1920 x 1080), never the window ({@link ViewSize}); `null` while the lens is perspective. The lens is read live. Because it is the same at any window size, gameplay may use it (a clamp, "keep the ship on screen") and a replay or a co-op peer at another size plays the same game. The drawn view always contains the box; a window that is not 16:9 shows more of the world beyond it on one axis. |
| `screenToRay` | `screenToRay(x: number, y: number, view?: number): Ray \| null` | The world ray through pixel `(x, y)` of view `view`'s cell (default 1, the view `ctx.camera` is): `origin` on the near plane and a unit `dir`. Pixels are the cell's own, top-left 0,0, in the viewport {@link ViewSize} reports (`pixelWidth` x `pixelHeight`, divided among the cells of a split-screen layout as `ctx.views` draws them). It goes through the matrices the renderer draws that view with, as of the top of the tick (like `viewYaw`), so the pixel you aim at is the pixel you see. `null` when that view has no camera or `x`/`y` is not a number; a pixel outside the cell extrapolates. Throws `ViewsError` `views-out-of-range` for a view outside 1 to 4. Under the mouse: `const p = ctx.input.pointer; if (p) ctx.camera.screenToRay(p.x, p.y, p.view)`. The pointer is simulation input and is recorded; the ray is cast through the camera the run draws, so a replay at another window size aims through that size's view of the same pointer pixel. |
| `pick` | `pick(x: number, y: number, opts?: CameraPickOptions): PickHit \| null` | The first collider along `screenToRay(x, y, opts.view)`, as `ctx.world.raycast` answers it. Triggers are seen through unless `opts.triggers` is true. With `opts.ground` (a height), a ray that hits no collider meets the plane y = ground instead, answered with `entity: null` and normal `[0, 1, 0]`. `null` for a miss, no ray, or a ray parallel to or pointing away from the plane. |
| `worldToScreen` | `worldToScreen(point: Vec3, view?: number): { x: number; y: number } \| null` | The pixel of view `view`'s cell (default 1) that world `point` is drawn at: `screenToRay`'s inverse, in the same pixels and through the same matrices. `null` when that view has no camera or the point is at or behind the eye. A point in front of the camera but off screen is **not** null: it answers pixels outside the cell (negative, or beyond `pixelWidth` / `pixelHeight`), so test the range yourself to know whether it is visible. |
| `arm` | `readonly arm: CameraArm` | The engine rig's boom this frame: its length, whether a wall collapsed it, what did, and the character fade. |

## Example

The tested fixture `fixtures/recipes/jump-pad-and-elevator/scenes/behaviors/camera-zone.ts`.

```ts
import type { Behavior } from "@argus/api"

/**
 * A trigger volume that retunes the camera while the player is inside it.
 *
 * The engine has no camera-zone concept and needs none: a volume, a `trigger` component and this.
 * Here the player's behaviour owns the camera (it re-sends its params when the view is turned), so
 * the zone tells it what to change rather than calling `ctx.camera.set` itself. In a scene where
 * nothing else writes the camera, the zone can call `ctx.camera.set(kind, params)` directly.
 */
interface Props {
  /** `ctx.camera.set` params to merge in on the way in, and on the way out. */
  inside?: Record<string, unknown>
  outside?: Record<string, unknown>
}

export default {
  onTrigger(ctx, trigger) {
    const params = trigger.entering ? ctx.props.inside : ctx.props.outside
    if (params) ctx.events.emit("camera-zone", { target: trigger.other.id, params })
  },
} satisfies Behavior<Record<string, never>, Props>
```
