# Camera

> Frame a subject at any screen size, fly between named views, let users orbit and pinch-zoom, keep the subject clear of your UI, and follow moving objects.

Apricot has one camera per scene and four ways to control it. Pick one per scene:

| Need | Use |
| --- | --- |
| A fixed view you set once | `<Scene camera={{ position, target, fov }}>` |
| Keep a flat area (a card, a hero object) fully in view at any size | `<FitCamera size>` |
| Named views, fly between them, drag to orbit, pinch to zoom, frame around UI, follow a moving object | `<CameraRig>` |
| A chase camera behind a vehicle | `<FollowCamera target>` |

> [!WARNING]
> Use one camera controller per scene. Two controllers (for example `<FitCamera>` and `<CameraRig>`) fight over the same camera every frame.

## A fixed camera

```tsx title="Fixed.tsx"
import { Box, Scene } from "@apricot/react";

export function Fixed() {
  return (
    <Scene style={{ flex: 1 }} camera={{ position: [3, 2, 5], target: [0, 0, 0], fov: 40 }}>
      <Box />
    </Scene>
  );
}
```

`camera` is read once at mount. Defaults: `position` `[0, 0, 6]`, `target` `[0, 0, 0]`, `fov` 50°, `near` 0.1, `far` 200.

## `<FitCamera>`: fit an area at any screen size

`<FitCamera>` moves the camera along its line of sight so an area of `size` (width, height, facing the camera) fits on screen with a `margin`. It refits on every resize: rotation, split screen, a tablet.

```tsx title="FitCard.tsx"
import { CARD_SIZE, Card } from "@apricot/assets";
import { FitCamera, Scene } from "@apricot/react";

export function FitCard() {
  return (
    <Scene style={{ flex: 1 }} look="none" environment="studio" background="#0e0f12">
      <FitCamera size={[CARD_SIZE.width, CARD_SIZE.height]} margin={0.12} />
      <Card finish="gold" />
    </Scene>
  );
}
```

## `<CameraRig>`: views, orbit and framing

`<CameraRig>` places the camera on an orbit around a **subject** (azimuth and elevation in degrees, plus a distance) and flies between views along arcs, so it swings around a car instead of cutting through it. Without a `distance`, it backs off until the subject fits the free part of the screen.

The subject is the rig's parent: put the rig **inside** the object to frame (a `<Model>`, `<Group>` or shape).

```tsx title="Viewer.tsx"
import { CameraRig, Scene } from "@apricot/react";
import { Model } from "@apricot/react/model";
import { useState } from "react";
import { Button, StyleSheet, View } from "react-native";

const VIEWS = ["three-quarter", "front", "left", "back", "top"] as const;

export function Viewer() {
  const [view, setView] = useState<(typeof VIEWS)[number]>("three-quarter");
  return (
    <View style={{ flex: 1 }}>
      <Scene style={StyleSheet.absoluteFill} look="none" environment="studio" background="#0e0f12">
        <Model src={require("./sneaker.glb")} ground>
          <CameraRig view={view} inset={{ bottom: 120 }} orbit={{ elevation: [2, 70], zoom: [0.6, 1.8] }} />
        </Model>
      </Scene>
      <View style={{ position: "absolute", bottom: 40, left: 0, right: 0, flexDirection: "row", justifyContent: "center" }}>
        {VIEWS.map((v) => (
          <Button key={v} title={v} onPress={() => setView(v)} />
        ))}
      </View>
    </View>
  );
}
```

### Views

`view` is a built-in name, a name from `views`, or a view object. Changing it flies there over `transition` seconds (default 0.9).

| Built-in view | Azimuth | Elevation |
| --- | --- | --- |
| `front` | 0° | 8° |
| `back` | 180° | 8° |
| `left` | 90° | 8° |
| `right` | −90° | 8° |
| `top` | 0° | 90° |
| `three-quarter` (default) | 35° | 16° |

A view object: `{ azimuth, elevation, distance, target, position, fov }`. All angles are degrees. `target` defaults to the subject's centre; `position` places the camera exactly instead of using angles.

```tsx title="NamedViews.tsx"
import { Box, CameraRig, Scene } from "@apricot/react";

export function NamedViews({ detail }: { detail: boolean }) {
  return (
    <Scene style={{ flex: 1 }}>
      <Box size={[2, 1, 1]}>
        <CameraRig
          views={{ detail: { azimuth: 60, elevation: 10, distance: 2.2 } }}
          view={detail ? "detail" : "three-quarter"}
          transition={1.2}
        />
      </Box>
    </Scene>
  );
}
```

### Framing around your UI

`inset={{ top, bottom, left, right }}` (points) tells the rig how much of the canvas your UI covers; the subject is framed in the rest. Measure a bottom panel with `onLayout` and pass its height. `margin` (default 0.1) adds room around the subject.

