This page builds one screen in six steps. Each step is complete code you can paste into an Expo screen. It assumes Installation is done.
1. A scene#
<Scene> is a React Native view that draws 3D. Give it a size like any view.
import { Scene } from "@apricot/react";
export default function Home() {
return <Scene style={{ flex: 1 }} />;
}An empty scene shows its background (a soft cream, from the default look="soft"). The camera sits at [0, 0, 6] and looks at the origin.
2. A shape with a material#
Shapes are centred on their position. Sizes are in scene units: think of one unit as one metre.
import { Box, Scene, Sphere } from "@apricot/react";
export default function Home() {
return (
<Scene style={{ flex: 1 }}>
<Box size={1.4} radius={0.15} position={[-1, 0, 0]} material="car-paint" color="#1f4fd1" />
<Sphere radius={0.7} position={[1.1, 0, 0]} material="chrome" />
</Scene>
);
}materialis a preset name. There are 16, such as"chrome","gold","glass","car-paint","rubber". See Shapes and materials.coloroverrides the preset's colour.
3. Better reflections#
Metals reflect their surroundings. environment="studio" adds a procedural photo studio (no image to load), and look="none" removes the default lights so the studio is the only light, which suits shiny objects.
import { Box, Scene, Sphere } from "@apricot/react";
export default function Home() {
return (
<Scene style={{ flex: 1 }} look="none" environment="studio" background="#0e0f12">
<Box size={1.4} radius={0.15} position={[-1, 0, 0]} material="car-paint" color="#1f4fd1" />
<Sphere radius={0.7} position={[1.1, 0, 0]} material="chrome" />
</Scene>
);
}4. Motion#
Behaviors are components that move their children. <Spin> turns them around an axis, in radians per second.
import { Box, Scene, Spin } from "@apricot/react";
export default function Home() {
return (
<Scene style={{ flex: 1 }} look="none" environment="studio" background="#0e0f12">
<Spin speed={0.6}>
<Box size={1.4} radius={0.15} material="car-paint" color="#1f4fd1" />
</Spin>
</Scene>
);
}The scene renders only while something moves. When nothing changes, it sleeps and costs nothing.
5. A swipe that spins it#
Animatable props (position, rotation, scale, speed…) take a plain value or a Reanimated shared value. Shared values are read by the render loop directly, so a gesture never re-renders React.
import { Box, Group, Scene, Spin } from "@apricot/react";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { cancelAnimation, useDerivedValue, useSharedValue, withDecay } from "react-native-reanimated";
export default function Home() {
const angle = useSharedValue(0);
const rotation = useDerivedValue<readonly [number, number, number]>(() => [0, angle.value, 0]);
const swipe = Gesture.Pan()
.onBegin(() => cancelAnimation(angle))
.onChange((e) => {
angle.value += e.changeX * 0.012; // radians per point
})
.onEnd((e) => {
angle.value = withDecay({ velocity: e.velocityX * 0.012, deceleration: 0.997 });
});
return (
<GestureDetector gesture={swipe}>
<Scene style={{ flex: 1 }} look="none" environment="studio" background="#0e0f12">
<Spin speed={0.6}>
<Group rotation={rotation}>
<Box size={1.4} radius={0.15} material="car-paint" color="#1f4fd1" />
</Group>
</Spin>
</Scene>
</GestureDetector>
);
}<Spin> adds a steady turn and the <Group> adds the swipe on top: nesting composes motion. The same pattern is packaged as a hook in the swipe-to-spin recipe.
6. Taps, colour changes and a loading state#
Every shape takes onPress. Changing color animates the material (over 0.4 s) without rebuilding its shader. loading shows your own view until the scene has drawn its first frame.
import { Box, Group, Scene, Spin } from "@apricot/react";
import { useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { cancelAnimation, useDerivedValue, useSharedValue, withDecay } from "react-native-reanimated";
const COLORS = ["#1f4fd1", "#a8102a", "#e3a21a", "#2f6b3a"];
export default function Home() {
const [color, setColor] = useState(0);
const angle = useSharedValue(0);
const rotation = useDerivedValue<readonly [number, number, number]>(() => [0, angle.value, 0]);
const swipe = Gesture.Pan()
.onBegin(() => cancelAnimation(angle))
.onChange((e) => {
angle.value += e.changeX * 0.012;
})
.onEnd((e) => {
angle.value = withDecay({ velocity: e.velocityX * 0.012, deceleration: 0.997 });
});
return (
<View style={{ flex: 1, backgroundColor: "#0e0f12" }}>
<GestureDetector gesture={swipe}>
<Scene
style={{ flex: 1 }}
look="none"
environment="studio"
background="#0e0f12"
loading={<ActivityIndicator color="#ffffff" style={{ flex: 1 }} />}
>
<Spin speed={0.6}>
<Group rotation={rotation}>
<Box
size={1.4}
radius={0.15}
material="car-paint"
color={COLORS[color]}
onPress={() => setColor((c) => (c + 1) % COLORS.length)}
/>
</Group>
</Spin>
</Scene>
</GestureDetector>
<Text style={{ color: "#8b9099", textAlign: "center", padding: 24 }}>
Swipe to spin · tap to repaint
</Text>
</View>
);
}The four ideas behind it#
- React for structure, shared values for motion. Mount, unmount and choose things with React state. Animate with shared values,
useFrameor behaviors. Never set React state every frame. (Animation) - The scene sleeps when nothing changes. Props, shared values and taps wake it. Code that changes objects directly calls
useScene().invalidate()or returnstruefromuseFrame. (Scenes) - Units are metres, angles depend on the API. Object rotations are radians; camera views are degrees. (Coordinates and units)
- Repeated things are one draw call. Use
<Instances>for many copies, and keep materials shared. (Performance)
Next#
- Core concepts explain each idea in depth.
- Guides cover one task each: models, cameras, lighting, gestures.
- Recipes are complete screens to copy.