PageSourceSearch

https://gain.io/_astro/gainio-scripts-motion.0f3ffd5fa987.js

js gain.io collected 2026-10-05 03:55:03 UTC 6,794 bytes, 107 lines download raw bytes

1/* Motion layer v2 — CSS-first (robust): IO reveals, CSS entrances, var-driven parallax.
2   GSAP only for the pinned walkthrough (scrub:true = event-driven, no ticker dependency). */
3window.initMotion = function () {
4  /* Re-entrant: the single-page shell re-mounts sections on route change, so any
5     ScrollTrigger built against the previous DOM is killed before rebuilding. */
6  if (window.ScrollTrigger && window.ScrollTrigger.getAll) window.ScrollTrigger.getAll().forEach(function (t) { t.kill(); });
7  window.__motionInit = true;
8  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
9  document.body.classList.add('has-motion');
10
11  /* Scroll reveals retired (craft pass): section headings and body copy now render at
12     their final state on load. The only motion left on scroll belongs to the two places
13     where it carries meaning — the MCP answer lines and the mobile notification cards —
14     and both are owned by their own components. */
15  if (reduce) return;
16
17  /* Split-text headline: word-level rise, 50ms stagger (CSS keyframes) */
18  document.querySelectorAll('[data-split]').forEach((el) => {
19    if (el.querySelector('.sw')) return;
20    el.innerHTML = el.textContent.trim().split(/\s+/).map((w) => '<span class="sw"><span class="swi">' + w + '</span></span>').join(' ');
21  });
22  document.querySelectorAll('.hero-h1 .swi').forEach((w, i) => { w.style.animationDelay = (.08 + i * .05) + 's'; w.classList.add('swi-a'); });
23
24  /* Lenis smooth scroll — self-driven rAF (independent of GSAP ticker) */
25  if (window.Lenis && !window.__lenis) {
26    const lenis = new Lenis({ duration: 1.05, smoothWheel: true });
27    window.__lenis = lenis;
28    const raf = (t) => { lenis.raf(t); requestAnimationFrame(raf); };
29    requestAnimationFrame(raf);
30    if (window.ScrollTrigger) lenis.on('scroll', ScrollTrigger.update);
31  }
32
33  /* Custom cursor: disabled — native pointer only */
34  if (false) {
35    const dot = document.createElement('span'), ring = document.createElement('span');
36    dot.className = 'cur-dot'; ring.className = 'cur-ring';
37    document.body.append(dot, ring);
38    let mx = -100, my = -100, rx = -100, ry = -100, live = false;
39    addEventListener('pointermove', (e) => {
40      mx = e.clientX; my = e.clientY;
41      if (!live) { live = true; document.body.classList.add('cur-live'); rx = mx; ry = my; }
42      dot.style.transform = 'translate(' + (mx - 3.5) + 'px,' + (my - 3.5) + 'px)';
43    }, { passive: true });
44    const loop = () => { rx += (mx - rx) * .16; ry += (my - ry) * .16; ring.style.transform = 'translate(' + (rx - 17) + 'px,' + (ry - 17) + 'px)'; requestAnimationFrame(loop); };
45    requestAnimationFrame(loop);
46    addEventListener('pointerover', (e) => { ring.classList.toggle('on', !!(e.target.closest && e.target.closest('a,button,[role="button"],input,textarea,select,[data-cursor]'))); }, { passive: true });
47  }
48
49  /* Section spotlight (cursor-tracking glow) + scroll drift (gentle counter-parallax) */
50  const fxSecs = [...document.querySelectorAll('main [data-screen-label] > section')].filter((s) => s.id !== 'walkthrough');
51  fxSecs.forEach((s) => {
52    s.classList.add('sec-fx');
53    s.addEventListener('pointermove', (e) => { const r = s.getBoundingClientRect(); s.style.setProperty('--mx', (e.clientX - r.left) + 'px'); s.style.setProperty('--my', (e.clientY - r.top) + 'px'); }, { passive: true });
54  });
55  /* Hero parallax: mouse-move + scroll drift via CSS vars, speed ∝ data-depth */
56  const hero = document.getElementById('hero');
57  const layers = hero ? [...hero.querySelectorAll('[data-depth]')] : [];
58  if (hero && layers.length) {
59    if (matchMedia('(min-width: 900px) and (hover: hover)').matches) {
60      hero.addEventListener('pointermove', (e) => {
61        const r = hero.getBoundingClientRect();
62        const dx = (e.clientX - r.left) / r.width - .5, dy = (e.clientY - r.top) / r.height - .5;
63        layers.forEach((l) => { const d = +l.dataset.depth; l.style.setProperty('--px', dx * d * 26 + 'px'); l.style.setProperty('--py', dy * d * 16 + 'px'); });
64        const card = document.getElementById('hv-card');
65        if (card) { card.style.setProperty('--ry', dx * 5 + 'deg'); card.style.setProperty('--rx', -dy * 4 + 'deg'); }
66      });
67      hero.addEventListener('pointerleave', () => {
68        layers.forEach((l) => { l.style.setProperty('--px', '0px'); l.style.setProperty('--py', '0px'); });
69        const card = document.getElementById('hv-card');
70        if (card) { card.style.setProperty('--ry', '0deg'); card.style.setProperty('--rx', '0deg'); }
71      });
72    }
73    const drift = () => { const y = Math.min(window.scrollY, 900); layers.forEach((l) => l.style.setProperty('--sy', -(y * .05 * +l.dataset.depth) + 'px')); };
74    if (window.__driftHandler) removeEventListener('scroll', window.__driftHandler);
75    window.__driftHandler = drift;
76    addEventListener('scroll', drift, { passive: true });
77  }
78
79  /* Walkthrough: pinned scrub timeline on desktop (scrub:true — driven by scroll events) */
80  if (window.gsap && window.ScrollTrigger && matchMedia('(min-width: 900px)').matches) {
81    gsap.registerPlugin(ScrollTrigger);
82    const wt = document.getElementById('walkthrough');
83    if (wt) {
84      const pctEl = document.getElementById('wt-pct'), pct = { v: 0 };
85      gsap.set('#wt-row', { opacity: 0, x: -26 });
86      gsap.set('#wt-done', { opacity: 0, scale: .8 });
87      gsap.set('#wt-bar', { width: '0%' });
88      gsap.set(['#wt-step-1', '#wt-step-2'], { opacity: .38 });
89      gsap.set('#wt-rail-fill', { height: '0%' });
90      if (pctEl) pctEl.textContent = '0%';
91      const dealX = () => document.getElementById('wt-col-2').getBoundingClientRect().left - document.getElementById('wt-col-0').getBoundingClientRect().left;
92      const tl = gsap.timeline({ scrollTrigger: { trigger: wt, start: 'top top', end: '+=1600', pin: true, scrub: true } });
93      tl.to('#wt-row', { opacity: 1, x: 0, duration: .6, ease: 'power2.out' })
94        .to('#wt-step-0', { opacity: .38, duration: .3 }, '+=.3')
95        .to('#wt-step-1', { opacity: 1, duration: .3 }, '<')
96        .to('#wt-rail-fill', { height: '50%', duration: .3, ease: 'none' }, '<')
97        .to('#wt-deal', { x: dealX, duration: 1, ease: 'power2.inOut' }, '<')
98        .to('#wt-step-1', { opacity: .38, duration: .3 }, '+=.4')
99        .to('#wt-step-2', { opacity: 1, duration: .3 }, '<')
100        .to('#wt-rail-fill', { height: '100%', duration: .3, ease: 'none' }, '<')
101        .to('#wt-bar', { width: '96%', duration: 1, ease: 'power2.out' }, '<')
102        .to(pct, { v: 96, duration: 1, onUpdate: () => { if (pctEl) pctEl.textContent = Math.round(pct.v) + '%'; } }, '<')
103        .to('#wt-done', { opacity: 1, scale: 1, duration: .5, ease: 'back.out(2)' }, '-=.3')
104        .to({}, { duration: .3 });
105    }
106  }
107};

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.