Objects drop over an existing screen for a few seconds and pile up at the bottom, without leaving the screen. Use it for a celebration, a reward or an empty-state moment.
Copy FallingPileOverlay.tsx into your app, then wrap a screen:
InboxScreen.tsx
import { Apple } from "@apricot/assets";
import { FallingPileOverlay } from "./FallingPileOverlay";
import { YourScreen } from "./YourScreen";
export function InboxScreen() {
return (
<FallingPileOverlay count={45} duration={5} items={<Apple />}>
<YourScreen />
</FallingPileOverlay>
);
}The component#
FallingPileOverlay.tsx
// Apricot recipe: falling-pile
// Copy this file into your app. Wrap any screen's content; 3D objects drop
// over it for `duration` seconds and pile up at the bottom. Touches go
// through to the screen below, and the 3D layer stops rendering once the pile
// has settled.
import { Pile, type PileProps, Scene } from "@apricot/react";
import type { ReactNode } from "react";
import { StyleSheet, View, type ViewStyle } from "react-native";
export interface FallingPileOverlayProps extends Omit<PileProps, "children"> {
/** The screen the objects fall over. */
children: ReactNode;
/** What to drop, such as <Apple />. */
items: ReactNode;
style?: ViewStyle;
/** Shows the fps / render-time HUD. */
debug?: boolean;
}
export function FallingPileOverlay({ children, items, style, debug, ...pile }: FallingPileOverlayProps) {
return (
<View style={[{ flex: 1 }, style]}>
{children}
{/* A narrow field of view keeps objects round near the screen edges. */}
<Scene
transparent
pointerEvents="none"
style={StyleSheet.absoluteFill}
camera={{ position: [0, 0, 20], fov: 30 }}
debug={debug}
>
<Pile scale={[0.58, 0.68]} {...pile}>
{items}
</Pile>
</Scene>
</View>
);
}It needs only @apricot/react (and @apricot/assets for the ready-made items).
Props#
Every <Pile> prop except children, plus:
| Prop | Meaning |
|---|---|
children |
The screen the objects fall over |
items |
What to drop: one or more meshes, such as <Apple /> |
style |
Style for the wrapping view (it has flex: 1) |
debug |
Shows the fps and render-time HUD |
Notes#
- Over the UI: the scene is transparent and
pointerEvents="none", so the screen underneath stays interactive (scrolling, buttons). - Physics: the objects collide with each other and the screen edges. Each kind of object is one draw call, however many there are.
- Battery: once the pile has settled, the scene stops rendering (
onSettledfires). A layout change, such as a rotation, wakes it so the pile adapts to the new screen. - Mix objects: pass several items, such as
items={<><Apple /><Apple color="#7bc043" /></>}. Any mesh works; its collision radius isuserData.radius, or else its bounding sphere. - Size:
scaleis relative to each item's own size; in this setup the screen is about 10 units tall. - Only when needed: mount the overlay when the moment happens (a reward was earned) and unmount it afterwards, or change its
keyto replay.
Variations#
Coins for a payment confirmation:
Paid.tsx
import { Coin } from "@apricot/assets";
import { FallingPileOverlay } from "./FallingPileOverlay";
import { Receipt } from "./Receipt";
export const Paid = () => (
<FallingPileOverlay count={36} duration={2.5} items={<Coin />} onSettled={() => console.log("settled")}>
<Receipt />
</FallingPileOverlay>
);