These docs are written so an AI assistant can read them and build what you ask for. This page is the short version: the rules, the import map and the mistakes to avoid. It is the first page in llms.txt.
Give your assistant the docs#
| You use | Give it |
|---|---|
| An assistant that can read URLs (Claude, ChatGPT, Cursor, Claude Code…) | https://matthiasbrandolin.dev/apricot/llms.txt: an index of every page, each linked as Markdown. |
| An assistant with a large context, or offline | https://matthiasbrandolin.dev/apricot/llms-full.txt: every page in one file. |
| One topic | Any page's Markdown: add .md to its URL (/apricot/guides/camera.md), or use Copy page at the top of the page. |
A prompt that works well:
Read https://matthiasbrandolin.dev/apricot/llms.txt and the pages it links
that matter for this task. Follow the rules in "Building with AI" exactly.
Build me: a product screen where a gold coin spins slowly, speeds up when I
swipe, and shows its name in a label above it. Expo SDK 57, TypeScript.Rules for writing Apricot code#
These are the facts that most often decide whether generated code works. Follow all of them.
-
Import from the right entry point.
- Everything is imported from
@apricot/react, exceptModel,useModel,useTexture,useTextureStateanddefineModelKit. Those come from@apricot/react/model. - Ready-made objects (
MoneyStack,Coin,Apple,Peach,Car,Card,Trees,Ground,ContactShadow,Backdrop,StudioFloor,CARD_SIZE…) come from@apricot/assets. - Utilities without React (
seededRandom,disposeObject,Path,Trip) come from@apricot/core. - App code does not import
three. Use shapes, models, materials and refs instead. The exception is building custom geometry (see Custom geometry and behaviors).
- Everything is imported from
-
<Scene>needs a size. Give itstyle={{ flex: 1 }}or a height, like any view. Everything 3D goes inside a<Scene>, and hooks such asuseFrameanduseSceneonly work in components rendered inside one. -
Most
<Scene>props are read once, at mount. These arelook,camera,fog,environment,transparent,background,shadows,maxPixelRatioandlinger; changing them later does nothing. Instead:- change reflections with
<Environment>, - change the time of day with
<Daylight>, - move the camera with
<CameraRig>or<FitCamera>, - remount the scene with a new
keyfor anything else.
frameloopis the one Scene prop that can change after mount. - change reflections with
-
Animate without React renders. Animatable props accept a plain value or a Reanimated shared value (
useSharedValue,useDerivedValue). Drive per-frame logic withuseFrame. Never callsetStateevery frame. -
The scene sleeps when nothing changes (
frameloop="demand").- In
useFrame, returntruewhile something is still animating, andfalseor nothing when it is done. - After changing an object directly (through a ref) outside
useFrame, calluseScene().invalidate().
- In
-
Units and angles:
- Distances are scene units, meant as metres. Y is up. The camera looks down −z by default, so a model's front faces +z.
- Object
rotationis Euler angles in radians[x, y, z].<Spin speed>is radians per second. - Camera views (
azimuth,elevation), orbit limits and jointrangeare in degrees.fovis degrees. - Colours are CSS strings (
"#ff9f59") or anything three.js accepts.
-
Many copies of the same thing are one
<Instances>, not a.map()of shapes. Each separate mesh costs about 80 µs per frame on a mid-range Android phone. -
Models are
.glbfiles bundled withrequire:<Model src={require("./car.glb")} />.- Optimise them first with
npx apricot-models ./models -o ./assets/models. Draco, meshopt and KTX2 cannot be decoded on the device. groundstands a model on y = 0.- Use a model kit (
defineModelKit) to name parts, then refer to those names inmaterialsandjoints.
- Optimise them first with
-
Gestures wrap the scene. Put
<Scene>inside aGestureDetector(react-native-gesture-handler) and write shared values in the gesture callbacks. The app root needsGestureHandlerRootView. -
Overlays:
<Scene transparent pointerEvents="none" style={StyleSheet.absoluteFill}>draws 3D over a screen and lets touches through to it. -
Order matters for cameras that follow. Put
<FollowCamera>(and<CameraRig follow>) after the object they follow, so they read this frame's position. -
Do not invent props. Every prop is listed in the API reference. If something is not there, build it with
useFrame, refs ordefineBehavior. -
It needs a development build. Expo Go cannot run Apricot.
Import map#
// Scenes, shapes, lights, cameras, motion, hooks, types
import {
Scene, Group, Primitive, Mesh, Box, Sphere, Cylinder, Cone, Plane, Torus, Capsule,
AmbientLight, HemisphereLight, DirectionalLight, PointLight, SpotLight,
Environment, Daylight, Blur, Label, Instances,
FitCamera, CameraRig, FollowCamera,
Spin, Vehicle, Pile, FollowPath,
useFrame, useScene, useAnimatable, usePath, useTrip, useTripStatus, useSceneTouches,
useMaterial, defineBehavior, definePreset, defineEnvironment, createMaterial,
type SceneObject, type Vec3, type CameraRigHandle, type InstancesHandle,
} from "@apricot/react";
// Models and textures (separate entry point: it carries the glTF loader)
import { Model, defineModelKit, useModel, useTexture, type PartPressEvent } from "@apricot/react/model";
// Ready-made procedural objects
import { MoneyStack, Coin, Apple, Peach, Car, Card, CARD_SIZE, Trees, Ground, ContactShadow, Backdrop, StudioFloor } from "@apricot/assets";
// Engine utilities (no React)
import { seededRandom, disposeObject } from "@apricot/core";Components at a glance#
| Need | Use | Page |
|---|---|---|
| A 3D view | <Scene> |
Scene |
| Basic shapes | <Box>, <Sphere>, <Cylinder>, <Cone>, <Plane>, <Torus>, <Capsule>, <Mesh geometry> |
Objects |
| Group and move things together | <Group position rotation scale> |
Objects |
| What things are made of | material="chrome" or { preset, color, roughness, metalness } |
Materials |
| Lighting | look, environment, <Environment>, <Daylight>, light components, shadows |
Lights |
| Frame an object at any screen size | <FitCamera size> |
Cameras |
| Named views, flying between them, drag to orbit | <CameraRig view orbit> |
Cameras |
| Chase camera | <FollowCamera target> |
Cameras |
A .glb model: paint parts, open doors, play clips |
<Model kit materials joints animations> |
Model |
| Spin something | <Spin speed axis> |
Motion |
| Anything per frame | useFrame(({ delta }) => …) |
Hooks |
| A reusable behavior | defineBehavior({ name, defaults, frame }) |
Motion |
| Hundreds of copies, tappable | <Instances items onPress> |
Instances |
| Objects that fall and pile up | <Pile count> |
Motion |
| A drivable toy car | <Vehicle throttle steer> |
Motion |
| A vehicle on a route with stops | usePath, useTrip, <FollowPath> |
Motion |
| A React Native view pinned to a 3D point | <Label attach position> |
Labels and blur |
| Out-of-focus background | <Blur radius> |
Labels and blur |
Common mistakes#
| Wrong | Right | Why |
|---|---|---|
import { Model } from "@apricot/react" |
import { Model } from "@apricot/react/model" |
The model loader is a separate entry point. |
rotation={[0, 90, 0]} |
rotation={[0, Math.PI / 2, 0]} |
Object rotations are radians. |
<CameraRig view={{ azimuth: Math.PI / 4 }} /> |
<CameraRig view={{ azimuth: 45 }} /> |
Camera angles are degrees. |
setState in useFrame or a gesture to move things |
A shared value passed to the prop, or a ref changed in useFrame |
React renders per frame are slow, and the scene does not need them. |
useFrame(() => { ref.current.rotation.y += 0.01 }) |
useFrame(({ delta }) => { ref.current.rotation.y += delta; return true; }) |
Use delta (seconds) for frame-rate independence, and return true to keep rendering. |
{seats.map(s => <Box key={s.id} … />)} for hundreds of seats |
<Instances items={seats}><Box … /></Instances> |
One draw call instead of hundreds. |
<Scene look={dark ? "studio" : "soft"}> to switch looks |
<Scene key={dark ? "dark" : "light"} look={…}> or <Environment name={…} /> |
Scene options are read once at mount. |
<Scene> with no style |
<Scene style={{ flex: 1 }}> |
A view with no size draws nothing. |
<FollowCamera target={car} /> before <Vehicle ref={car}> |
Put it after the vehicle | It must read the car's position after the car moved this frame. |
src="./car.glb" |
src={require("./car.glb")} |
Bundled models are loaded from the asset module. A string is fetched as a URL. |
Shiny metal under look="soft" with no environment |
look="none" environment="studio" |
Metals need something to reflect. |
A complete example to learn from#
A swipeable gold coin with a label, using rules 1, 2, 4, 6 and 9:
import { Coin } from "@apricot/assets";
import { Group, Label, Scene, Spin } from "@apricot/react";
import { Text } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { cancelAnimation, useDerivedValue, useSharedValue, withDecay } from "react-native-reanimated";
export function CoinScreen() {
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 (
<GestureDetector gesture={swipe}>
<Scene style={{ flex: 1 }} look="none" environment="studio" background="#0e0f12" camera={{ position: [0, 0, 4] }}>
<Spin speed={0.5}>
<Group rotation={rotation}>
{/* A coin lies flat; stand it up to face the camera. */}
<Coin radius={1} rotation={[Math.PI / 2, 0, 0]} />
</Group>
</Spin>
<Label position={[0, 1.3, 0]}>
<Text style={{ color: "#ffffff", fontWeight: "600" }}>Gold coin</Text>
</Label>
</Scene>
</GestureDetector>
);
}