// Parallax, scroll-reveal, and atmospheric imagery helpers // Uses Unsplash Source (no API key needed) for nature backgrounds. // All images are drawn into with decoding=async & loading=lazy. // Parallax uses CSS custom properties driven by requestAnimationFrame — GPU-cheap. const { useState, useEffect, useRef } = React; // ─── Nature imagery bank (Unsplash — royalty-free, hotlinkable) // Each slot is a curated URL; swap for real brand photography later. const IMAGERY = { hero: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=1800&q=80&auto=format&fit=crop', // misty forest hero2: 'https://images.unsplash.com/photo-1511497584788-876760111969?w=1800&q=80&auto=format&fit=crop', // fog mountains programs: 'https://images.unsplash.com/photo-1465146344425-f00d5f5c8f07?w=1800&q=80&auto=format&fit=crop', // fern process: 'https://images.unsplash.com/photo-1518495973542-4542c06a5843?w=1800&q=80&auto=format&fit=crop', // sun through trees quiz: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1800&q=80&auto=format&fit=crop', // mountain lake science: 'https://images.unsplash.com/photo-1501854140801-50d01698950b?w=1800&q=80&auto=format&fit=crop', // top-down forest team: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1800&q=80&auto=format&fit=crop', // mountain range testim: 'https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=1800&q=80&auto=format&fit=crop', // golden hour field finalCta: 'https://images.unsplash.com/photo-1483728642387-6c3bdd6c93e5?w=1800&q=80&auto=format&fit=crop', // mountain dawn programBg: { 'body': 'https://images.unsplash.com/photo-1551632811-561732d1e306?w=1400&q=80&auto=format&fit=crop', // hiker from behind with backpack on mountain trail 'gi': 'https://images.unsplash.com/photo-1518495973542-4542c06a5843?w=1400&q=80&auto=format&fit=crop', // forest light — renewal 'neuro': 'https://images.unsplash.com/photo-1419242902214-272b3f66ee7a?w=1400&q=80&auto=format&fit=crop', // milky way — clarity, cognition 'regen': 'https://images.unsplash.com/photo-1502082553048-f009c37129b9?w=1400&q=80&auto=format&fit=crop', // river rock — regeneration 'hormone': 'https://images.unsplash.com/uploads/1412533519888a485b488/bb9f9777?w=1400&q=80&auto=format&fit=crop', // man walking on forest path — balance in motion 'longevity': 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1400&q=80&auto=format&fit=crop', // ancient lake — longevity } }; // ─── Global parallax driver // Tracks scrollY once per frame and writes to CSS var --sy on . // Then any element with data-parallax="N" uses calc(var(--sy) * N) in CSS for translate. function useGlobalScrollVar() { useEffect(() => { let ticking = false; const update = () => { document.documentElement.style.setProperty('--sy', window.scrollY + 'px'); document.documentElement.style.setProperty('--syN', String(window.scrollY)); ticking = false; }; const onScroll = () => { if (!ticking) { window.requestAnimationFrame(update); ticking = true; } }; window.addEventListener('scroll', onScroll, { passive: true }); update(); return () => window.removeEventListener('scroll', onScroll); }, []); } // ─── Scroll-reveal: fades + lifts children as they enter viewport. // Apply className="reveal" to any element. The observer toggles .in-view. function useScrollReveal() { useEffect(() => { const els = document.querySelectorAll('.reveal, .reveal-stagger > *'); if (!('IntersectionObserver' in window)) { els.forEach(el => el.classList.add('in-view')); return; } const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in-view'); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }); els.forEach(el => io.observe(el)); // Safety net: force-reveal anything that hasn't intersected within 300ms // (fixes cases where elements are inside transformed ancestors that confuse IO) const safety = setTimeout(() => { els.forEach(el => { const r = el.getBoundingClientRect(); if (r.top < window.innerHeight && r.bottom > 0) el.classList.add('in-view'); }); }, 300); return () => { clearTimeout(safety); io.disconnect(); }; }, []); } // ─── Parallax image layer — uses section-local scroll position for smooth motion. // speed: 0.0 = static, 0.3 = gentle, 0.6 = strong, negative = reverse. const ParallaxImage = ({ src, alt = '', speed = 0.3, scale = 1.2, overlay = 0.6, blur = 0, className = '', children, position = 'center center' }) => { const ref = useRef(null); const imgRef = useRef(null); const [offset, setOffset] = useState(0); useEffect(() => { const el = ref.current; if (!el) return; let rafId = null; const update = () => { const rect = el.getBoundingClientRect(); const vh = window.innerHeight; // -1 when bottom of section is at top of viewport; 1 when top of section at bottom. const progress = (rect.top + rect.height / 2 - vh / 2) / (vh + rect.height); setOffset(-progress * speed * 100); rafId = null; }; const onScroll = () => { if (rafId == null) rafId = window.requestAnimationFrame(update); }; window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); update(); return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); }; }, [speed]); return (
{children}
); }; // ─── Drifting particles (subtle pollen/ember motes) const DriftingParticles = ({ count = 18, color = 'rgba(201, 168, 117, 0.45)' }) => { const dots = React.useMemo(() => Array.from({ length: count }, (_, i) => ({ left: Math.random() * 100, top: Math.random() * 100, size: 1 + Math.random() * 2.5, dur: 18 + Math.random() * 28, delay: -Math.random() * 30, drift: (Math.random() - 0.5) * 40, })), [count]); return ( ); }; // ─── Ambient Ken Burns: gentle slow zoom + pan, layered under hero. const KenBurnsImage = ({ src, opacity = 0.45 }) => (