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> |
A fixed camera#
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.
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).
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.
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:
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:
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 adistance(and usually atarget). Use it for big scenes such as a map or a cinema hall.
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.