// scenes.jsx — v2 // Premium cinematic hero: the PERSON and the EXPERIENCE are the subject. // UI/Booking is reduced to elegant floating moments that support the story. const C = { white: '#FFFFFF', off: '#F8F4EA', // warm cream — shirt color & bright surfaces cream: '#FBF8EF', // brightest paper paper: '#F4EEDE', // card surface warm: '#E8E0CD', // deeper warm cream black: '#050505', ink: '#1A1612', // primary dark text on light inkSoft: '#3A3228', dark: '#171717', darker: '#0B0B0B', gold: '#D4AF37', goldDeep: '#A07E20', soft: '#F4E3A1', border: 'rgba(20,16,12,0.10)', text: 'rgba(26,22,18,0.88)', textDim: 'rgba(26,22,18,0.55)', textFaint: 'rgba(26,22,18,0.30)', }; const FONT_SANS = "'Manrope', 'Helvetica Neue', system-ui, sans-serif"; const FONT_SERIF = "'Cormorant Garamond', 'Times New Roman', serif"; const FONT_MONO = "'JetBrains Mono', ui-monospace, monospace"; // ── Helpers ───────────────────────────────────────────────────────────────── const lerp = (a, b, t) => a + (b - a) * t; const fadeIn = (t, dur, ease = Easing.easeOutCubic) => ease(clamp(t / dur, 0, 1)); const win = (t, inStart, inDur, outStart, outDur, eIn = Easing.easeOutCubic, eOut = Easing.easeInCubic) => { if (t < inStart) return 0; if (t < inStart + inDur) return eIn((t - inStart) / inDur); if (t < outStart) return 1; if (t < outStart + outDur) return 1 - eOut((t - outStart) / outDur); return 0; }; // ── Cinematic backdrop ────────────────────────────────────────────────────── // Two-stage atmosphere: interior warmth (0-5s) → exterior depth (5s+) function Backdrop() { const t = useTime(); const exteriorMix = clamp((t - 4.5) / 2.5, 0, 1); const interiorMix = 1 - exteriorMix; const breathe = 0.5 + 0.5 * Math.sin(t * 0.5); return (
{/* Base — warm cream gradient */}
{/* Interior: warm spotlight wash */}
{/* Subtle architectural columns hint (warm dark hairlines) */}
{/* Exterior: warm horizon glow rising */}
); } // Subtle floor reflection line — present in both phases function FloorLine({ t }) { const o = fadeIn(t - 0.3, 1.2); return (
); } // Floor reflection blur of figure function FloorReflection({ t, personX, walking }) { const o = fadeIn(t - 0.4, 1.0) * 0.5; return (
); } // ── Particles ──────────────────────────────────────────────────────────────── const PARTICLES = Array.from({ length: 32 }, (_, i) => { const r = (n) => { const x = Math.sin(i * 9301 + n * 49297) * 233280; return x - Math.floor(x); }; return { x: r(1) * 1600, y: r(2) * 900, size: 1 + r(3) * 2.6, speed: 0.03 + r(4) * 0.08, drift: 18 + r(5) * 42, phase: r(6) * Math.PI * 2, opacity: 0.25 + r(7) * 0.55, }; }); function Particles() { const t = useTime(); return (
{PARTICLES.map((p, i) => { const y = ((p.y - t * p.speed * 80) % 900 + 900) % 900; const x = p.x + Math.sin(t * 0.5 + p.phase) * p.drift; const flicker = 0.6 + 0.4 * Math.sin(t * 1.1 + p.phase * 2); return (
); })}
); } function Vignette() { return (
); } function Grain() { return (
")`, }} /> ); } // ── Person figure — elegant fashion-illustration silhouette ───────────────── // SVG paths form a coherent silhouette. Subtle gold rim-light strokes. function PersonFigure({ highlight = 0 }) { // highlight 0..1: gold rim glows brighter (used at door arrival) const rimW = 1.0 + 0.6 * highlight; const rim = `rgba(212,175,55,${0.45 + 0.45 * highlight})`; const rimSoft = `rgba(244,227,161,${0.22 + 0.3 * highlight})`; return ( 0 ? `drop-shadow(0 0 ${15 + 35 * highlight}px rgba(212,175,55,${0.3 * highlight})) drop-shadow(0 12px 30px rgba(0,0,0,0.7))` : 'drop-shadow(0 12px 30px rgba(0,0,0,0.7))', display: 'block', }}> {/* Head — clean oval silhouette */} {/* Hair sits on top — distinct darker shape with sweep */} {/* Hair sweep gold rim — right side */} {/* Subtle jaw shadow */} {/* Neck */} {/* Shirt — white triangle visible between lapels */} {/* Shirt collar wings */} {/* Tie knot */} {/* Tie body — slim, reaches to mid-jacket */} {/* Tie subtle gold thread */} {/* Suit jacket — sharper shoulders, hem at hip */} {/* Lapel sharp edges */} {/* Lapel pin — gold */} {/* Right-edge gold rim highlight on jacket */} {/* Interior shadow on jacket for volume */} {/* Pocket square — soft gold peek */} {/* Suit buttons — small gold dots on right side */} {/* Jacket vent at bottom center */} {/* RIGHT arm (figure's right, screen-left): bent holding phone at chest */} {/* Right hand */} {/* Phone held at chest — larger, more visible */} {/* Phone glow */} {/* Screen content */} {/* Gold check accent on screen */} {/* Screen bloom */} {/* Glint */} {/* LEFT arm (figure's left, screen-right): relaxed at side, hand peeks past jacket hem */} {/* Left hand */} {/* Trousers below jacket — visible from hem to ankle, with crease */} {/* Crease lines */} {/* Shoes — sleek pointed */} {/* Subtle gold ground glint near shoes when highlighted */} {highlight > 0 && ( )} ); } // ── Person container: positions + animates the figure ─────────────────────── function Person({ t }) { // Position keyframes: // 0–4: standing center (x=560, body center) // 4–8.5: gentle walk to right (x=560 → x=1100) // 8.5–10: hold near door (x=1100) // 10–12: fade slightly behind the door glow const px = interpolate( [0, 4.2, 8.4, 10.4, 12], [560, 560, 1100, 1100, 1100], Easing.easeInOutCubic )(t); // Vertical bob while walking const walkPhase = clamp((t - 4.2) / 4.2, 0, 1); const isWalking = t > 4.2 && t < 8.5; const bob = isWalking ? Math.sin((t - 4.2) * 4.5) * 4 : 0; // Subtle stride sway (tilt left/right) const sway = isWalking ? Math.sin((t - 4.2) * 4.5) * 1.5 : 0; // Scene-1: figure scale slightly smaller in interior, grows as approaching door const scale = interpolate( [0, 4.2, 8.0, 10.2, 12], [1.0, 1.0, 1.06, 1.0, 0.96], Easing.easeInOutCubic )(t); // Door arrival: gold rim highlight intensifies const highlight = clamp((t - 8.0) / 1.6, 0, 1) * clamp(1 - (t - 10.5) / 1.2, 0, 1); // Fade in const reveal = fadeIn(t - 0.2, 1.0); // Fade out at very end (subtle, walks "into" the gold) const fadeAtEnd = 1 - clamp((t - 10.8) / 0.8, 0, 1) * 0.55; // Figure dimensions (rendered) const figW = 280 * scale; const figH = 760 * scale; return ( <> {/* Floor shadow */}
); } // ── Spotlight pool that follows the person ───────────────────────────────── function Spotlight({ t }) { // Follows person from center to door const px = interpolate( [0, 4.2, 8.4, 12], [560, 560, 1100, 1100], Easing.easeInOutCubic )(t); const reveal = fadeIn(t - 0.4, 1.2); // Brighter near door const intensity = interpolate( [0, 4, 7, 9, 12], [0.55, 0.6, 0.7, 1.0, 0.9], Easing.easeInOutCubic )(t); return (
); } // ── Restaurant facade — emerges in background as person walks right ──────── function RestaurantFacade({ t }) { // Visible 5.0 onwards const reveal = fadeIn(t - 5.0, 2.0, Easing.easeOutCubic); if (reveal <= 0.01) return null; // Facade is to the right of frame, recedes in distance const baseX = 1180; const baseY = 220; const w = 520; const h = 520; // Door glow intensifies as person nears const doorGlow = clamp((t - 7.5) / 2.0, 0, 1); // Camera parallax: facade slides slightly left as person walks right const parallaxX = interpolate( [5.0, 10.5, 12], [60, 0, -20], Easing.easeInOutCubic )(t); const cols = 5; const colW = 70; const colGap = (w - cols * colW) / (cols + 1); const rows = [ { y: baseY + 30, h: 100 }, { y: baseY + 170, h: 100 }, ]; return (
{/* Facade mass — warm stone */}
{/* Cornice line — darker warm */}
{/* Windows — lit warm gold rectangles against stone */} {rows.map((row, ri) => Array.from({ length: cols }).map((_, i) => { const wx = colGap + i * (colW + colGap); const wt = fadeIn(t - 5.2 - ri * 0.15 - i * 0.08, 0.7); const flicker = 0.85 + 0.15 * Math.sin(t * 1.3 + i * 7 + ri * 3); return (
); }) )} {/* Door — large arched centerpiece, deep warm interior glowing through */}
{/* Door split */}
{/* Signage above door */}
Lumera
Buchung · Mandanten
{/* Pool of gold light at door base, spilling onto ground */}
); } // ── Distant interior architecture (visible 0-5s, then dissolves) ─────────── function InteriorArchitecture({ t }) { const reveal = win(t, 0.5, 1.0, 4.5, 1.5); if (reveal <= 0.01) return null; return (
{/* Tall vertical sconce lights / architectural columns */}
{/* Subtle wall panels */}
); } // ── Floating UI moments — minimal, support the person ────────────────────── // 1. Category pill (1.5-3.5): "Restaurant" highlighted gold // 2. Confirmation card (3.0-4.8): "19:30 · Di 19. Mai" // Both appear NEAR the person, like notifications function FloatingMoments({ t }) { // Person center x stays at 560 during scene 1, so floating UI appears to right of them. return ( <> ); } function CategoryHint({ t }) { // visible 1.4 - 3.4 const o = win(t, 1.3, 0.55, 3.2, 0.5); if (o <= 0) return null; // Stagger of 3 chips, with "Restaurant" highlighted const items = [ { label: 'Restaurant', delay: 1.4, selected: true }, { label: 'Arzttermin', delay: 1.55, selected: false }, { label: 'Beratung', delay: 1.70, selected: false }, ]; return (
— Schritt 01 · Kategorie
{items.map((it, i) => { const localT = t - it.delay; const enterT = fadeIn(localT, 0.5, Easing.easeOutBack); const x = (1 - enterT) * -30; const selGlow = it.selected ? clamp((t - 2.2) / 0.5, 0, 1) : 0; const tapPhase = it.selected ? t - 2.7 : -1; const tapPulse = tapPhase >= 0 && tapPhase < 0.7 ? 1 - tapPhase / 0.7 : 0; return (
0 ? C.ink : C.inkSoft, background: selGlow > 0 ? `linear-gradient(135deg, rgba(212,175,55,${0.40 + 0.25 * selGlow}), rgba(244,227,161,${0.30 + 0.20 * selGlow}))` : 'rgba(255,253,247,0.7)', border: `1px solid ${selGlow > 0 ? `rgba(212,175,55,${0.55 + 0.35 * selGlow})` : 'rgba(20,16,12,0.10)'}`, boxShadow: selGlow > 0 ? `0 6px 24px rgba(160,126,32,${0.18 * selGlow + 0.25 * tapPulse}), 0 0 ${10 + 22 * selGlow}px rgba(212,175,55,${0.25 * selGlow + 0.35 * tapPulse})` : '0 4px 12px rgba(60,45,18,0.06)', backdropFilter: 'blur(8px)', opacity: enterT, transform: `translateX(${x}px) scale(${1 + 0.04 * tapPulse})`, alignSelf: 'flex-start', display: 'flex', alignItems: 'center', gap: 12, minWidth: 240, }}>
0 ? C.goldDeep : 'rgba(20,16,12,0.25)', boxShadow: selGlow > 0 ? `0 0 8px rgba(212,175,55,0.7)` : 'none', }}/> {it.label}
); })}
); } function ConfirmationCard({ t }) { // appears 3.4 - 5.6, drifts away as person starts walking const o = win(t, 3.4, 0.6, 5.2, 0.7); if (o <= 0) return null; const enterT = fadeIn(t - 3.4, 0.7, Easing.easeOutCubic); const y = (1 - enterT) * 24; // Slot pulse highlight const pulsePhase = t - 4.2; const pulse = pulsePhase > 0 && pulsePhase < 0.7 ? 1 - pulsePhase / 0.7 : 0; return (
— Schritt 02 · Termin wählen
Lumière, Restaurant
19:30
Dienstag
19. Mai
{/* Mini time strip */}
{['18:30','19:00','19:30','20:00','20:30'].map(slot => { const sel = slot === '19:30'; return (
{slot}
); })}
— 2 Personen · Fensterplatz
); } // ── Final checkmark + "Termin bestätigt" overlay ─────────────────────────── function FinalConfirmation({ t }) { // Appears 10.0 onward if (t < 9.7) return null; const localT = t - 9.7; const cardIn = fadeIn(localT, 0.6, Easing.easeOutCubic); const cardY = (1 - cardIn) * 24; const ringScale = lerp(0.6, 1, Easing.easeOutBack(clamp(localT / 0.7, 0, 1))); const checkProgress = clamp((localT - 0.4) / 0.45, 0, 1); const textIn = fadeIn(localT - 0.7, 0.5); const textY = (1 - textIn) * 10; const ring1 = clamp((localT - 0.55) / 1.8, 0, 1); const ring2 = clamp((localT - 1.2) / 1.8, 0, 1); return (
{[ring1, ring2].map((r, i) => r > 0 && (
))}
Reservierung
Termin bestätigt
Lumière · Di 19. Mai · 19:30 · 2 Personen
); } // ── Root composition ────────────────────────────────────────────────────── function HeroAnimation() { const t = useTime(); return ( <> ); } Object.assign(window, { HeroAnimation, C });