export function initReveal() { const items = document.querySelectorAll('[data-reveal]'); if (!('IntersectionObserver' in window)) { for (const item of items) item.classList.add('is-visible'); return; } const groups = new Map(); const observer = new IntersectionObserver( (entries) => { // things that come into view together get a small stagger let index = 0; for (const entry of entries) { if (!entry.isIntersecting) continue; for (const item of groups.get(entry.target)) { item.style.setProperty('--delay', `${Math.min(index++ * 90, 450)}ms`); item.classList.add('is-visible'); } observer.unobserve(entry.target); } }, { rootMargin: '0px 0px -6% 0px' }, ); for (const item of items) { // Chrome applies the element's own clip-path, so a fully clipped heading never // counts as intersecting. Watch the parent for those instead. const target = item.dataset.reveal === 'mask' ? item.parentElement : item; if (!groups.has(target)) groups.set(target, []); groups.get(target).push(item); observer.observe(target); } } // Fallback for browsers without scroll-driven animations export function initTimeline() { const timeline = document.querySelector('[data-timeline]'); if (!timeline || CSS.supports('animation-timeline: view()')) return; if (matchMedia('(prefers-reduced-motion: reduce)').matches) return; let ticking = false; const update = () => { const rect = timeline.getBoundingClientRect(); const progress = (innerHeight * 0.75 - rect.top) / rect.height; timeline.style.setProperty('--progress', Math.min(1, Math.max(0, progress)).toFixed(3)); ticking = false; }; addEventListener( 'scroll', () => { if (!ticking) requestAnimationFrame(update); ticking = true; }, { passive: true }, ); update(); }