<Pile> drops count copies of its children from above the visible area over duration seconds. They collide with each other and the screen edges, roll and heap up at the bottom, and never leave the screen. Each kind of object is drawn in one call, however many fall. Once everything rests the scene stops rendering and onSettled fires.
import { Apple } from "@apricot/assets";
import { Pile, Scene } from "@apricot/react";
export function AppleDrop() {
return (
<Scene style={{ flex: 1 }} look="soft" camera={{ position: [0, 0, 20], fov: 30 }}>
<Pile count={45} duration={5} scale={[0.58, 0.68]}>
<Apple />
</Pile>
</Scene>
);
}How it works#
- Area. By default the pile fills what the camera sees,
depth(0.5) units deep on each side of z = 0. It follows resizes, so after a rotation the pile re-forms for the new screen. Passbounds={{ min: [x, y, z], max: [x, y, z] }}to use a fixed box instead; its floor ismin[1]. - Children are templates. Each child is one kind of object, not drawn itself. Their own
position,rotationandscaleare ignored: every copy gets a random place, tumble and size. Only meshes work (shapes,<Mesh>,<Apple>,<Coin>,<Peach>); a<Group>or<Model>is skipped with a warning. - Collisions treat every object as a sphere. Its radius is the mesh's
userData.radiusif set (the fruit assets set it), or else its geometry's bounding sphere. - Size.
scaleis a random range relative to each child's own size (default[0.9, 1.1]). With the camera at[0, 0, 20]and fov 30°, the screen is about 10 units tall, so[0.58, 0.68]makes a 1-unit apple about a sixteenth of the screen height.
Mixing kinds#
Give several children; each copy picks one of them at random.
import { Apple, Peach } from "@apricot/assets";
import { Pile } from "@apricot/react";
export const Fruit = () => (
<Pile count={60} duration={4}>
<Apple />
<Apple color="#7bc043" />
<Peach />
</Pile>
);Over a screen#
Make the scene transparent and let touches through, so the screen below stays usable while objects fall on it. The falling-pile recipe wraps this as a component.
import { Coin } from "@apricot/assets";
import { Pile, Scene } from "@apricot/react";
import type { ReactNode } from "react";
import { StyleSheet, View } from "react-native";
export function Celebrate({ children }: { children: ReactNode }) {
return (
<View style={{ flex: 1 }}>
{children}
<Scene transparent pointerEvents="none" style={StyleSheet.absoluteFill} camera={{ position: [0, 0, 20], fov: 30 }}>
<Pile count={40} duration={3} scale={[0.6, 0.7]}>
<Coin />
</Pile>
</Scene>
</View>
);
}Replaying and removing#
The drop starts when the pile mounts and restarts when an option changes (options are compared by value, so inline arrays do not restart it). To replay it, change its key; to remove it, unmount it.
import { Apple } from "@apricot/assets";
import { Pile, Scene } from "@apricot/react";
import { useState } from "react";
import { Button, StyleSheet, View } from "react-native";
export function Replay() {
const [round, setRound] = useState(0);
const [done, setDone] = useState(false);
return (
<View style={{ flex: 1 }}>
<Scene style={StyleSheet.absoluteFill} camera={{ position: [0, 0, 20], fov: 30 }}>
<Pile key={round} count={30} duration={3} onSettled={() => setDone(true)}>
<Apple />
</Pile>
</Scene>
{done && (
<Button
title="Again"
onPress={() => {
setDone(false);
setRound((r) => r + 1);
}}
/>
)}
</View>
);
}Options#
| Prop | Default | Meaning |
|---|---|---|
count |
required | How many objects to drop in total |
duration |
5 |
Seconds over which they drop |
bounds |
the visible area | { min, max } box to fill. Changing it moves the walls without restarting |
depth |
0.5 |
Half the thickness of the default area |
scale |
[0.9, 1.1] |
Random size range, relative to each child |
speed |
2 |
Downward speed when objects appear, units per second |
spin |
4 |
Largest initial tumble, radians per second |
seed |
1 |
Random seed: the same seed gives the same drop. Change it for a different one |
physics |
see below | Solver settings |
onSettled |
Called once everything has dropped and rests |
physics tunes the solver:
| Field | Default | Effect |
|---|---|---|
gravity |
[0, -15, 0] |
Units per second² |
friction |
0.6 |
Friction with the floor (walls have none) |
bodyFriction |
0.15 |
Friction between objects. High values stack them in towers instead of heaps |
damping |
0.05 |
Speed lost per second |
contactDamping |
4 |
Extra damping while touching something, so objects stop rolling |
restSpeed |
0.12 |
Below this speed an object counts as resting |
settleTime |
0.4 |
Seconds everything must rest before the pile sleeps |
maxSpeed |
40 |
Speed limit |
iterations |
2 |
Solver passes per step; more keeps deep piles tighter |
maxSubstep |
1/120 |
Longest simulation step, seconds |
Cost#
The solver is plain JavaScript on the JS thread. A few dozen objects are cheap; a few hundred keep the JS thread busy until they settle. Prefer fewer, larger objects.
Related#
- falling-pile recipe.
- Motion reference.
- Assets:
Apple,Peach,Coin.