// 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 (
);
}
// ── 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 (
);
}
// ── 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 (
);
}
// ── 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,
});