// 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 (
")`,
}} />
);
}
// ── 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 */}
{/* 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
{/* 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 });