← All primitives
FlyThrough
FrontierA free-flight 3D world: pilot a spaceship through an infinite wrap-around field of glowing prismatic crystals — cursor steers, hold to boost. Fog + bloom; the browser as a game engine.
A full mini-experience: mouse/WASD/scroll controls, a target-to-chase score HUD, and a toggleable generative-audio reactor. Render full-screen via next/dynamic ssr:false. The HUD uses Prism's color tokens (text-mist/border-line/bg-obsidian — copy from globals.css or restyle). Honors prefers-reduced-motion.
$
npx shadcn@latest add https://prism.icglabs.co/r/fly-through.jsonDependencies:three@react-three/fiber@react-three/postprocessing
View raw manifest →components/prism/FlyThrough.tsx
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { EffectComposer, Bloom } from "@react-three/postprocessing";
import * as THREE from "three";
import { useDeviceTier } from "@/lib/useDeviceTier";
import { AutoDpr, PauseWhenOffscreen } from "@/components/showcase/CanvasPerf";
/**
* FlyThrough — a free-flight 3D world you pilot like a spaceship through an
* infinite, wrap-around field of glowing prismatic crystals. Mouse steers
* (yaw/pitch), W/S or scroll throttles, A/D rolls, hold/Shift boosts. Chase the
* bright marker for points. Toggle the generative soundtrack and the field pulses
* to it. Fog hides the wrap; bloom makes it glow.
*
* Self-contained; renders its own HUD. Honors prefers-reduced-motion (static field).
*/
const PALETTE = ["#8b5cff", "#5b7cff", "#1fd4e6", "#ff3d81", "#ffb24d", "#9bf25a"];
type Controls = {
keys: Set<string>;
throttle: number; // wheel-adjusted base speed
boost: boolean;
};
function CrystalField({
count,
size,
reduce,
energy,
}: {
count: number;
size: number;
reduce: boolean;
energy: React.RefObject<number>;
}) {
const ref = useRef<THREE.InstancedMesh>(null);
const light = useRef<THREE.PointLight>(null);
const dummy = useMemo(() => new THREE.Object3D(), []);
const camera = useThree((s) => s.camera);
const data = useMemo(() => {
const colors = PALETTE.map((c) => new THREE.Color(c));
return Array.from({ length: count }, () => ({
base: new THREE.Vector3(
(Math.random() - 0.5) * size,
(Math.random() - 0.5) * size,
(Math.random() - 0.5) * size
),
scale: 0.35 + Math.random() * 1.9,
color: colors[(Math.random() * colors.length) | 0],
rot: new THREE.Euler(Math.random() * 6.28, Math.random() * 6.28, Math.random() * 6.28),
spin: (Math.random() - 0.5) * 0.7,
}));
}, [count, size]);
useEffect(() => {
const m = ref.current;
if (!m) return;
data.forEach((d, i) => m.setColorAt(i, d.color));
if (m.instanceColor) m.instanceColor.needsUpdate = true;
}, [data]);
useFrame((_, dt) => {
const m = ref.current;
if (!m) return;
const cam = camera.position;
const step = Math.min(dt, 0.05);
const pulse = 1 + (energy.current ?? 0) * 0.35;
for (let i = 0; i < data.length; i++) {
const d = data[i];
if (!reduce) d.rot.y += d.spin * step;
const x = d.base.x - cam.x;
const y = d.base.y - cam.y;
const z = d.base.z - cam.z;
dummy.position.set(
cam.x + (x - size * Math.round(x / size)),
cam.y + (y - size * Math.round(y / size)),
cam.z + (z - size * Math.round(z / size))
);
dummy.rotation.copy(d.rot);
dummy.scale.setScalar(d.scale * pulse);
dummy.updateMatrix();
m.setMatrixAt(i, dummy.matrix);
}
m.instanceMatrix.needsUpdate = true;
if (light.current) {
light.current.position.copy(cam);
light.current.intensity = 22 + (energy.current ?? 0) * 55;
}
});
return (
<>
<ambientLight intensity={0.5} />
<directionalLight position={[3, 5, 2]} intensity={1.0} />
<pointLight ref={light} intensity={24} distance={62} decay={1.6} color="#9fd0ff" />
<instancedMesh ref={ref} args={[undefined, undefined, count]}>
<octahedronGeometry args={[1, 0]} />
<meshStandardMaterial roughness={0.32} metalness={0.18} />
</instancedMesh>
</>
);
}
function Target({ onReach }: { onReach: () => void }) {
const ref = useRef<THREE.Mesh>(null);
const camera = useThree((s) => s.camera);
const pos = useRef(new THREE.Vector3(0, 0, -45));
const relocate = useCallback(() => {
const fwd = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
const up = new THREE.Vector3(0, 1, 0).applyQuaternion(camera.quaternion);
pos.current
.copy(camera.position)
.addScaledVector(fwd, 55 + Math.random() * 45)
.addScaledVector(right, (Math.random() - 0.5) * 44)
.addScaledVector(up, (Math.random() - 0.5) * 30);
}, [camera]);
useEffect(() => {
relocate();
}, [relocate]);
useFrame((_, dt) => {
const m = ref.current;
if (!m) return;
m.position.copy(pos.current);
m.rotation.y += dt * 1.2;
m.rotation.x += dt * 0.7;
const s = 1 + Math.sin(performance.now() * 0.004) * 0.12;
m.scale.setScalar(s);
if (camera.position.distanceTo(pos.current) < 6) {
onReach();
relocate();
}
});
return (
<mesh ref={ref}>
<icosahedronGeometry args={[2, 0]} />
<meshStandardMaterial color="#ffffff" emissive="#1fd4e6" emissiveIntensity={2.4} toneMapped={false} />
</mesh>
);
}
function Pilot({ reduce, controls }: { reduce: boolean; controls: React.RefObject<Controls> }) {
const camera = useThree((s) => s.camera) as THREE.PerspectiveCamera;
const yawV = useRef(0);
const pitchV = useRef(0);
const rollV = useRef(0);
const spd = useRef(16);
useFrame((state, dt) => {
if (reduce) return;
const c = controls.current;
if (!c) return;
const step = Math.min(dt, 0.05);
const px = Math.abs(state.pointer.x) < 0.06 ? 0 : state.pointer.x;
const py = Math.abs(state.pointer.y) < 0.06 ? 0 : state.pointer.y;
yawV.current += (-px * 0.7 - yawV.current) * 0.06;
pitchV.current += (py * 0.5 - pitchV.current) * 0.06;
const rollInput = (c.keys.has("a") ? 1 : 0) - (c.keys.has("d") ? 1 : 0);
rollV.current += (rollInput * 1.1 - rollV.current) * 0.08;
camera.rotateY(yawV.current * step);
camera.rotateX(pitchV.current * step);
camera.rotateZ(rollV.current * step);
// throttle: scroll-wheel base + W/S nudge + boost
let target = c.throttle;
if (c.keys.has("w")) target += 16;
if (c.keys.has("s")) target -= 12;
if (c.boost || c.keys.has("shift")) target += 26;
target = THREE.MathUtils.clamp(target, 4, 80);
spd.current += (target - spd.current) * 0.03;
camera.translateZ(-spd.current * step);
const fov = 56 + (spd.current - 16) * 0.45;
camera.fov = THREE.MathUtils.lerp(camera.fov, THREE.MathUtils.clamp(fov, 50, 92), 0.1);
camera.updateProjectionMatrix();
});
return null;
}
function AudioReactor({ enabled, energy }: { enabled: boolean; energy: React.RefObject<number> }) {
useEffect(() => {
if (!enabled) return;
const Ctx =
window.AudioContext ||
(window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
if (!Ctx) return;
const ctx = new Ctx();
const master = ctx.createGain();
master.gain.value = 0.0001;
master.connect(ctx.destination);
const analyser = ctx.createAnalyser();
analyser.fftSize = 256;
const lp = ctx.createBiquadFilter();
lp.type = "lowpass";
lp.frequency.value = 700;
lp.connect(master);
lp.connect(analyser);
const oscs = [110, 164.81, 220, 277.18].map((f, i) => {
const o = ctx.createOscillator();
o.type = "sawtooth";
o.frequency.value = f;
o.detune.value = (i - 1.5) * 4;
const g = ctx.createGain();
g.gain.value = 0.06;
o.connect(g).connect(lp);
o.start();
return o;
});
// tremolo LFO so the energy (and visuals) pulse rhythmically
const lfo = ctx.createOscillator();
lfo.frequency.value = 1.7;
const lfoGain = ctx.createGain();
lfoGain.gain.value = 0.22;
lfo.connect(lfoGain).connect(master.gain);
lfo.start();
master.gain.linearRampToValueAtTime(0.42, ctx.currentTime + 1.6);
ctx.resume?.();
let raf = 0;
const data = new Uint8Array(analyser.frequencyBinCount);
const tick = () => {
analyser.getByteFrequencyData(data);
let s = 0;
for (let i = 0; i < data.length; i++) s += data[i];
const v = s / data.length / 255;
energy.current = (energy.current ?? 0) * 0.7 + v * 0.3;
raf = requestAnimationFrame(tick);
};
tick();
return () => {
cancelAnimationFrame(raf);
oscs.forEach((o) => o.stop());
lfo.stop();
ctx.close();
energy.current = 0;
};
}, [enabled, energy]);
return null;
}
export function FlyThrough({ count = 460, size = 150 }: { count?: number; size?: number }) {
const [reduce, setReduce] = useState(false);
const [score, setScore] = useState(0);
const [sound, setSound] = useState(false);
const energy = useRef(0);
const controls = useRef<Controls>({ keys: new Set(), throttle: 16, boost: false });
const q = useDeviceTier();
const [low, setLow] = useState(false);
const n = Math.max(140, Math.round(count * q.scale));
const bloomOn = q.bloom && !low;
useEffect(() => {
setReduce(window.matchMedia("(prefers-reduced-motion: reduce)").matches);
const c = controls.current;
const down = (e: KeyboardEvent) => c.keys.add(e.key.toLowerCase());
const up = (e: KeyboardEvent) => c.keys.delete(e.key.toLowerCase());
const pDown = () => (c.boost = true);
const pUp = () => (c.boost = false);
const wheel = (e: WheelEvent) => {
c.throttle = THREE.MathUtils.clamp(c.throttle - e.deltaY * 0.02, 4, 70);
};
window.addEventListener("keydown", down);
window.addEventListener("keyup", up);
window.addEventListener("pointerdown", pDown);
window.addEventListener("pointerup", pUp);
window.addEventListener("pointercancel", pUp);
window.addEventListener("wheel", wheel, { passive: true });
return () => {
window.removeEventListener("keydown", down);
window.removeEventListener("keyup", up);
window.removeEventListener("pointerdown", pDown);
window.removeEventListener("pointerup", pUp);
window.removeEventListener("pointercancel", pUp);
window.removeEventListener("wheel", wheel);
};
}, []);
const reach = useCallback(() => setScore((s) => s + 1), []);
return (
<div className="relative h-full w-full" style={{ touchAction: "none" }}>
<Canvas
dpr={q.dpr}
gl={{ antialias: false, powerPreference: "high-performance" }}
camera={{ position: [0, 0, 0], fov: 56, near: 0.1, far: 200 }}
>
<color attach="background" args={["#07070c"]} />
<fog attach="fog" args={["#07070c", 12, 78]} />
<CrystalField count={n} size={size} reduce={reduce} energy={energy} />
<Target onReach={reach} />
<Pilot reduce={reduce} controls={controls} />
<AudioReactor enabled={sound} energy={energy} />
<AutoDpr onLow={() => setLow(true)} />
<PauseWhenOffscreen />
{bloomOn && (
<EffectComposer>
<Bloom intensity={0.95} luminanceThreshold={0.3} luminanceSmoothing={0.3} mipmapBlur radius={0.7} />
</EffectComposer>
)}
</Canvas>
{/* HUD */}
<div className="pointer-events-none absolute inset-0 z-10 flex flex-col justify-between p-6">
<div className="flex items-start justify-end gap-3">
<div className="rounded-full border border-line bg-obsidian/50 px-4 py-2 text-sm text-mist backdrop-blur">
Targets <span className="font-semibold text-cyan">{score}</span>
</div>
{!reduce && (
<button
onClick={() => setSound((s) => !s)}
className="pointer-events-auto rounded-full border border-line bg-obsidian/50 px-4 py-2 text-sm text-mist backdrop-blur transition-colors hover:text-ink"
>
{sound ? "♪ Sound on" : "♪ Sound off"}
</button>
)}
</div>
<div className="mx-auto rounded-full border border-line bg-obsidian/50 px-4 py-2 text-center text-xs text-mist backdrop-blur">
Mouse or drag to steer · W/S or scroll to throttle · A/D roll · hold to boost · chase the marker
</div>
</div>
</div>
);
}