Most apps never need this page. Reach for it when no shape, model or behavior does the job.
Your own behavior: defineBehavior#
A behavior is a component that wraps children and moves them every frame, like <Spin>. defineBehavior turns a frame function into one. Every prop accepts a plain value or a shared value.
import { defineBehavior } from "@apricot/react";
/** Floats its children up and down. */
export const Bob = defineBehavior({
name: "Bob",
defaults: { height: 0.2, speed: 1.5 },
frame: (object, { height, speed }, { time }) => {
object.position.y = Math.sin(time * speed * Math.PI * 2) * height;
return true;
},
});import { Scene, Spin, Torus } from "@apricot/react";
import { Bob } from "./Bob";
export const Floating = () => (
<Scene style={{ flex: 1 }} look="studio">
<Bob height={0.3}>
<Spin speed={0.8}>
<Torus material="gold" />
</Spin>
</Bob>
</Scene>
);| Field | Meaning |
|---|---|
name |
The component's display name, and the wrapping group's name |
defaults |
Every prop with its default value. The keys become the component's props |
frame(object, props, state) |
Runs before each frame. object is a group wrapping the children; props are the current values (shared values already read); state is { time, delta, frame } |
Return true from frame while something still moves, so the scene keeps rendering under frameloop="demand"; return false when it is at rest. Change the wrapping group (its position, rotation, scale), not the children, so behaviors stack.
Spin is built the same way:
export const Spin = defineBehavior({
name: "Spin",
defaults: { speed: 1, axis: [0, 1, 0] as Vec3 },
frame: (object, { speed, axis }, { delta }) => stepSpin(object, speed, axis, delta),
});For one-off logic in a single component, useFrame is simpler: see Animation.
Your own geometry: <Mesh>#
<Mesh geometry> draws any three.js BufferGeometry with a preset material, and takes the same props as the shapes (material, color, onPress, transforms, shadows).
import { Mesh } from "@apricot/react";
import { useEffect, useMemo } from "react";
import * as THREE from "three/webgpu";
function starShape(points = 5, outer = 1, inner = 0.45) {
const shape = new THREE.Shape();
for (let i = 0; i < points * 2; i++) {
const r = i % 2 === 0 ? outer : inner;
const a = (i / (points * 2)) * Math.PI * 2 + Math.PI / 2;
if (i === 0) shape.moveTo(Math.cos(a) * r, Math.sin(a) * r);
else shape.lineTo(Math.cos(a) * r, Math.sin(a) * r);
}
return shape;
}
export function Star({ color = "#e9bd63" }: { color?: string }) {
const geometry = useMemo(() => {
const g = new THREE.ExtrudeGeometry(starShape(), { depth: 0.2, bevelEnabled: true, bevelSize: 0.04, bevelThickness: 0.04 });
g.center();
return g;
}, []);
// The geometry is yours: the mesh does not dispose it.
useEffect(() => () => geometry.dispose(), [geometry]);
return <Mesh geometry={geometry} material="gold" color={color} />;
}- Import three.js from
three/webgpu(Metro resolves plainthreethere too, withwithApricot). - Build the geometry once (
useMemo) and dispose it on unmount. Rebuilding it every render rebuilds the mesh's buffers every render. - Geometries you make are yours: Apricot never disposes them. See Objects and lifetime.
One draw call for a multi-part object: mergeColoredParts#
Each mesh costs a draw call. mergeColoredParts bakes several geometries, each with its own colour, into one geometry with vertex colours. Draw it with vertexColors: true and a white material. The apple, peach and toy car in @apricot/assets are built this way.
import { mergeColoredParts } from "@apricot/core";
import { Mesh } from "@apricot/react";
import { useEffect, useMemo } from "react";
import * as THREE from "three/webgpu";
export function Mushroom() {
const geometry = useMemo(
() =>
mergeColoredParts([
{ geometry: new THREE.CylinderGeometry(0.18, 0.24, 0.6, 24), color: "#f3ead8" },
{
geometry: new THREE.SphereGeometry(0.5, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2),
color: "#d9342b",
matrix: new THREE.Matrix4().makeTranslation(0, 0.25, 0),
},
]),
[],
);
useEffect(() => () => geometry.dispose(), [geometry]);
return <Mesh geometry={geometry} material={{ preset: "clay", color: "#ffffff", vertexColors: true }} />;
}A part's color is a colour, a function of each vertex's position (for gradients), or "vertex" to keep the geometry's own color attribute. matrix places the part. UVs are dropped, so merged objects cannot take image textures.
Existing three.js objects: <Primitive>#
<Primitive object> adds any three.js Object3D as it is, with transform props and children. Use it for objects from the @apricot/assets factories (createCar(), createTrees()) or from other three.js code.
import { Primitive } from "@apricot/react";
import { disposeObject } from "@apricot/core";
import { useEffect, useMemo } from "react";
import * as THREE from "three/webgpu";
export function Lamp() {
const lamp = useMemo(() => {
const group = new THREE.Group();
const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 2), new THREE.MeshStandardNodeMaterial({ color: "#222222" }));
pole.position.y = 1;
group.add(pole);
return group;
}, []);
useEffect(() => () => disposeObject(lamp), [lamp]);
return <Primitive object={lamp} position={[1, 0, 0]} />;
}disposeObject(object) frees every geometry, material and texture under an object. Call it on unmount for objects you built.
Your own material presets: definePreset#
Register a preset once, at startup (for example next to your theme), and use its name anywhere a preset is accepted:
import { definePreset } from "@apricot/react";
definePreset("brand-orange", { color: "#ff7a1a", roughness: 0.35, metalness: 0, clearcoat: 0.6, clearcoatRoughness: 0.1 });
definePreset("frosted", { color: "#ffffff", roughness: 0.35, metalness: 0, transmission: 1, ior: 1.4, thickness: 0.2 });A PresetSpec needs color, roughness and metalness, and can add clearcoat, clearcoatRoughness, brushed, flakes, transmission, ior, thickness, attenuationColor, attenuationDistance, emissive (a colour or "color") and emissiveIntensity. Registering an existing name replaces it. See Materials reference.
Your own studio: defineEnvironment#
An environment is a procedural studio: a three-colour sky and up to six soft boxes, evaluated in the shader.
import { defineEnvironment } from "@apricot/react";
defineEnvironment("warm-studio", {
sky: ["#5a4a3c", "#2a221c", "#0d0b09"],
boxes: [
{ direction: [0.4, 0.6, 0.7], size: [0.3, 0.15], intensity: 6, color: "#ffe2c0" },
{ direction: [-0.9, 0.1, 0.4], size: [0.06, 0.6], intensity: 4, color: "#fff4e6" },
],
intensity: 1,
});Then <Scene environment="warm-studio"> or <Environment name="warm-studio" />. A box's direction points from the object towards the box (+z is towards the default camera); size is its half width and half height in radians. See Lighting.
Your own scene components#
Components built on the scene use the same hooks Apricot's own do:
| Hook | Use |
|---|---|
useScene() |
The scene: invalidate(), camera, scene, size, onResize, ready |
useFrame(cb) |
Per-frame code; return true while moving |
useAnimatable(value) |
Read a plain-or-shared value inside useFrame |
useParent() / useAttach(object) |
Attach a three.js object to the nearest Apricot parent while mounted |
ParentContext |
Make your object the parent of your component's children |
useTransform(object, props) |
Apply position, rotation, scale props (plain or shared) |
import { type Animatable, ParentContext, useAnimatable, useAttach, useFrame } from "@apricot/react";
import { type ReactNode, useState } from "react";
import * as THREE from "three/webgpu";
/** Scales its children with a heartbeat. */
export function Pulse({ rate = 1, children }: { rate?: Animatable<number>; children?: ReactNode }) {
const [group] = useState(() => new THREE.Group());
useAttach(group);
const readRate = useAnimatable(rate);
useFrame(({ time }) => {
const beat = 1 + Math.max(0, Math.sin(time * Math.PI * 2 * readRate())) * 0.08;
group.scale.setScalar(beat);
return true;
});
return <ParentContext.Provider value={group}>{children}</ParentContext.Provider>;
}All of these must run inside <Scene>. See Hooks reference.
Related#
- Objects and lifetime.
- Core reference:
mergeColoredParts,disposeObject,seededRandom. - Performance.