// scenes.jsx — Cinematic 15s timeline. Composes atmospheric backdrops with // the recreated SURVIVAL phone screens and end-card text. const G = { // Cinematic grade inkBlack: '#06080a', forestDeep: '#0a1612', forestMid: '#0e2218', forestNear: '#1a3422', mist: '#5a7568', amber: '#d8a55c', amberSoft: 'rgba(216, 165, 92, 0.45)', coldBlue: '#3a5868', primary: '#3FDB5A', white: '#e8f0e8', // Letterbox bar: '#000', }; // ── Atmospheric layers ────────────────────────────────────────────────────── function MistLayer({ opacity = 0.6, drift = 0 }) { return (
); } function Vignette({ strength = 0.6 }) { return (
); } function Letterbox() { return ( <>
); } // Tree silhouettes — feathered tops, vertical strokes function TreeBand({ y = 600, height = 200, density = 14, color = G.forestDeep, seed = 1, opacity = 1 }) { // Deterministic pseudo-random const rand = (i) => { const x = Math.sin(seed * 37 + i * 91) * 10000; return x - Math.floor(x); }; const trees = []; for (let i = 0; i < density; i++) { const x = (i / density) * 100 + (rand(i) - 0.5) * 4; const h = 0.55 + rand(i + 100) * 0.45; const w = 5 + rand(i + 200) * 7; trees.push( ); } return ( {trees} ); } // Mountain silhouettes — jagged ridge line function MountainRidge({ y = 400, height = 240, color = G.coldBlue, seed = 1, opacity = 1 }) { const rand = (i) => { const x = Math.sin(seed * 53 + i * 67) * 10000; return x - Math.floor(x); }; const points = ['0,100']; const steps = 18; for (let i = 0; i <= steps; i++) { const x = (i / steps) * 100; const peakH = 35 + rand(i) * 50; points.push(`${x},${100 - peakH}`); } points.push('100,100'); return ( ); } // Sun shafts — radial gleam suggesting golden-hour light through trees function SunShaft({ x = '60%', y = '40%', size = 600, color = G.amberSoft }) { return (
); } // Floating dust particles function Particles({ count = 30, seed = 1, opacity = 0.4 }) { const time = useTime(); const dots = []; for (let i = 0; i < count; i++) { const r1 = ((Math.sin(seed * 13 + i * 31) + 1) * 50); const r2 = ((Math.sin(seed * 7 + i * 47) + 1) * 50); const drift = Math.sin(time * 0.3 + i) * 1.5; const bob = Math.sin(time * 0.6 + i * 1.3) * 1; const size = 1.2 + ((Math.sin(seed + i * 5) + 1) * 0.8); dots.push(
); } return
{dots}
; } // ── Phone composition wrapper ────────────────────────────────────────────── // Renders an iPhone shell at given transform with the given screen inside. function PhoneFrame({ children, scale = 1, x = 0, y = 0, rotate = 0, glow = false }) { return (
{children} {/* Glass sheen */}
); } // ── SCENE 1: Cold open — atmospheric forest ──────────────────────────────── function Scene1() { const { progress, localTime } = useSprite(); const camPush = 1 + progress * 0.04; const wordmarkOpacity = interpolate([0, 0.35, 0.8, 1], [0, 1, 1, 0])(progress); const wordmarkY = interpolate([0, 0.4], [12, 0], Easing.easeOutCubic)(progress); return ( <>
{/* Wordmark */}
WHEN THE GRID GOES DARK
); } // ── SCENE 2: Home screen reveal ──────────────────────────────────────────── function Scene2() { const { progress } = useSprite(); const phoneEnter = Easing.easeOutCubic(clamp(progress * 2, 0, 1)); const phoneScale = 0.85 + 0.05 * phoneEnter; const phoneY = (1 - phoneEnter) * 100; const captionOp = interpolate([0.25, 0.45, 0.85, 1], [0, 1, 1, 0])(progress); const camPush = 1 + progress * 0.03; return ( <>
{/* Caption */}
01 / KNOW
An entire
survival library
in your pocket.
); } // ── SCENE 3: Plant ID hero close-up ──────────────────────────────────────── function Scene3() { const { progress } = useSprite(); const camPush = 1.05 + progress * 0.06; const phoneScale = 1.05 + progress * 0.03; const tagOp = interpolate([0.15, 0.35, 0.9, 1], [0, 1, 1, 0])(progress); const tagX = interpolate([0, 0.35], [-30, 0], Easing.easeOutCubic)(progress); return ( <>
{/* Side caption */}
02 / FORAGE
Identify
plants —
no signal needed.
); } // ── SCENE 4: Compass / GPS ───────────────────────────────────────────────── function Scene4() { const { progress, localTime } = useSprite(); // Compass rotates from 142° toward 0° (true north) const bearing = interpolate([0, 1], [142, 0], Easing.easeInOutCubic)(progress); const camPush = 1.02 + progress * 0.04; const captionOp = interpolate([0.2, 0.4, 0.9, 1], [0, 1, 1, 0])(progress); return ( <>
{/* Coordinates appearing as if HUD */}
03 / NAVIGATE
True north,
always.
LAT    60.4720° N
LNG    8.4689° E
ALT    1284 m
); } // ── SCENE 5: First Aid urgency ───────────────────────────────────────────── function Scene5() { const { progress } = useSprite(); const captionOp = interpolate([0.15, 0.35, 0.9, 1], [0, 1, 1, 0])(progress); // Red rim pulse const pulse = (Math.sin(progress * Math.PI * 6) + 1) * 0.5; return ( <>
{/* Caption */}
04 / TREAT
Every second
counts —
step-by-step,
offline.
); } // ── SCENE 6: Offline maps + No Signal ────────────────────────────────────── function Scene6() { const { progress } = useSprite(); const captionOp = interpolate([0.15, 0.35, 0.9, 1], [0, 1, 1, 0])(progress); // Animated download progression const dl = Math.floor(progress * 1.5); return ( <>
{/* Caption with No Signal badge */}
{/* No Signal pill */}
NO SIGNAL
05 / MAPS
Download a region.
Forget the bars.
); } // ── SCENE 7: Payoff — pull back to fjord vista ───────────────────────────── function Scene7() { const { progress } = useSprite(); const camPull = 1.3 - progress * 0.3; // Phone shrinks/recedes const phoneScale = interpolate([0, 0.5], [1.0, 0.45], Easing.easeInOutCubic)(progress); const phoneY = interpolate([0, 0.5], [-50, 100], Easing.easeInOutCubic)(progress); const phoneOpacity = interpolate([0.5, 0.75], [1, 0.5])(progress); // Tagline timing const line1Op = interpolate([0.35, 0.5, 1], [0, 1, 1])(progress); const line1Y = interpolate([0.35, 0.55], [16, 0], Easing.easeOutCubic)(progress); const line2Op = interpolate([0.6, 0.75, 1], [0, 1, 1])(progress); const line2Y = interpolate([0.6, 0.8], [16, 0], Easing.easeOutCubic)(progress); const wordmarkOp = interpolate([0.78, 0.9, 1], [0, 1, 1])(progress); return ( <>
{/* Distant peaks */} {/* Water reflection band */}
{/* Receding phone */}
{/* End-card tagline */}
Works offline.
When it matters most.
{/* Wordmark + App Store hint */}
SURVIVAL
OFFLINE SURVIVAL KIT  ·  ON THE APP STORE
); } Object.assign(window, { Scene1, Scene2, Scene3, Scene4, Scene5, Scene6, Scene7, PhoneFrame, Letterbox, MistLayer, Vignette, TreeBand, MountainRidge, SunShaft, Particles, });