const REQUIRED_NODES = ['.orbit_stage', '.timeline_line', '.timeline_cursor', '.timeline_cursor-blur', '.h1[data-title-variant="primary"]', '.h1[data-title-variant="secondary"]', '.h1_box', '.chapter_evolution', '.pin_wrapper', '.orbit_card:nth-child(4)']; const hasAllDeps = () => window.gsap && window.ScrollTrigger && window.SplitText && REQUIRED_NODES.every(sel => document.querySelector(sel)); const isMobile = () => window.innerWidth <= 640; const bootOrbit = () => { if (!window.gsap || !window.ScrollTrigger || !window.SplitText) { document.body.classList.add('orbit-ready'); return; } gsap.registerPlugin(ScrollTrigger, SplitText); const stage = document.querySelector('.orbit_stage'); const timelineLine = document.querySelector('.timeline_line'); const cursor = document.querySelector('.timeline_cursor'); const cursorBlur = document.querySelector('.timeline_cursor-blur'); const cards = gsap.utils.toArray('.orbit_card'); const titlePrimary = document.querySelector('.h1[data-title-variant="primary"]'); const titleSecondary = document.querySelector('.h1[data-title-variant="secondary"]'); const titleWrapper = document.querySelector('.h1_box'); if (!stage || !timelineLine || !cursor || !cursorBlur || cards.length !== 4 || !titlePrimary || !titleSecondary || !titleWrapper) { document.body.classList.add('orbit-ready'); return; } const mobile = isMobile(); titleSecondary.style.opacity = '0'; titleSecondary.style.willChange = 'opacity, transform'; const initSplitText = () => { const splitPrimary = new SplitText(titlePrimary, { type: 'lines', linesClass: 'title_line' }); const splitSecondary = new SplitText(titleSecondary, { type: 'lines', linesClass: 'title_line' }); return { linesPrimary: splitPrimary.lines, linesSecondary: splitSecondary.lines }; }; const infoBlocks = cards.map(card => card.querySelector('.card_info')); const splitters = cards.map(card => card.querySelector('.splitter_card')); const GAP = 10, BASE_OFFSET = 140, EXTRA_Y = [0, 55, 0, 55], MOBILE_QUERY = '(max-width: 640px)'; const MOBILE_SETTINGS = { gap: 4, fromWidth: '7rem', fromHeight: '15em', toHeight: '15em', toWidthPercents: [58, 52, 48, 42] }; const getIsMobile = () => window.matchMedia(MOBILE_QUERY).matches; let titleLinesPrimary, titleLinesSecondary; const primeElements = () => { if (!titleLinesPrimary || !titleLinesSecondary) return; gsap.set(infoBlocks, { opacity: 0, y: 16, force3D: !mobile }); gsap.set(splitters, { width: '0%', force3D: false }); gsap.set(cursor, { scaleY: 0, x: timelineLine.offsetLeft, force3D: false }); gsap.set(cursorBlur, { width: 0, x: timelineLine.offsetLeft, force3D: false }); if (mobile) { gsap.set([titlePrimary, titleSecondary], { transformStyle: 'flat', force3D: false }); gsap.set(titleLinesSecondary, { opacity: 0, y: 20, filter: 'blur(8px)', force3D: false }); } else { gsap.set([titlePrimary, titleSecondary], { transformPerspective: 1400, transformStyle: 'preserve-3d' }); gsap.set(titleLinesSecondary, { opacity: 0, rotateX: 70, yPercent: 50, z: 80, filter: 'blur(12px)' }); } gsap.set(titleWrapper, { transformOrigin: 'center top', force3D: !mobile }); }; const resolveMobileWidthPercent = index => { const override = MOBILE_SETTINGS.toWidthPercents; if (Array.isArray(override)) return override[index] ?? override[override.length - 1]; if (typeof override === 'number') return override; return null; }; const buildTargets = mobile => { const stageWidth = stage.offsetWidth, lineWidth = timelineLine.offsetWidth, lineLeft = timelineLine.offsetLeft, lineTop = timelineLine.offsetTop; if (!mobile) { const usableWidth = lineWidth - GAP * (cards.length - 1); const slotWidthPx = usableWidth / cards.length; const lineLeftPercent = (lineLeft / stageWidth) * 100; return cards.map((_, index) => { const leftInsidePx = index * (slotWidthPx + GAP); const cardCenterPx = lineLeft + leftInsidePx + slotWidthPx / 2; const leftPercent = (leftInsidePx / stageWidth) * 100; const widthPercent = (slotWidthPx / stageWidth) * 100; const extra = EXTRA_Y[index] || 0; return { top: `${lineTop - (BASE_OFFSET + extra)}px`, left: `calc(${lineLeftPercent}% + ${leftPercent}%)`, width: `${widthPercent}%`, centerPx: cardCenterPx }; }); } const gapPx = MOBILE_SETTINGS.gap; const widthsPx = cards.map((_, i) => { const percent = resolveMobileWidthPercent(i) ?? (100 / cards.length); return (percent / 100) * stageWidth; }); let cursorPx = lineLeft; return cards.map((_, index) => { const extra = EXTRA_Y[index] || 0; const widthPx = widthsPx[index]; const leftPercent = (cursorPx / stageWidth) * 100; const widthPercent = (widthPx / stageWidth) * 100; const centerPx = cursorPx + widthPx / 2; const target = { top: `${lineTop - (BASE_OFFSET + extra)}px`, left: `${leftPercent}%`, width: `${widthPercent}%`, centerPx }; cursorPx += widthPx + gapPx; return target; }); }; let targets = []; const leftExitCards = []; const rightExitCards = []; const classifyCardsForExit = () => { leftExitCards.length = 0; rightExitCards.length = 0; const stageCenter = stage.offsetWidth / 2; cards.forEach((card, index) => { if (targets[index].centerPx <= stageCenter) { leftExitCards.push(card); } else { rightExitCards.push(card); } }); }; const parallaxTargets = mobile ? null : cards.map(card => ({ element: card, tx: 0, ty: 0 })); const moveRange = { x: 18, y: 10 }; const applyTranslate3D = target => { if (!target) return; target.element.style.translate = `${target.tx}px ${target.ty}px 0`; }; const parallax = mobile ? null : (evt => { const rect = stage.getBoundingClientRect(); const relX = ((evt.clientX - rect.left) / rect.width - 0.5) * 2; const relY = ((evt.clientY - rect.top) / rect.height - 0.5) * 2; parallaxTargets.forEach((target, i) => { const strength = 1 - i * 0.15; const nextX = relX * moveRange.x * strength; const nextY = relY * moveRange.y * strength; gsap.to(target, { tx: nextX, ty: nextY, duration: 0.3, overwrite: true, onUpdate: () => applyTranslate3D(target) }); }); }); const resetParallax = (smooth = true) => { if (mobile || !parallaxTargets) return; parallaxTargets.forEach(target => { gsap.to(target, { tx: 0, ty: 0, duration: smooth ? 0.5 : 0, overwrite: true, onUpdate: () => applyTranslate3D(target) }); }); }; const handlePointerLeave = () => resetParallax(true); let parallaxEnabled = false; const toggleParallax = shouldEnable => { if (mobile || !parallax) return; if (shouldEnable === parallaxEnabled) return; parallaxEnabled = shouldEnable; if (shouldEnable) { stage.addEventListener('pointermove', parallax); stage.addEventListener('pointerleave', handlePointerLeave); } else { stage.removeEventListener('pointermove', parallax); stage.removeEventListener('pointerleave', handlePointerLeave); resetParallax(false); } }; const exitDistance = () => stage.offsetWidth * 0.8; let tl; const finalExitProgress = () => { if (!tl || !tl.labels || typeof tl.labels.finalExit === 'undefined') return 1; return tl.labels.finalExit / tl.duration(); }; const updateParallaxState = self => { if (mobile) return; const pastFinalExit = self.progress >= finalExitProgress(); toggleParallax(!pastFinalExit); }; const markReady = () => document.body.classList.add('orbit-ready'); const setupTimeline = mobile => { if (tl) { tl.scrollTrigger?.kill(); tl.kill(); tl = null; } if (!titleLinesPrimary || !titleLinesSecondary) return; resetParallax(false); primeElements(); targets = buildTargets(mobile); classifyCardsForExit(); const triggerElement = document.querySelector('.chapter_evolution'); if (!triggerElement) return; const scrollEnd = mobile ? '+=140%' : '+=250%'; tl = gsap.timeline({ scrollTrigger: { trigger: '.chapter_evolution', start: 'top top', end: scrollEnd, scrub: mobile ? 0.3 : true, pin: '.pin_wrapper', invalidateOnRefresh: !mobile, refreshPriority: mobile ? -1 : 0, anticipatePin: mobile ? 0 : 1, pinSpacing: true } }); tl.to('.core_video', { scale: 0.75, y: '-4vh', marginTop: 0, ease: 'power1.out', force3D: !mobile }, 0); cards.forEach((card, index) => { const widthTarget = () => targets[index].width; const toVars = { top: () => targets[index].top, left: () => targets[index].left, width: widthTarget, height: mobile ? MOBILE_SETTINGS.toHeight : '96px', scale: 1, rotation: 0, ease: 'power1.inOut', force3D: !mobile }; tl.to(card, toVars, 0.2); }); tl.to('.timeline_line', { opacity: 1, duration: 0.5, force3D: false }, 0.25); tl.to(cursor, { scaleY: 1, duration: 0.35, ease: 'power2.out', force3D: false }, 0.32); tl.to(cursorBlur, { width: () => timelineLine.offsetWidth, duration: 1, ease: 'none', force3D: false }, 0.35); tl.to(cursor, { x: () => timelineLine.offsetLeft + timelineLine.offsetWidth, duration: 1, ease: 'none', force3D: false }, 0.35); tl.to(infoBlocks, { opacity: 1, y: 0, duration: 0.4, ease: 'power2.out', stagger: 0.05, force3D: !mobile }, 0.35); tl.to(splitters, { width: '100%', duration: 0.45, ease: 'power1.out', stagger: 0.05, force3D: false }, 0.4); tl.addLabel('titleSwap', 0.55); if (mobile) { tl.to(titleLinesPrimary, { y: -20, opacity: 0, filter: 'blur(8px)', duration: 0.35, stagger: 0.04, ease: 'power2.in', force3D: false }, 'titleSwap'); tl.addLabel('titleGap', 'titleSwap+=0.3'); tl.call(() => (titleSecondary.style.opacity = '1'), null, 'titleGap-=0.01'); tl.fromTo(titleLinesSecondary, { y: 20, opacity: 0, filter: 'blur(8px)' }, { y: 0, opacity: 1, filter: 'blur(0px)', duration: 0.4, stagger: 0.04, ease: 'power2.out', force3D: false }, 'titleGap'); } else { tl.to(titleLinesPrimary, { rotateX: -75, yPercent: -45, z: -90, opacity: 0, filter: 'blur(12px)', duration: 0.45, stagger: 0.06, ease: 'power3.in' }, 'titleSwap'); tl.addLabel('titleGap', 'titleSwap+=0.4'); tl.call(() => (titleSecondary.style.opacity = '1'), null, 'titleGap-=0.01'); tl.fromTo(titleLinesSecondary, { rotateX: 75, yPercent: 45, z: 90, opacity: 0, filter: 'blur(12px)' }, { rotateX: 0, yPercent: 0, z: 0, opacity: 1, filter: 'blur(0px)', duration: 0.55, stagger: 0.06, ease: 'power3.out' }, 'titleGap'); } tl.addLabel('finalExit'); tl.to(leftExitCards, { x: () => -exitDistance(), y: '+=40', opacity: 0, duration: 0.7, ease: 'power3.in', force3D: !mobile }, 'finalExit'); tl.to(rightExitCards, { x: () => exitDistance(), y: '+=40', opacity: 0, duration: 0.7, ease: 'power3.in', force3D: !mobile }, 'finalExit'); tl.to('.core_video', { scale: 0.35, opacity: 0, filter: 'blur(8px)', duration: 0.8, ease: 'power2.inOut', force3D: !mobile }, 'finalExit+=0.1'); tl.to([cursor, cursorBlur], { opacity: 0, duration: 0.35, force3D: false }, 'finalExit'); tl.to(titleWrapper, { scale: 0.8, opacity: 0, filter: 'blur(6px)', y: '-2vh', duration: 0.65, ease: 'power2.in', force3D: !mobile }, 'finalExit+=0.05'); const masterTrigger = tl.scrollTrigger; if (masterTrigger && !mobile) { masterTrigger.eventCallback('onUpdate', updateParallaxState); masterTrigger.eventCallback('onLeave', () => toggleParallax(false)); masterTrigger.eventCallback('onLeaveBack', () => toggleParallax(true)); } if (!mobile) { toggleParallax(true); } }; const initTimeline = (mobile) => { if (mobile) { if ('requestIdleCallback' in window) { requestIdleCallback(() => { const split = initSplitText(); titleLinesPrimary = split.linesPrimary; titleLinesSecondary = split.linesSecondary; setupTimeline(mobile); markReady(); }, { timeout: 50 }); } else { requestAnimationFrame(() => { const split = initSplitText(); titleLinesPrimary = split.linesPrimary; titleLinesSecondary = split.linesSecondary; setupTimeline(mobile); markReady(); }); } } else { const split = initSplitText(); titleLinesPrimary = split.linesPrimary; titleLinesSecondary = split.linesSecondary; setupTimeline(mobile); markReady(); } }; ScrollTrigger.matchMedia({ [MOBILE_QUERY]: () => { initTimeline(true); }, '(min-width: 641px)': () => { initTimeline(false); } }); window.addEventListener('resize', () => { const mobileNow = getIsMobile(); targets = buildTargets(mobileNow); classifyCardsForExit(); gsap.set(cursor, { x: timelineLine.offsetLeft }); gsap.set(cursorBlur, { x: timelineLine.offsetLeft, width: 0 }); resetParallax(false); if (!mobileNow) { ScrollTrigger.refresh(); } }); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { document.body.style.overflow = 'visible'; setTimeout(bootOrbit, 0); }); } else { document.body.style.overflow = 'visible'; setTimeout(bootOrbit, 0); }