// hero.jsx — Cinematic immersive Hero, fully reimagined const { useState: _useState, useEffect: _useEffect, useRef: _useRef, useMemo } = React; // ---------- Mascot (Burkina flag head + 3D body) ---------- // Tête animée : // - Idle : balance gauche/droite toutes les 5s (animation CSS) // - Mouse-aware : suit légèrement la souris (±8° de rotation max) // - Hover : pause idle pour laisser le tracking souris dominer // // PNG splits : // - assets/mascot-body.png : corps en costume + bras + ticket Faso Travel // - assets/mascot-head.png : tête loupe drapeau BF (avec manche) function Mascot({ size = 380 }) { const containerRef = React.useRef(null); const [tilt, setTilt] = React.useState({ rot: 0, y: 0 }); const [active, setActive] = React.useState(false); React.useEffect(() => { // Respect prefers-reduced-motion : pas de tracking souris si l'user // demande des animations réduites (accessibilité). if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; let raf = 0; let target = { rot: 0, y: 0 }; const handle = (e) => { const rect = containerRef.current?.getBoundingClientRect(); if (!rect) return; const cx = rect.left + rect.width / 2; const cy = rect.top + rect.height * 0.25; // ancre sur la zone tête (pas centre du body) const dx = (e.clientX - cx) / Math.max(400, window.innerWidth); const dy = (e.clientY - cy) / Math.max(400, window.innerHeight); target = { rot: Math.max(-1, Math.min(1, dx * 2)) * 8, y: Math.max(-1, Math.min(1, dy * 2)) * 4, }; if (!raf) { raf = requestAnimationFrame(() => { setTilt(target); raf = 0; }); } }; window.addEventListener('mousemove', handle); return () => { window.removeEventListener('mousemove', handle); if (raf) cancelAnimationFrame(raf); }; }, []); // translate3d force la couche GPU (compositor-only) → animation fluide // sans repaint. Cohérent avec @keyframes mascot-head-idle dans styles.css. const headStyle = { transform: `translate3d(-50%, ${tilt.y.toFixed(2)}px, 0) rotate(${tilt.rot.toFixed(2)}deg)`, }; return (
setActive(true)} onMouseLeave={() => setActive(false)} aria-label="Mascotte FasoTravel — Burkina explorateur" > Mascotte FasoTravel
); } // ---------- Cinematic bus ---------- function DrivingBus() { return (
FASOTRAVEL 11 BF 226
{Array.from({ length: 12 }).map((_, i) => ( ))}
); } // ---------- Birds ---------- function Birds() { return (
{Array.from({ length: 5 }).map((_, i) => ( ))}
); } // ---------- Sun rays ---------- function SunRays() { return ( {Array.from({ length: 9 }).map((_, i) => { const cx = 200 + i * 160; return ( ); })} ); } // ---------- Floating dust motes ---------- function HeroMotes() { const motes = useMemo(() => Array.from({ length: 28 }).map((_, i) => ({ x: ((i * 37) + (i*i*7)) % 100, y: ((i * 53) + 13) % 100, s: 1 + (i % 4) * 0.6, dur: 12 + (i % 8) * 1.8, del: (i % 5) * -2.5, })), []); return (
{motes.map((m, i) => ( ))}
); } // ---------- HeroScene ---------- // Background : photo réelle "Burkina Faso sur la carte d'Afrique de l'Ouest" // fournie par l'utilisateur (assets/burkina-map.jpg). Les couches SVG // précédentes (montagnes, sun, étoiles) sont conservées en superposition très // atténuée pour la profondeur, mais ne dominent plus la scène. function HeroScene() { const y = useScrollY(); const px = (mult) => Math.min(y * mult, 400); return (
{/* Photo de fond — Burkina Faso planté sur la carte Afrique de l'Ouest */}
{/* Overlay sombre dégradé pour garantir la lisibilité du texte par-dessus */}
{/* Sun rays */} {/* Stars */} {Array.from({ length: 80 }).map((_, i) => { const x = (i * 31 + (i*i)%17) % 100; const yy = ((i * 23) % 60); const r = 0.4 + ((i % 6) * 0.22); const op = 0.4 + ((i * 11) % 6) * 0.08; return ; })} {/* Shooting stars */} {[0,1,2,3].map(i => (
))} {/* Sun disk */}
{/* Distant mountain layer */} {/* Mid mountain layer */} {/* Near mountain layer */} {/* Atmospheric fog bands */}
{/* Horizon road + bus — épuré : route en perspective sobre, plus de traînée LED RGB qui traversait la scène. La voie centrale guide juste l'œil vers le point de fuite à l'horizon. */}
{/* Sol terre rouge sahélienne — gradient simple, pas de bandes */} {/* Bande de route en perspective vers le point de fuite */} {/* Lignes blanches discontinues centrales (vraie marque routière) */} {/* Quelques arbres sahéliens éparpillés (plus discrets, moins nombreux) */}
); } // Salutations rotatives — incarne l'identité culturelle dès le 1er regard function GreetingRotator({ greetings }) { const [idx, setIdx] = _useState(0); _useEffect(() => { if (!greetings || greetings.length < 2) return; const id = setInterval(() => setIdx((i) => (i + 1) % greetings.length), 2400); return () => clearInterval(id); }, [greetings]); if (!greetings || greetings.length === 0) return null; const cur = greetings[idx]; return (
{cur.text} — {cur.lang}
); } function Hero({ t, onNotify }) { const heroLogoParallax = useParallax(-0.06); const heroSceneParallax = useParallax(0.12); return (
{/* LogoCoin animé (spin + halo) — déplacé en floating top-right pour * laisser le Mascot prendre la place principale du hero sans perdre * l'animation signature du logo. Pointer-events:none pour ne pas * bloquer les interactions avec ce qui est dessous. */}
{t.hero.eyebrow}

{t.hero.title1}
{t.hero.title2}

{t.hero.sub}

{/* LANDING-WEB-CTA (déploiement — 2026-06-18) : bouton principal qui ouvre directement l'app passager web (app.fasotravel.app). */}
{t.stats.slice(0, 4).map((s, i) => (
{s.l}
))}
{/* variant="blur" et non "mask" car la tête est positionnée avec * top:-25% (dépasse au-dessus de sa bbox) et le mask clip-path * coupe les débordements. Le blur fade-in n'a pas ce souci. */}
); } Object.assign(window, { HeroScene, Hero, DrivingBus, Birds, SunRays, HeroMotes, GreetingRotator, Mascot });