### Orbit: drag, pinch and wheel

`orbit` lets the user turn the camera around the subject with one finger and pinch to zoom (the mouse wheel zooms on the web). Taps on objects still work.

| Option | Meaning (default) |
| --- | --- |
| `turn` | Degrees the camera may turn either way from the view (all the way round) |
| `elevation` | `[lowest, highest]` degrees above the horizon (`[0, 85]`: never below the floor) |
| `zoom` | `[nearest, farthest]` as fractions of the view's distance (`[0.5, 2]`), or `false` |
| `speed` | 1 turns 180° for a drag across the whole canvas (1) |
| `inertia` | Keeps turning after a flick, slowing down (`true`) |
| `returnAfter` | Seconds after the last touch to fly back to the view (stay where left) |

Setting `view` to its current value does not fly back after the user orbited. Use the rig's ref:

```tsx title="BackToView.tsx"
import { Box, CameraRig, type CameraRigHandle, Scene } from "@apricot/react";
import { useRef } from "react";
import { Button, View } from "react-native";

export function BackToView() {
  const rig = useRef<CameraRigHandle>(null);
  return (
    <View style={{ flex: 1 }}>
      <Scene style={{ flex: 1 }}>
        <Box>
          <CameraRig ref={rig} orbit />
        </Box>
      </Scene>
      <Button title="Reset view" onPress={() => rig.current?.flyTo("three-quarter")} />
    </View>
  );
}
```

`flyTo(view, seconds?)` animates; `jumpTo(view)` cuts.

#### Inside a ScrollView or pager

On Android the orbit drag keeps scroll views and pagers from taking it. iOS cannot block a scroll view from JavaScript, so turn scrolling off while a finger is on the scene:

```tsx title="InScroll.tsx"
import { CameraRig, Scene } from "@apricot/react";
import { Model } from "@apricot/react/model";
import { useState } from "react";
import { ScrollView } from "react-native";

export function InScroll() {
  const [scrolls, setScrolls] = useState(true);
  return (
    <ScrollView scrollEnabled={scrolls}>
      <Scene style={{ height: 360 }}>
        <Model src={require("./pack.glb")}>
          <CameraRig orbit onOrbitStart={() => setScrolls(false)} onOrbitEnd={() => setScrolls(true)} />
        </Model>
      </Scene>
    </ScrollView>
  );
}
```

### Following and drifting

- `follow={ref}` eases the orbit's centre after a moving object (a train, a runner). `followStiffness` (default 3) sets how tightly.
- `drift={2}` slowly circles the subject at 2° per second; it pauses while the user touches the scene.
- `subject={null}` frames nothing: give views a `distance` (and usually a `target`). Use it for big scenes such as a map or a cinema hall.

```tsx title="FollowTrain.tsx"
import { Box, CameraRig, FollowPath, Scene, type SceneObject, usePath, useTrip, type Vec3 } from "@apricot/react";
import { useRef } from "react";

const ROUTE: Vec3[] = [
  [-60, 0, 20],
  [0, 0, -10],
  [60, 0, 20],
];

export function FollowTrain() {
  const line = usePath(ROUTE);
  const trip = useTrip({ path: line, stops: [5, line.length - 5], speed: 12, dwell: 4 });
  const train = useRef<SceneObject>(null);
  return (
    <Scene style={{ flex: 1 }} camera={{ far: 1000 }}>
      <FollowPath path={line} trip={trip} ref={train}>
        <Box size={[2, 2, 8]} color="#1f8a4c" position={[0, 1, 0]} />
      </FollowPath>
      {/* After the train, so it follows this frame's position. */}
      <CameraRig follow={train} view={{ azimuth: 55, elevation: 26, distance: 40 }} drift={2} />
    </Scene>
  );
}
```

## `<FollowCamera>`: a chase camera

`<FollowCamera target={ref}>` moves the camera behind and above a target, easing after it and looking a little ahead. It is made for `<Vehicle>`, and it ignores the target's pitch.

| Prop | Meaning (default) |
| --- | --- |
| `target` | A ref to the object to chase (required) |
| `distance` | Units behind the target (6) |
| `height` | Units above the target (2.6) |
| `lookAhead` | `[ahead, up]`: the point looked at (`[3, 0.6]`) |
| `stiffness` | How quickly it catches up, per second (4) |

Its options are read once at mount. Place it **after** the target in the tree. See [Vehicles](https://matthiasbrandolin.dev/apricot/guides/vehicles).

## Related

- [Cameras reference](https://matthiasbrandolin.dev/apricot/reference/cameras).
- [Coordinates and units](https://matthiasbrandolin.dev/apricot/concepts/coordinates#camera-views).
