// Reusable visual primitives for the Yohann Escher hero motion. // Brand tokens, cards, lines, glows, dashboards. const YE = { bgDark: '#101214', bgDeeper: '#0B0D0E', card: '#191C1E', cardLight: '#202325', cardElev: '#23272A', cream: '#F7EFE6', creamSoft: '#FFF8F0', beige: '#E6D9C7', textMuted: 'rgba(247,239,230,0.62)', textDim: 'rgba(247,239,230,0.38)', border: 'rgba(255,255,255,0.10)', borderStrong: 'rgba(255,255,255,0.16)', orange: '#FF6B1A', orangeDeep: '#E9580C', orangeSoft: '#FFE4D2', orangeGlow: 'rgba(255,107,26,0.35)', }; const SANS = "'Manrope', 'Inter', system-ui, -apple-system, sans-serif"; const SERIF = "'Instrument Serif', 'Cormorant Garamond', Georgia, serif"; const MONO = "'JetBrains Mono', ui-monospace, monospace"; // ── Vignette / atmospheric background ────────────────────────────────────── function StageBackdrop() { return (
{/* fine grid */} {/* vignette */}
); } // ── Pulsing orange dot ────────────────────────────────────────────────────── function PulseDot({ x, y, size = 14, intensity = 1, label }) { const t = useTime(); const pulse = 0.5 + 0.5 * Math.sin(t * 3.2); const ringScale = 1 + (t * 1.6) % 2; // continuous ripple const ringOpacity = Math.max(0, 1 - ((t * 1.6) % 2) / 2) * 0.5 * intensity; return (
{label && (
{label}
)}
); } // ── Floating chip / module card ──────────────────────────────────────────── function ModuleChip({ x, y, label, icon, width = 'auto', delay = 0, life = Infinity, state = 'normal', // normal | broken | active | dim driftX = 0, driftY = 0, driftFreq = 0.5, scale = 1, highlight = false, }) { const t = useTime(); const localT = Math.max(0, t - delay); const enter = clamp(localT / 0.5, 0, 1); const eased = Easing.easeOutCubic(enter); const lifeT = life === Infinity ? 1 : clamp((t - delay) / life, 0, 1); const exit = life === Infinity ? 1 : (lifeT > 0.85 ? 1 - (lifeT - 0.85) / 0.15 : 1); // gentle float const fx = Math.sin(t * driftFreq + x * 0.01) * driftX; const fy = Math.cos(t * driftFreq * 0.9 + y * 0.01) * driftY; const broken = state === 'broken'; const dim = state === 'dim'; const active = state === 'active' || highlight; const flicker = broken ? (0.55 + 0.45 * Math.sin(t * 9 + x)) : 1; const opacity = eased * exit * flicker * (dim ? 0.45 : 1); return (
{icon && ( {icon} )} {label} {active && ( )}
); } // ── Tiny SVG icons ────────────────────────────────────────────────────────── const Icons = { bolt: , user: , msg: , chart: , doc: , funnel: , cal: , cog: , star: , spark: , check: , globe: , flow: , }; // ── Connecting line between two points (animated draw) ───────────────────── function ConnectLine({ x1, y1, x2, y2, start = 0, drawDur = 0.6, color = 'rgba(255,255,255,0.16)', dashed = false, broken = false, width = 1, glow = false, pulse = false, pulseDelay = 0, }) { const t = useTime(); const localT = Math.max(0, t - start); const draw = Easing.easeInOutCubic(clamp(localT / drawDur, 0, 1)); const dx = x2 - x1, dy = y2 - y1; const len = Math.hypot(dx, dy); if (len === 0) return null; // For broken lines, render as two segments with gap if (broken) { const gap = 0.15 + 0.1 * Math.sin(t * 2 + x1); const seg1End = 0.5 - gap / 2; const seg2Start = 0.5 + gap / 2; return ( ); } // pulse dot traveling along line let pulseEl = null; if (pulse) { const cycle = 1.4; const pT = ((t - pulseDelay) % cycle) / cycle; if (t > pulseDelay && pT >= 0) { const px = x1 + dx * pT; const py = y1 + dy * pT; pulseEl = ( ); } } return ( {pulseEl} ); } // ── Curved connector ────────────────────────────────────────────────────── function CurveConnect({ x1, y1, x2, y2, start = 0, drawDur = 0.6, color = 'rgba(255,107,26,0.5)', width = 1.2, pulse = false, pulseDelay = 0, pulseDur = 1.6 }) { const t = useTime(); const localT = Math.max(0, t - start); const draw = Easing.easeInOutCubic(clamp(localT / drawDur, 0, 1)); const mx = (x1 + x2) / 2; const my = (y1 + y2) / 2; const dx = x2 - x1; const dy = y2 - y1; // perpendicular offset for curve const len = Math.hypot(dx, dy); const ox = -dy / len * (len * 0.18); const oy = dx / len * (len * 0.18); const cx = mx + ox * 0.4; const cy = my + oy * 0.4; const path = `M ${x1} ${y1} Q ${cx} ${cy} ${x2} ${y2}`; // pulse along curve let pulseEl = null; if (pulse && t > pulseDelay) { const pT = ((t - pulseDelay) % pulseDur) / pulseDur; // approximate position on quadratic curve const u = pT; const px = (1 - u) * (1 - u) * x1 + 2 * (1 - u) * u * cx + u * u * x2; const py = (1 - u) * (1 - u) * y1 + 2 * (1 - u) * u * cy + u * u * y2; pulseEl = ( ); } return ( {pulseEl} ); } // ── Glow blob ────────────────────────────────────────────────────────────── function Glow({ x, y, size = 400, color = YE.orangeGlow, opacity = 0.6 }) { return (
); } // ── Caption / scene text ─────────────────────────────────────────────────── function SceneCaption({ children, y = 950, italicWord, italicEnd, ...rest }) { const { localTime, duration } = useSprite(); const enter = Easing.easeOutCubic(clamp(localTime / 0.5, 0, 1)); const exitStart = duration - 0.4; const exit = localTime > exitStart ? 1 - clamp((localTime - exitStart) / 0.4, 0, 1) : 1; const opacity = enter * exit; const ty = (1 - enter) * 14; return (
{children}
); } // Italic editorial fragment in orange function Em({ children }) { return ( {children} ); } // Tiny label function Eyebrow({ children, x = '50%', y = 80, opacity = 1 }) { return (
{children}
); } // ── Animated counter ─────────────────────────────────────────────────────── function CountUp({ to, from = 0, dur = 1.2, delay = 0, prefix = '', suffix = '', decimals = 0 }) { const { localTime } = useSprite(); const t = clamp((localTime - delay) / dur, 0, 1); const eased = Easing.easeOutCubic(t); const v = from + (to - from) * eased; return {prefix}{v.toFixed(decimals)}{suffix}; } Object.assign(window, { YE, SANS, SERIF, MONO, StageBackdrop, PulseDot, ModuleChip, Icons, ConnectLine, CurveConnect, Glow, SceneCaption, Em, Eyebrow, CountUp, });