PageSourceSearch

https://generare.bio/motion.js

js generare.bio collected 2026-10-06 16:23:11 UTC 4,583 bytes, 99 lines download raw bytes

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.