(() => { // ===== utils ===== const $ = (s, r = document) => r.querySelector(s), $$ = (s, r = document) => r.querySelectorAll(s); const on = (el, ev, fn, opt) => (el.addEventListener(ev, fn, opt), () => el.removeEventListener(ev, fn, opt)); const RRM = () => matchMedia('(prefers-reduced-motion: reduce)').matches, MOBILE = () => matchMedia('(max-width: 767px)').matches; const CLEANUPS = []; const addCleanup = fn => CLEANUPS.push(fn); function destroyAll() { while (CLEANUPS.length) { try { CLEANUPS.pop()() } catch { } } ScrollTrigger?.getAll().forEach(x => x.kill()); gsap?.globalTimeline.getChildren().forEach(t => t?.kill?.()); } // ===== overlay / route ===== function showOverlay(reason = 'initial') { const ov = $('#page-transition-overlay'); if (!ov) return; if (ov.parentElement !== document.body) document.body.appendChild(ov); ov.style.zIndex = '2147483647'; ov.classList.add('cover'); ov.setAttribute('aria-hidden', 'false'); dispatchEvent(new CustomEvent('overlay:show', { detail: { reason } })); } function hideOverlay(reason = 'initial') { const ov = $('#page-transition-overlay'); const done = () => { ov?.setAttribute('aria-hidden', 'true'); dispatchEvent(new CustomEvent('overlay:complete', { detail: { reason } })); }; if (!ov) return done(); const onEnd = () => { ov.removeEventListener('transitionend', onEnd); done(); }; ov.addEventListener('transitionend', onEnd, { once: true }); requestAnimationFrame(() => ov.classList.remove('cover')); setTimeout(onEnd, 1600); } function gatePageReady({ maxWait = 1200, minHold = 200 } = {}) { // wait hero media const v = document.querySelector('[data-hero-video], .w-background-video video, video[autoplay][muted]'); const i = document.querySelector('[data-hero-image], img[data-hero]'); const waits = []; if (v) waits.push(new Promise(r => { if (v.readyState >= 3) return r();['loadeddata', 'canplay', 'playing', 'timeupdate'].forEach(ev => v.addEventListener(ev, () => r(), { once: true })); v.play?.().catch(() => { }); })); if (i) waits.push(i.decode ? i.decode() : Promise.resolve()); const min = new Promise(r => setTimeout(r, minHold)), cap = new Promise(r => setTimeout(r, maxWait)); return Promise.race([waits.length ? Promise.race(waits) : Promise.resolve(), cap]).then(() => min); } function routeTransitionGo(hrefOrEvent, maybeHrefOrDelay = 2000, maybeDelay = 2000) { // adds `.cover` to `.transition-bottom`, let ev = null, href = null, delay = 2000; if (typeof hrefOrEvent === 'string') { href = hrefOrEvent; delay = Number(maybeHrefOrDelay) || 250; } else if (hrefOrEvent && typeof hrefOrEvent === 'object') { ev = hrefOrEvent; try { ev.preventDefault?.(); } catch { } href = typeof maybeHrefOrDelay === 'string' ? maybeHrefOrDelay : (ev.currentTarget?.href || ev.target?.closest?.('a[href]')?.href || null); delay = Number(maybeDelay) || 250; } if (!href) return; if (RRM()) { location.assign(href); return; } const pane = document.querySelector('.transition-bottom'); if (!pane) { location.assign(href); return; } pane.classList.add('cover'); // Play the transition fully, then navigate. setTimeout(() => location.assign(href), delay); // Safety: remove cover shortly after (or if bfcache restores the page). const clearAfter = delay + 500; const clear = () => pane.classList.remove('cover'); setTimeout(clear, clearAfter); addEventListener('pageshow', (e) => { if (e.persisted) clear(); }, { once: true }); } function initPageTransitions({ selector = 'a[href]', delay = 2000, ignoreSelector = '[data-no-transition], [data-transition="off"]' } = {}) { if (document.documentElement.dataset.pagefxTransitionsInit === '1') return; document.documentElement.dataset.pagefxTransitionsInit = '1'; const isSamePageHash = href => { try { const u = new URL(href, location.href); if (!u.hash) return false; return u.origin === location.origin && u.pathname === location.pathname && u.search === location.search; } catch { return false; } }; addEventListener('click', (e) => { if (e.defaultPrevented) return; if (e.button !== 0) return; // only left click if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; const a = e.target?.closest?.(selector); if (!a) return; if (a.closest(ignoreSelector)) return; if (a.hasAttribute('download')) return; if ((a.getAttribute('target') || '').toLowerCase() === '_blank') return; const href = a.getAttribute('href') || ''; if (!href || href === '#' || href.startsWith('javascript:')) return; if (/^(mailto:|tel:|sms:|whatsapp:)/i.test(href)) return; if (isSamePageHash(href)) return; // Intercept both internal and external navigations (except filtered cases above). routeTransitionGo(e, a.href, delay); }, true); } // ===== Lenis + ScrollTrigger proxy ===== function initLenis() { if (RRM()) return null; if (!window.Lenis || !window.gsap || !window.ScrollTrigger) { console.warn('[Lenis/GSAP missing]'); return null; } if (window.__lenis__) return window.__lenis__; const L = new Lenis({ lerp: .12, smoothWheel: true, smoothTouch: false }); gsap.ticker.add(t => L.raf(t * 1000)); gsap.ticker.lagSmoothing(0); ScrollTrigger.scrollerProxy(document.documentElement, { scrollTop(v) { return arguments.length ? window.scrollTo(0, v) : window.pageYOffset }, getBoundingClientRect() { return { top: 0, left: 0, width: innerWidth, height: innerHeight } } }); addEventListener('scroll', () => ScrollTrigger.update(), { passive: true }); const refreshAll = () => { L.resize?.(); ScrollTrigger.refresh(true); }; addEventListener('load', refreshAll, { once: true }); addEventListener('resize', () => gsap.delayedCall(.15, refreshAll), { passive: true }); addEventListener('overlay:show', () => { try { L.stop() } catch { } }); addEventListener('overlay:complete', () => { try { L.start() } catch { } }); return (window.lenis = window.__lenis__ = L); } // ===== navbar compact/hide ===== function initNavbarSmart({ root = '.navbar', compactOffset = 40 } = {}) { const bar = $(root); if (!bar) return; let lastY = 0; const upd = () => { const y = window.scrollY; bar.classList.toggle('is-compact', y > compactOffset); const dy = y - lastY; if (Math.abs(dy) > 8) bar.classList.toggle('is-hidden', dy > 0 && y > 80); lastY = y; }; const st = ScrollTrigger.create({ start: 0, end: 'max', onUpdate: upd }); upd(); addCleanup(() => st.kill()); } // ===== text wrap words effect ===== function wrapWords(el) { if (el.querySelector('.word')) return el.querySelectorAll('.word'); const nodes = [...el.childNodes], frag = document.createDocumentFragment(); nodes.forEach(n => { if (n.nodeType === 3) { (n.textContent || '').split(/(\s+)/).forEach(tok => { if (!tok) return; if (/\s+/.test(tok)) frag.appendChild(document.createTextNode(tok)); else { const s = document.createElement('span'); s.className = 'word'; s.textContent = tok; frag.appendChild(s); } }); } else frag.appendChild(n); }); el.innerHTML = ''; el.appendChild(frag); return el.querySelectorAll('.word'); } // ===== text reveal effect===== function initTextReveal() { gsap.utils.toArray('[data-animate="word"]').forEach(el => { const t = gsap.utils.toArray(wrapWords(el)); if (!t.length) return; const tw = gsap.from(t, { yPercent: 100, duration: .9, ease: 'power4.out', stagger: .14, scrollTrigger: { trigger: el, start: 'top 85%', once: true } }); addCleanup(() => tw.scrollTrigger?.kill()); }); gsap.utils.toArray('[data-animate="text-line"]').forEach(el => { if (el.querySelector('.text-line')) return; const nodes = [...el.childNodes]; el.innerHTML = ''; const inners = []; nodes.forEach(node => { if (node.nodeType === 1 && node.tagName === 'BR') { el.appendChild(node); return; } if (node.nodeType === 3 && !node.textContent.trim()) return; const w = document.createElement('span'); w.className = 'text-line'; const i = document.createElement('span'); i.className = 'text-line-inner'; i.appendChild(node); w.appendChild(i); el.appendChild(w); inners.push(i); }); if (!inners.length) return; const tw = gsap.from(inners, { yPercent: 100, duration: .9, ease: 'power4.out', stagger: .16, scrollTrigger: { trigger: el, start: 'top 85%', once: true } }); addCleanup(() => tw.scrollTrigger?.kill()); }); gsap.utils .toArray('[data-animate]:not([data-animate="word"]):not([data-animate="text-line"]):not([data-animate="chars"]):not([data-animate="typewriter"])') .forEach(el => { const t = el.getAttribute('data-animate'); const from = { opacity: 0 }; if (t === 'fade-up') from.y = 40; else if (t === 'fade-left') from.x = -60; else if (t === 'fade-right') from.x = 60; if (t === 'scale-in') Object.assign(from, { scale: 0, transformOrigin: '50% 50%' }); const tw = gsap.from(el, { ...from, duration: (t === 'scale-in' ? 1 : .85), ease: 'power3.out', scrollTrigger: { trigger: el, start: 'top 85%', once: true } }); addCleanup(() => tw.scrollTrigger?.kill()); }); } // ===== marquee ===== function initMarqueeEnhanced() { $$('.marquee[wb-data="marquee"]').forEach(m => { const row = m.querySelector('.marquee-row'); const track = row?.querySelector('.marquee-track'); if (!row || !track) return; if (!row._cloned) { row.appendChild(track.cloneNode(true)); row._cloned = true; } const ds = m.dataset; const axis = ds.axis === 'y' ? 'y' : 'x'; const dir = ds.direction === 'reverse' ? -1 : 1; const hoverPause = ds.hoverPause === 'true'; const scrollOnly = ds.scrollOnly === 'true'; const scrollSync = ds.scrollSync === 'true'; const hasScrollTrigger = scrollOnly || scrollSync; const rot = parseInt(ds.rot || '720', 10); const speedPx = Number(ds.speed) || 120; let tween, st, ro, io; const icons = gsap.utils.toArray(m.querySelectorAll('.marquee-sep__icon')); if (icons.length) { gsap.set(icons, { transformOrigin: '50% 50%', force3D: true }); } // one seamless cycle size (for continuous auto marquee) const measure = () => { if (axis === 'x') { const total = row.scrollWidth; const count = row.children.length || 1; return Math.max(1, total / count); } else { const r = track.getBoundingClientRect(); const h = Math.max(1, Math.round(r.height)); // snap to int track.style.height = h + 'px'; // lock height to same int return h; } }; const build = (keep = 0) => { const raw = measure(); const dist = axis === 'y' ? Math.round(raw) : raw; // snap for vertical const dur = dist / Math.max(20, speedPx); const from = { x: 0, y: 0 }; const to = { ease: 'none', repeat: -1, duration: dur }; if (axis === 'y') { from.y = dir === -1 ? -dist : 0; to.y = dir === -1 ? 0 : -dist; } else { from.x = dir === -1 ? -dist : 0; to.x = dir === -1 ? 0 : -dist; } const prev = tween ? tween.progress() : keep; tween && tween.kill(); gsap.set(row, from); tween = gsap.to(row, to); if (!isNaN(prev)) tween.progress(prev % 1); }; // ===== scroll-only variant (normalized by container width) ===== if (scrollOnly) { const factor = Number(ds.speed || ds.duration) || 1; const baseDist = () => { const r = m.getBoundingClientRect(); const size = axis === 'y' ? r.height : r.width; // same for normal / reverse return size * factor; }; const from = { x: 0, y: 0 }; const to = { ease: 'none' }; if (axis === 'y') { to.y = () => (dir === -1 ? baseDist() : -baseDist()); } else { to.x = () => (dir === -1 ? baseDist() : -baseDist()); } to.scrollTrigger = { trigger: m, start: 'top bottom', end: 'bottom top', scrub: 0.7, onUpdate: s => { if (icons.length) { gsap.set(icons, { rotation: dir * rot * -s.progress }); } } }; tween = gsap.fromTo(row, from, to); st = tween.scrollTrigger; } // ===== normal / scrollSync ===== else { build(0); if (hoverPause) { const enter = () => tween?.pause(); const leave = () => tween?.resume(); m.addEventListener('mouseenter', enter, { passive: true }); m.addEventListener('mouseleave', leave, { passive: true }); addCleanup(() => { m.removeEventListener('mouseenter', enter); m.removeEventListener('mouseleave', leave); }); } if (scrollSync) { st = ScrollTrigger.create({ trigger: m, start: 'top bottom', end: 'bottom top', scrub: true, onUpdate: s => { if (!tween) return; const ts = 1 + s.getVelocity() / 300; tween.timeScale(gsap.utils.clamp(0.3, 3, ts)); } }); } const rebuild = () => tween && build(tween.progress()); ro = new ResizeObserver(() => rebuild()); ro.observe(track); addEventListener('load', rebuild, { once: true }); } if (!hasScrollTrigger) { io = new IntersectionObserver(es => { const vis = es.some(e => e.isIntersecting); vis ? tween?.resume() : tween?.pause(); }, { threshold: 0.05 }); io.observe(m); } addCleanup(() => { tween?.kill(); st?.kill(); ro?.disconnect?.(); io?.disconnect?.(); }); }); } // ===== tilt hover effect ===== function initTilt({ sel = '[data-tilt]', child = '[data-tilt-child]', max = 10, pers = 500 } = {}) { if (RRM() || MOBILE()) return; gsap.utils.toArray($$(sel)).forEach(w => { const t = w.querySelector(child); if (!t) return; const move = e => { const r = t.getBoundingClientRect(), dx = (e.clientX - (r.left + r.width / 2)) / (r.width / 2), dy = (e.clientY - (r.top + r.height / 2)) / (r.height / 2); gsap.to(t, { rotationY: gsap.utils.clamp(-max, max, dx * max), rotationX: gsap.utils.clamp(-max, max, -dy * max), transformPerspective: pers, force3D: true, ease: 'power1.out', duration: .2, overwrite: 'auto' }); }; const leave = () => gsap.to(t, { rotationY: 0, rotationX: 0, transformPerspective: pers, duration: .2, ease: 'power1.out', overwrite: 'auto' }); const off1 = on(w, 'mousemove', move, { passive: true }), off2 = on(w, 'mouseleave', leave, { passive: true }); addCleanup(() => { off1(); off2(); }); }); } // ===== header show/hide ===== function initHeaderTween({ sel = '.header', shownTop = 40, hiddenTop = -150, threshold = 300 } = {}) { const el = document.querySelector(sel); if (!el || !window.gsap || RRM()) return () => { }; let lastY = window.scrollY, state = 'shown', ticking = false; const show = () => { if (state === 'shown') return; state = 'shown'; gsap.to(el, { top: shownTop, duration: .45, ease: 'power2.out', overwrite: true }); }; const hide = () => { if (state === 'hidden') return; state = 'hidden'; gsap.to(el, { top: hiddenTop, duration: .9, ease: 'power2.out', overwrite: true }); }; const onScroll = () => { if (ticking) return; ticking = true; requestAnimationFrame(() => { const y = window.scrollY; if (y < threshold) show(); else (y > lastY ? hide() : show()); lastY = y; ticking = false; }); }; window.addEventListener('scroll', onScroll, { passive: true }); gsap.set(el, { top: shownTop }); return () => window.removeEventListener('scroll', onScroll); } // ===== pillar slider (horizontal pin) ===== function initPillarSlider() { if (!window.gsap || !window.ScrollTrigger) return () => { }; const prefers = RRM(); const cleanups = []; document.querySelectorAll('[data-pillar-slider="true"]').forEach(root => { const list = root.querySelector('.pillar-slider-list'); const section = root.closest('section') || root; const pinWrap = root.querySelector('.pillar-slider-pin'); const pinTarget = pinWrap || list; // prefer explicit wrapper; fallback to list if (!list || !section || !pinTarget) return; if (prefers) return; list.style.display = 'flex'; list.style.flexWrap = 'nowrap'; // Lock height of the pinned area to avoid jump when spacer is inserted. const h = pinTarget.getBoundingClientRect().height; if (h) { pinTarget.style.minHeight = h + 'px'; pinTarget.style.width = pinTarget.style.width || '100%'; } const BP = Number(root.getAttribute('data-pillar-bp') || 992); const EXTRA = Number(root.getAttribute('data-pillar-extra') || 500); const measure = () => { const viewport = root.getBoundingClientRect().width || window.innerWidth || 0; if (!viewport || viewport < BP) return 0; const total = list.scrollWidth || 0; if (!total) return 0; const base = total - viewport; if (base > 0) return base + EXTRA; if (EXTRA > 0) return EXTRA; const card = list.querySelector('.pillar-slider-item') || list.firstElementChild; if (!card) return 0; const cardW = card.getBoundingClientRect().width || 0; return cardW; }; const tween = gsap.to(list, { x: () => { const dist = measure(); return dist ? -dist : 0; }, ease: 'none', scrollTrigger: { trigger: section, start: 'top top', end: () => { const dist = measure(); return dist ? `+=${dist}` : '+=0'; }, scrub: 1, pin: pinTarget, pinSpacing: true, anticipatePin: 1, invalidateOnRefresh: true, onRefresh(self) { const pin = self.pin || pinTarget || section; const spacer = pin && pin.parentNode; if (!spacer || !spacer.classList.contains('pin-spacer')) return; const cls = section.getAttribute('data-pillar-spacer-class'); if (!cls) return; cls.split(/\s+/).forEach(c => c && spacer.classList.add(c)); } } }); cleanups.push(() => { try { tween.scrollTrigger && tween.scrollTrigger.kill(); tween.kill(); } catch { } }); }); return () => cleanups.forEach(fn => { try { fn(); } catch { } }); } // ===== word cycler (simple / chars / typewriter) ===== function initWordCycler({ selector = '[data-words]', threshold = 0.25 } = {}) { const hasTextPlugin = !!(window.gsap && window.TextPlugin); if (hasTextPlugin) gsap.registerPlugin(TextPlugin); const timers = new WeakMap(); const writers = new WeakMap(); const parseWords = str => (str || '').split(',').map(s => s.trim()).filter(Boolean); const bindOne = el => { if (timers.has(el) || writers.has(el)) return; const words = parseWords(el.dataset.words); if (!words.length) return; const intervalMs = Math.max(300, parseInt(el.dataset.wordsInterval || '1300', 10)); const delayMs = Math.max(0, parseInt(el.dataset.wordsDelay || '0', 10)); const anim = el.dataset.animate; let mode = 'simple'; if (anim === 'chars') mode = 'chars'; if (anim === 'typewriter' && hasTextPlugin) mode = 'typewriter'; if (anim === 'typewriter' && !hasTextPlugin) mode = 'chars'; if (mode === 'typewriter') { const textSpan = document.createElement('span'); textSpan.className = 'tw-text'; const cursorSpan = document.createElement('span'); cursorSpan.className = 'tw-cursor'; cursorSpan.textContent = '_'; el.innerHTML = ''; el.appendChild(textSpan); el.appendChild(cursorSpan); const typeDur = 0.8; const holdDur = 0.6; const eraseDur = 0.6; const gapDur = intervalMs / 1000; const tl = gsap.timeline({ repeat: -1, paused: true }); words.forEach(word => { const tlWord = gsap.timeline({ repeat: 1, yoyo: true, repeatDelay: gapDur }); tlWord .to(textSpan, { duration: typeDur, text: word, ease: 'none' }) .to({}, { duration: holdDur }); tl.add(tlWord); }); const cursorTl = gsap.to(cursorSpan, { opacity: 0, repeat: -1, yoyo: true, duration: 0.5, ease: 'power1.inOut', paused: true }); writers.set(el, { tl, cursorTl, delayMs, started: false, delayId: null }); return; } let idx = words.findIndex(w => w === el.textContent.trim()); if (idx < 0) idx = 0; timers.set(el, { mode, words, idx, intervalMs, delayMs, running: false, timeout: null }); }; const showCharsWord = (el, word) => { el.innerHTML = ''; const chars = []; for (const ch of word) { if (/\s/.test(ch)) { el.appendChild(document.createTextNode(ch)); } else { const span = document.createElement('span'); span.className = 'char'; span.textContent = ch; el.appendChild(span); chars.push(span); } } if (!chars.length) return; gsap.from(chars, { opacity: 0, yPercent: 100, duration: 0.6, stagger: 0.03, ease: 'power2.out' }); }; const start = el => { if (writers.has(el)) { const st = writers.get(el); if (st.started) return; st.started = true; st.delayId = setTimeout(() => { st.tl.play(0); st.cursorTl.play(0); }, st.delayMs); return; } const st = timers.get(el); if (!st || st.running) return; st.running = true; const loop = () => { if (!st.running) return; st.idx = (st.idx + 1) % st.words.length; const word = st.words[st.idx]; if (st.mode === 'chars') { showCharsWord(el, word); } else { el.textContent = word; } st.timeout = setTimeout(loop, st.intervalMs); }; st.timeout = setTimeout(loop, st.delayMs); }; const stop = el => { if (writers.has(el)) { const st = writers.get(el); if (!st.started) return; st.started = false; clearTimeout(st.delayId); st.delayId = null; st.tl.pause(0); st.cursorTl.pause(0); return; } const st = timers.get(el); if (!st) return; st.running = false; clearTimeout(st.timeout); st.timeout = null; }; const els = document.querySelectorAll(selector); if (!els.length) return; els.forEach(bindOne); const io = new IntersectionObserver(entries => { entries.forEach(en => { const el = en.target; if (!timers.has(el) && !writers.has(el)) return; en.isIntersecting ? start(el) : stop(el); }); }, { threshold }); els.forEach(el => io.observe(el)); addCleanup(() => { io.disconnect(); els.forEach(stop); }); } // ===== text opacity on scroll ===== function initTextOpacity({ selector = '[data-text-opacity], .text-opacity', baseOpacity = 0.2 } = {}) { if (!window.gsap || !window.ScrollTrigger) return; const els = document.querySelectorAll(selector); if (!els.length) return; els.forEach(el => { if (el.dataset.fxOpacityInit === '1') return; el.dataset.fxOpacityInit = '1'; const nodes = [...el.childNodes]; el.textContent = ''; const words = []; nodes.forEach(node => { if (node.nodeType === 3) { // text node (node.textContent || '').split(/(\s+)/).forEach(part => { if (!part) return; if (/^\s+$/.test(part)) { el.appendChild(document.createTextNode(part)); } else { const span = document.createElement('span'); span.className = 'fx-opacity-word'; span.textContent = part; el.appendChild(span); words.push(span); } }); return; } if (node.nodeType === 1 && node.tagName === 'BR') { el.appendChild(node.cloneNode()); return; } el.appendChild(node.cloneNode(true)); }); const minOpacity = el.dataset.opacityMin ? parseFloat(el.dataset.opacityMin) || baseOpacity : baseOpacity; words.forEach((word, index) => { gsap.fromTo( word, { opacity: minOpacity }, { opacity: 1, ease: 'none', scrollTrigger: { trigger: word, start: `top+=${index * 20} 80%`, end: `top+=${index * 20} 40%`, toggleActions: 'play reverse play reverse', scrub: true } } ); }); }); } // ===== export ===== window.PageFX ||= {}; Object.assign(window.PageFX, { initLenis, initNavbarSmart, initTextReveal, initTextOpacity, initMarqueeEnhanced, initTilt, initHeaderTween, initPillarSlider, initWordCycler, initPageTransitions, showOverlay, hideOverlay, gatePageReady, destroyAll, routeTransitionGo }); })();