// Scroll-triggered reveal + subtle parallax — IntersectionObserver driven. // Adds `.is-visible` to [data-reveal] elements when they enter the viewport, // and drives `--scroll` on [data-parallax] elements so CSS can move them. (function installReveal() { if (window.__revealInstalled) return; window.__revealInstalled = true; // One-shot reveal: add .is-visible, keep it (no flicker on scroll back). const io = new IntersectionObserver((entries) => { for (const entry of entries) { if (entry.isIntersecting) { entry.target.classList.add('is-visible'); io.unobserve(entry.target); } } }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }); // rAF-driven parallax. Updates --scroll (0..1 across viewport) per element. const parallaxEls = new Set(); let rafId = null; function tick() { rafId = null; const vh = window.innerHeight || 800; parallaxEls.forEach((el) => { const r = el.getBoundingClientRect(); // Progress: 0 when element just entered from bottom, 1 when it leaves top. const progress = 1 - (r.top + r.height * 0.5) / (vh + r.height * 0.5); el.style.setProperty('--scroll', Math.max(0, Math.min(1, progress)).toFixed(3)); }); } function onScroll() { if (rafId == null) rafId = requestAnimationFrame(tick); } // Observe any matching elements already in DOM + watch for new ones. function isInView(el) { const r = el.getBoundingClientRect(); const vh = window.innerHeight || 800; return r.bottom > 0 && r.top < vh * 0.92; } function scan(root = document) { const process = (el) => { if (el.classList.contains('is-visible')) return; // If already in view at scan time, reveal immediately (no IO dependency) if (isInView(el)) { el.classList.add('is-visible'); } else { io.observe(el); } }; if (root.nodeType === 1 && root.matches && root.matches('[data-reveal]:not(.is-visible)')) { process(root); } if (root.nodeType === 1 && root.matches && root.matches('[data-parallax]')) { parallaxEls.add(root); } if (root.querySelectorAll) { root.querySelectorAll('[data-reveal]:not(.is-visible)').forEach(process); root.querySelectorAll('[data-parallax]').forEach((el) => parallaxEls.add(el)); } tick(); } const mo = new MutationObserver((muts) => { for (const m of muts) { m.addedNodes.forEach((n) => { if (n.nodeType === 1) scan(n); }); } }); function start() { scan(); mo.observe(document.body, { childList: true, subtree: true }); window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); // Belt-and-suspenders: re-scan repeatedly for first 2s in case // React mounts after the initial scan. Cheap — scan is a qsa. let ticks = 0; const poll = setInterval(() => { scan(); if (++ticks >= 20) clearInterval(poll); // stop after 2s }, 100); // Fallback on scroll (some browsers/iframes IO can be flaky) window.addEventListener('scroll', () => scan(), { passive: true }); } if (document.body) start(); else document.addEventListener('DOMContentLoaded', start); })(); // Simple Reveal wrapper component — wraps children in a div with // `data-reveal` so the observer above handles it. function Reveal({ as: Tag = 'div', delay = 0, kind = 'up', className = '', children, ...rest }) { const ref = React.useRef(null); return ( {children} ); } // Stagger container: auto-delays direct [data-reveal] children. function Stagger({ step = 80, start = 0, children, className = '', as: Tag = 'div', ...rest }) { const childArr = React.Children.toArray(children); return ( {childArr.map((child, i) => React.isValidElement(child) ? React.cloneElement(child, { style: { ...(child.props.style || {}), '--reveal-delay': `${start + i * step}ms`, }, }) : child )} ); } Object.assign(window, { Reveal, Stagger });