<Blur> draws its children softly, behind everything else, as if out of focus: fruit tumbling behind a sharp product, a skyline, bokeh. They are lit like the rest of the scene. The blur is drawn at a fraction of the screen's size, so it costs little.
Hero.tsx
import { Peach } from "@apricot/assets";
import { Blur, Group, Scene, Sphere } from "@apricot/react";
export function Hero() {
return (
<Scene style={{ flex: 1 }} look="soft" background="#ffffff" camera={{ position: [0, 0, 8], fov: 35 }}>
<Blur radius={6}>
<Group position={[0, 0, -6]}>
<Sphere position={[-3, 1.5, 0]} color="#ffb38a" />
<Sphere position={[2.5, -1, 0]} color="#ffd1a8" />
<Sphere position={[3, 2.5, -2]} color="#ff9f59" />
</Group>
</Blur>
<Peach size={1.1} />
</Scene>
);
}| Prop | Default | Meaning |
|---|---|---|
radius |
6 |
How soft, in points on the screen. It can change at any time |
children |
What to blur: shapes, models, instances, groups |
Rules#
- Blurred objects are always drawn behind the sharp ones, whatever their depth. Place them further back anyway (negative z), so they look right when the camera moves.
- Everything inside
<Blur>is blurred together, in one layer. Use one<Blur>per scene. - With
frameloop="demand"the blur is redrawn whenever the scene renders. Animate blurred objects withuseFrame(returningtruewhile they move) or with shared values. - Until the blurred objects' shaders are ready (the first moments after mount), the layer is blank.
Falling objects behind a product#
Instances animated in useFrame make a moving, out-of-focus background for the cost of one draw call per part. A peach spinning in front of smaller peaches tumbling down behind it:
PeachHero.tsx
import { createPeach, Peach } from "@apricot/assets";
import { disposeObject, seededRandom } from "@apricot/core";
import { Blur, FitCamera, type InstanceItem, Instances, type InstancesHandle, Scene, Spin, useFrame } from "@apricot/react";
import { useEffect, useMemo, useRef } from "react";
const COUNT = 36;
function FallingPeaches() {
// Small and out of focus: a coarse peach is plenty.
const peach = useMemo(() => createPeach({ detail: 0.15 }), []);
useEffect(() => () => disposeObject(peach), [peach]);
const fallers = useMemo(() => {
const random = seededRandom(11);
return Array.from({ length: COUNT }, () => {
const z = -2.5 - random() * 8;
const spread = 2.2 + -z * 0.42;
return {
x: (random() * 2 - 1) * spread,
y: (random() * 2 - 1) * (4 + -z * 0.5),
z,
speed: 0.5 + random() * 0.7,
spin: random() * 1.6 - 0.8,
scale: 0.38 + random() * 0.3,
};
});
}, []);
const items = useMemo<InstanceItem[]>(() => fallers.map((f) => ({ position: [f.x, f.y, f.z], scale: f.scale })), [fallers]);
const copies = useRef<InstancesHandle>(null);
const time = useRef(0);
useFrame(({ delta }) => {
const handle = copies.current;
if (!handle) return true;
time.current += delta;
fallers.forEach((f, i) => {
const height = 4 + -f.z * 0.5;
f.y -= f.speed * delta;
if (f.y < -height) f.y += height * 2;
handle.set(i, { position: [f.x, f.y, f.z], rotation: [time.current * f.spin, time.current * f.spin, 0], scale: f.scale });
});
return true;
});
return <Instances ref={copies} object={peach} items={items} />;
}
export function PeachHero() {
return (
<Scene style={{ flex: 1 }} look="soft" background="#ffffff" camera={{ position: [0, 0, 8], fov: 35 }}>
<Blur radius={4}>
<FallingPeaches />
</Blur>
<FitCamera size={[1.8, 1.8]} margin={0.1} />
<Spin speed={0.35}>
<Peach size={1.1} />
</Spin>
</Scene>
);
}