1// Short, one-time entrances. Content is visible if JavaScript or the observer is unavailable. 2export function initScrollMotion() { 3 if (!('IntersectionObserver' in window)) return; 4 const preference = window.matchMedia('(prefers-reduced-motion: reduce)'); 5 const elements = [...document.querySelectorAll('.hero-copy, .science-grid h2, .science-copy, .library-column, .process-heading, .process-steps li, .team-copy, .team-photos figure, .events-intro, .footer-wordmark')]; 6 const revealed = new WeakSet(); 7 const observer = new IntersectionObserver(entries => { 8 for (const entry of entries) { 9 if (!entry.isIntersecting || preference.matches) continue; 10 revealed.add(entry.target); 11 entry.target.classList.add('motion-enter'); 12 observer.unobserve(entry.target); 13 } 14 }, {threshold: 0.12}); 15 function syncPreference() { 16 observer.disconnect(); 17 for (const element of elements) { 18 if (preference.matches) element.classList.remove('motion-enter'); 19 else if (!revealed.has(element)) observer.observe(element); 20 } 21 } 22 preference.addEventListener('change', syncPreference); 23 syncPreference(); 24} 25 26// Native sticky positioning carries the hold/reveal; only the artwork has inertia. 27export function initHeroZoom() { 28 const hero = document.querySelector('.hero'); 29 const stage = hero?.closest('.hero-stage'); 30 const science = document.querySelector('#science'); 31 if (!stage || !science) return; 32 const preference = window.matchMedia('(prefers-reduced-motion: reduce)'); 33 const mobile = window.matchMedia('(max-width: 700px)'); 34 const clamp = value => Math.max(0, Math.min(1, value)); 35 const ease = value => { const p = clamp(value); return p * p * (3 - 2 * p); }; 36 let frame = 0, previousTime = 0, progress = 0, heroHeight = 1, hold = 1; 37 let active = false; 38 39 function render() { 40 const pull = ease(progress / (mobile.matches ? 0.7 : 0.85)); 41 const departure = ease((progress - (mobile.matches ? 0.22 : 0.3)) / 0.65); 42 hero.style.setProperty('--hero-zoom', (1 + pull * (mobile.matches ? 0.09 : 0.17)).toFixed(5)); 43 hero.style.setProperty('--hero-copy-y', `${(-departure * (mobile.matches ? 24 : 42)).toFixed(2)}px`); 44 hero.style.setProperty('--hero-copy-opacity', (1 - departure).toFixed(4)); 45 hero.style.setProperty('--hero-cue-opacity', (1 - ease(progress / 0.22)).toFixed(4)); 46 47 } 48 49 function tick(time) { 50 const bounds = stage.getBoundingClientRect(); 51 const target = clamp(-bounds.top / (hold + heroHeight)); 52 const delta = previousTime ? Math.min(time - previousTime, 64) : 16; 53 previousTime = time; 54 // Exponential damping settles in about 350ms, independent of display refresh rate. 55 progress += (target - progress) * (1 - Math.exp(-delta / 90)); 56 if (Math.abs(target - progress) < 0.0001) progress = target; 57 render(); 58 if (progress !== target) frame = requestAnimationFrame(tick); 59 else { frame = 0; previousTime = 0; } 60 } 61 function schedule() { 62 if (active && !frame) frame = requestAnimationFrame(tick); 63 } 64 function measure() { 65 heroHeight = hero.offsetHeight; 66 hold = heroHeight * (mobile.matches ? 0.18 : 0.5); 67 stage.style.setProperty('--hero-height', `${heroHeight}px`); 68 science.style.setProperty('--hero-overlap', `${heroHeight}px`); 69 stage.style.setProperty('--hero-hold', `${hold}px`); 70 schedule(); 71 } 72 function syncPreference() { 73 active = !preference.matches; 74 if (frame) cancelAnimationFrame(frame); 75 frame = 0; 76 previousTime = 0; 77 stage.classList.toggle('hero-stage-motion', active); 78 science.classList.toggle('science-reveal', active); 79 window.removeEventListener('scroll', schedule); 80 window.removeEventListener('resize', measure); 81 if (active) { 82 measure(); 83 // Restored scroll positions and deep links start in their completed visual state. 84 progress = clamp(-stage.getBoundingClientRect().top / (hold + heroHeight)); 85 render(); 86 window.addEventListener('scroll', schedule, {passive:true}); 87 window.addEventListener('resize', measure); 88 } else { 89 for (const name of ['--hero-zoom','--hero-copy-y','--hero-copy-opacity','--hero-cue-opacity']) hero.style.removeProperty(name); 90 stage.style.removeProperty('--hero-height'); 91 stage.style.removeProperty('--hero-hold'); 92 science.style.removeProperty('--hero-overlap'); 93 } 94 } 95 preference.addEventListener('change', syncPreference); 96 // Font swapping can change the hero height on narrow/short viewports. 97 document.fonts?.ready.then(() => { if (active) measure(); }); 98 syncPreference(); 99}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.