(() => { 'use strict'; const desktopQuery = window.matchMedia('(min-width: 1024px)'); let trigger = null; let marks = []; let rafId = null; let lastProgress = -1; const COUNT = 60; const MAX_DISTANCE = 15; const getState = (index, progress, count) => { const active = progress * (count - 1); const distance = Math.abs(index - active); const intensity = Math.max(0, 1 - distance / MAX_DISTANCE); return { backgroundColor: `rgba(255,255,255,${intensity})`, height: 12 + intensity * 22 }; }; const updateMarks = (progress, immediate = false) => { if (!marks.length) return; const count = marks.length; const useRAF = !immediate && Math.abs(progress - lastProgress) > 0.01; if (useRAF && rafId === null) { rafId = requestAnimationFrame(() => { rafId = null; lastProgress = progress; marks.forEach((mark, index) => { const props = getState(index, progress, count); gsap.to(mark, { ...props, duration: 0.2, overwrite: true }); }); }); return; } if (immediate) { lastProgress = progress; marks.forEach((mark, index) => { const props = getState(index, progress, count); gsap.set(mark, props); }); } }; const initDesktopRuler = () => { if (trigger) return; if (typeof gsap === 'undefined' || typeof ScrollTrigger === 'undefined') { return; } gsap.registerPlugin(ScrollTrigger); const ruler = document.querySelector('.ruler_marks'); if (!ruler) return; const existingMarks = ruler.querySelectorAll('span'); if (existingMarks.length === 0) { const fragment = document.createDocumentFragment(); const initialProgress = (() => { const doc = document.documentElement; const max = doc.scrollHeight - doc.clientHeight; return max > 0 ? doc.scrollTop / max : 0; })(); for (let i = 0; i < COUNT; i++) { const mark = document.createElement('span'); const { backgroundColor, height } = getState(i, initialProgress, COUNT); mark.style.backgroundColor = backgroundColor; mark.style.height = `${height}px`; fragment.appendChild(mark); marks.push(mark); } ruler.appendChild(fragment); } else { marks = Array.from(existingMarks); } trigger = ScrollTrigger.create({ start: 'top top', end: 'bottom bottom', scrub: 0.5, onUpdate: self => updateMarks(self.progress) }); const refreshHandler = () => { if (trigger) updateMarks(trigger.progress, true); }; ScrollTrigger.addEventListener('refresh', refreshHandler); updateMarks(trigger.progress, true); }; const cleanup = () => { if (rafId) { cancelAnimationFrame(rafId); rafId = null; } if (trigger) { trigger.kill(); trigger = null; } marks = []; }; const handleDesktopEnter = (event) => { if (event.matches && !trigger) { requestIdleCallback ? requestIdleCallback(initDesktopRuler, { timeout: 100 }) : setTimeout(initDesktopRuler, 50); } else if (!event.matches && trigger) { cleanup(); } }; const init = () => { if (desktopQuery.matches) { if (requestIdleCallback) { requestIdleCallback(initDesktopRuler, { timeout: 200 }); } else { setTimeout(initDesktopRuler, 100); } } else { desktopQuery.addEventListener('change', handleDesktopEnter); } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } window.addEventListener('beforeunload', cleanup); })();