PageSourceSearch

https://refect.app/static/dist-v/1790357571/landing/landingMotion.js

js refect.app collected 2026-09-28 06:15:58 UTC 16,014 bytes, 375 lines download raw bytes

1let cleanupLandingMotion = null;
2/* ─── Shared single-RAF scroll scheduler ────────────────────────────────────
3   All scroll-driven functions register here. One scroll listener → one RAF
4   → all updates in the same frame (reads first, then writes = no thrashing).
5   ─────────────────────────────────────────────────────────────────────────── */
6let _rafId = 0;
7let _lastScrollY = -1;
8const _updateQueue = [];
9function _flushUpdates() {
10    _rafId = 0;
11    const sy = window.scrollY;
12    if (sy === _lastScrollY)
13        return;
14    _lastScrollY = sy;
15    for (const fn of _updateQueue)
16        fn();
17}
18function _scheduleScroll() {
19    if (_rafId !== 0)
20        return;
21    _rafId = window.requestAnimationFrame(_flushUpdates);
22}
23function _addUpdate(fn) {
24    _updateQueue.push(fn);
25    if (_updateQueue.length === 1) {
26        window.addEventListener('scroll', _scheduleScroll, { passive: true });
27    }
28    return () => {
29        const idx = _updateQueue.indexOf(fn);
30        if (idx >= 0)
31            _updateQueue.splice(idx, 1);
32        if (_updateQueue.length === 0) {
33            window.removeEventListener('scroll', _scheduleScroll);
34            if (_rafId !== 0) {
35                window.cancelAnimationFrame(_rafId);
36                _rafId = 0;
37            }
38            _lastScrollY = -1;
39        }
40    };
41}
42/* ─────────────────────────────────────────────────────────────────────────── */
43function revealOnScroll() {
44    const revealElements = Array.from(document.querySelectorAll('[data-landing-reveal]'));
45    if (revealElements.length === 0) {
46        return () => { };
47    }
48    revealElements.forEach((element, index) => {
49        const delay = element.dataset['revealDelay'] ?? String((index % 4) * 90);
50        element.style.transitionDelay = `${delay}ms`;
51    });
52    const observer = new IntersectionObserver((entries) => {
53        entries.forEach((entry) => {
54            if (!entry.isIntersecting)
55                return;
56            const target = entry.target;
57            target.classList.remove('opacity-0', 'translate-y-6');
58            target.classList.add('opacity-100', 'translate-y-0');
59            observer.unobserve(target);
60        });
61    }, {
62        threshold: 0.18,
63        rootMargin: '0px 0px -8% 0px',
64    });
65    revealElements.forEach((element) => observer.observe(element));
66    return () => observer.disconnect();
67}
68function initStoryScroller() {
69    const scrollRoot = document.querySelector('[data-story-scroll-root]');
70    const panels = Array.from(document.querySelectorAll('[data-story-panel]'));
71    const visuals = Array.from(document.querySelectorAll('[data-story-visual]'));
72    const progressContainer = document.querySelector('[data-story-progress]');
73    const progressFill = document.querySelector('[data-story-progress-fill]');
74    const progressDot = document.querySelector('[data-story-progress-dot]');
75    const isDesktop = window.matchMedia('(min-width: 1024px)');
76    if (!scrollRoot || panels.length === 0 || visuals.length === 0 || !isDesktop.matches) {
77        return () => { };
78    }
79    const maxStage = Math.max(1, panels.length - 1);
80    let progress = 0;
81    let target = 0;
82    let animRafId = 0;
83    let scrollableDistance = 1;
84    // Cache element position — avoids getBoundingClientRect in hot scroll path
85    let cachedRootTop = 0;
86    let cachedProgressHeight = 0;
87    const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
88    const updateProgress = (value) => {
89        if (!progressContainer || !progressFill || !progressDot)
90            return;
91        const snapped = Math.round(value);
92        const ratio = snapped / maxStage;
93        const padding = cachedProgressHeight * 0.06;
94        const trackHeight = Math.max(0, cachedProgressHeight - padding * 2);
95        const top = padding + ratio * trackHeight;
96        progressDot.style.top = `${top.toFixed(1)}px`;
97        progressFill.style.height = '100%';
98    };
99    const setStageState = (element, offset) => {
100        const absOffset = Math.abs(offset);
101        const opacity = Math.max(0.18, 1 - absOffset * 0.55);
102        const translateY = offset * 22;
103        const scale = 1 - Math.min(absOffset, 1) * 0.02;
104        element.style.opacity = opacity.toFixed(3);
105        element.style.transform = `translate3d(0, ${translateY.toFixed(1)}px, 0) scale(${scale.toFixed(3)})`;
106        element.style.pointerEvents = opacity > 0.6 ? 'auto' : 'none';
107    };
108    const paint = () => {
109        panels.forEach((panel) => {
110            panel.style.opacity = '1';
111            panel.style.transform = 'translate3d(0, 0, 0) scale(1)';
112            panel.style.pointerEvents = 'auto';
113        });
114        visuals.forEach((visual, visualIndex) => {
115            setStageState(visual, visualIndex - progress);
116            visual.style.zIndex = String(10 - Math.round(Math.abs(visualIndex - progress) * 2));
117        });
118        updateProgress(progress);
119    };
120    const animate = () => {
121        const delta = target - progress;
122        progress += delta * 0.18;
123        if (Math.abs(delta) < 0.001)
124            progress = target;
125        paint();
126        if (Math.abs(target - progress) < 0.001) {
127            animRafId = 0;
128            return;
129        }
130        animRafId = window.requestAnimationFrame(animate);
131    };
132    const requestRender = () => {
133        if (animRafId === 0)
134            animRafId = window.requestAnimationFrame(animate);
135    };
136    const updateLayout = () => {
137        scrollableDistance = Math.max(1, scrollRoot.offsetHeight - window.innerHeight * 0.6);
138        // Re-cache position on resize (offsetTop avoids forced layout during scroll)
139        cachedRootTop = scrollRoot.getBoundingClientRect().top + window.scrollY;
140        if (progressContainer)
141            cachedProgressHeight = progressContainer.offsetHeight;
142    };
143    // Scroll update: uses cached position — no getBoundingClientRect
144    const handleScroll = () => {
145        const raw = (window.scrollY - cachedRootTop) / scrollableDistance;
146        target = clamp(raw, 0, 1) * maxStage;
147        requestRender();
148    };
149    const handleResize = () => { updateLayout(); handleScroll(); };
150    updateLayout();
151    handleScroll();
152    window.addEventListener('resize', handleResize);
153    const removeScrollUpdate = _addUpdate(handleScroll);
154    return () => {
155        if (animRafId !== 0)
156            window.cancelAnimationFrame(animRafId);
157        removeScrollUpdate();
158        window.removeEventListener('resize', handleResize);
159    };
160}
161function initParallax() {
162    const parallaxElements = Array.from(document.querySelectorAll('[data-landing-parallax]'));
163    if (parallaxElements.length === 0)
164        return () => { };
165    let items = [];
166    const cachePositions = () => {
167        items = parallaxElements.map((el) => ({
168            el,
169            speed: Number(el.dataset['landingParallax'] ?? '0'),
170            offsetTop: el.getBoundingClientRect().top + window.scrollY,
171            height: el.offsetHeight,
172        }));
173    };
174    const update = () => {
175        const sy = window.scrollY;
176        const viewportCenter = window.innerHeight / 2;
177        for (const { el, speed, offsetTop, height } of items) {
178            const elementCenter = offsetTop - sy + height / 2;
179            const offset = Math.max(-28, Math.min(28, (viewportCenter - elementCenter) * speed));
180            el.style.transform = `translate3d(0, ${offset.toFixed(1)}px, 0)`;
181        }
182    };
183    const handleResize = () => { cachePositions(); };
184    cachePositions();
185    update();
186    window.addEventListener('resize', handleResize);
187    const removeScrollUpdate = _addUpdate(update);
188    return () => {
189        removeScrollUpdate();
190        window.removeEventListener('resize', handleResize);
191    };
192}
193function initStrengthSectionExpand() {
194    const section = document.querySelector('[data-landing-strength-section]');
195    const background = section?.querySelector('[data-landing-strength-bg]');
196    if (!section || !background)
197        return () => { };
198    const isDesktop = window.matchMedia('(min-width: 768px)');
199    const COLLAPSED_SCALE = 0.68;
200    let cachedTop = 0;
201    let cachedHeight = 0;
202    let lastScale = -1;
203    const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
204    const easeOutCubic = (value) => 1 - Math.pow(1 - value, 3);
205    const cachePosition = () => {
206        const rect = section.getBoundingClientRect();
207        cachedTop = rect.top + window.scrollY;
208        cachedHeight = rect.height;
209    };
210    const paint = () => {
211        if (!isDesktop.matches) {
212            if (lastScale !== 1) {
213                background.style.transform = 'translate3d(0, 0, 0) scaleX(1)';
214                background.style.borderRadius = '0px';
215                lastScale = 1;
216            }
217            return;
218        }
219        const top = cachedTop - window.scrollY;
220        const bottom = top + cachedHeight;
221        const entryProgress = clamp((window.innerHeight - top) / (window.innerHeight * 0.62), 0, 1);
222        const exitProgress = clamp((bottom - window.innerHeight * 0.18) / (window.innerHeight * 0.5), 0, 1);
223        const progress = easeOutCubic(Math.min(entryProgress, exitProgress));
224        const scale = COLLAPSED_SCALE + (1 - COLLAPSED_SCALE) * progress;
225        const roundedScale = Number(scale.toFixed(3));
226        if (roundedScale === lastScale)
227            return;
228        lastScale = roundedScale;
229        background.style.borderRadius = '32px';
230        background.style.transform = `translate3d(0, 0, 0) scaleX(${roundedScale})`;
231    };
232    const handleResize = () => {
233        cachePosition();
234        lastScale = -1;
235        paint();
236    };
237    cachePosition();
238    paint();
239    window.addEventListener('resize', handleResize);
240    const removeScrollUpdate = _addUpdate(paint);
241    return () => {
242        removeScrollUpdate();
243        window.removeEventListener('resize', handleResize);
244        background.style.removeProperty('transform');
245        background.style.removeProperty('border-radius');
246        background.style.removeProperty('will-change');
247    };
248}
249function initStickyMediaCta() {
250    const root = document.querySelector('[data-sticky-cta-root]');
251    if (!root)
252        return () => { };
253    const steps = Array.from(root.querySelectorAll('[data-sticky-cta-step]'));
254    const media = Array.from(root.querySelectorAll('[data-sticky-cta-media]'));
255    const sticky = root.querySelector('[data-sticky-cta-sticky]');
256    const track = root.querySelector('[data-sticky-cta-track]');
257    const isDesktop = window.matchMedia('(min-width: 1024px)');
258    if (!isDesktop.matches || steps.length === 0 || media.length === 0)
259        return () => { };
260    const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
261    const primeMediaImages = () => {
262        root.querySelectorAll('img').forEach((image) => {
263            image.loading = 'eager';
264            image.decoding = 'async';
265            if (typeof image.decode === 'function') {
266                void image.decode().catch(() => { });
267            }
268        });
269    };
270    const state = {
271        segmentHeight: 0,
272        rootTop: 0,
273        numItems: Math.min(steps.length, media.length),
274        stepCenters: [],
275        maxBlockShift: 0,
276        lastPaintKey: '',
277    };
278    const updateLayout = () => {
279        state.numItems = Math.min(steps.length, media.length);
280        if (state.numItems === 0)
281            return;
282        // All reads first
283        const rootRect = root.getBoundingClientRect();
284        state.rootTop = window.scrollY + rootRect.top;
285        state.segmentHeight = root.offsetHeight / state.numItems;
286        state.stepCenters = steps.slice(0, state.numItems).map((step) => {
287            const stepRect = step.getBoundingClientRect();
288            return stepRect.top - rootRect.top + stepRect.height / 2;
289        });
290        const firstCenter = state.stepCenters[0] ?? 0;
291        const lastCenter = state.stepCenters[state.stepCenters.length - 1] ?? firstCenter;
292        state.maxBlockShift = Math.max(0, lastCenter - firstCenter);
293        if (state.maxBlockShift < 8) {
294            state.maxBlockShift = state.segmentHeight * Math.max(0, state.numItems - 1) * 0.58;
295        }
296        // All writes after reads
297        if (sticky && state.segmentHeight > 0) {
298            sticky.style.height = `${state.segmentHeight.toFixed(1)}px`;
299            sticky.style.top = `calc(50vh - ${(state.segmentHeight / 2).toFixed(1)}px)`;
300        }
301        if (track)
302            track.style.willChange = 'transform';
303        state.lastPaintKey = '';
304    };
305    const update = () => {
306        if (state.numItems === 0 || state.segmentHeight <= 0)
307            return;
308        // All reads first
309        const scrollAnchor = window.scrollY + window.innerHeight * 0.5;
310        // Calculations (pure math, no DOM reads)
311        const rawProgress = (scrollAnchor - state.rootTop - state.segmentHeight * 0.5) / state.segmentHeight;
312        const progress = clamp(rawProgress, 0, state.numItems - 1);
313        const activeIndex = clamp(Math.round(progress), 0, state.numItems - 1);
314        const normalizedProgress = state.numItems > 1 ? progress / (state.numItems - 1) : 0;
315        const trackShift = (normalizedProgress * 2 - 1) * 92;
316        const lowerIndex = Math.floor(progress);
317        const upperIndex = Math.min(state.numItems - 1, lowerIndex + 1);
318        const mix = progress - lowerIndex;
319        const lowerCenter = state.stepCenters[lowerIndex] ?? 0;
320        const upperCenter = state.stepCenters[upperIndex] ?? lowerCenter;
321        const interpolatedCenter = lowerCenter + (upperCenter - lowerCenter) * mix;
322        const baseCenter = state.stepCenters[0] ?? interpolatedCenter;
323        const blockShift = clamp(interpolatedCenter - baseCenter, 0, state.maxBlockShift);
324        const paintKey = [
325            activeIndex,
326            progress.toFixed(3),
327            blockShift.toFixed(1),
328            trackShift.toFixed(1),
329        ].join('|');
330        if (paintKey === state.lastPaintKey)
331            return;
332        state.lastPaintKey = paintKey;
333        // All writes last
334        if (track)
335            track.style.transform = `translate3d(0, ${blockShift.toFixed(1)}px, 0)`;
336        media.forEach((item, i) => {
337            const distance = i - progress;
338            const absDistance = Math.abs(distance);
339            const opacity = i === activeIndex ? 1 : 0;
340            const translateY = distance * 168 + trackShift;
341            const scale = 1 - Math.min(absDistance, 1.2) * 0.08;
342            item.style.opacity = opacity.toFixed(3);
343            item.style.transform = `translate3d(0, ${translateY.toFixed(1)}px, 0) scale(${scale.toFixed(3)})`;
344            item.style.pointerEvents = i === activeIndex ? 'auto' : 'none';
345            item.style.zIndex = String(30 - Math.round(Math.abs(distance) * 10));
346        });
347        steps.forEach((step, i) => {
348            step.style.opacity = i === activeIndex ? '1' : '0.55';
349        });
350    };
351    const handleResize = () => { updateLayout(); update(); };
352    updateLayout();
353    primeMediaImages();
354    update();
355    window.addEventListener('resize', handleResize);
356    const removeScrollUpdate = _addUpdate(update);
357    return () => {
358        removeScrollUpdate();
359        window.removeEventListener('resize', handleResize);
360    };
361}
362export function initLandingMotion() {
363    cleanupLandingMotion?.();
364    const cleanups = [
365        revealOnScroll(),
366        initStrengthSectionExpand(),
367        initStoryScroller(),
368        initStickyMediaCta(),
369        initParallax(),
370    ];
371    cleanupLandingMotion = () => {
372        cleanups.forEach((cleanup) => cleanup());
373        cleanupLandingMotion = null;
374    };
375}

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.