(() => { // ===== 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(href, delay = 250) { const pane = document.querySelector('.transition-bottom') || (() => { const d = document.createElement('div'); d.className = 'transition-bottom'; document.body.appendChild(d); return d; })(); pane.classList.add('cover'); setTimeout(() => location.href = href, delay); } // ===== 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'), track = row?.querySelector('.marquee-track'); if (!row || !track) return; if (!row._cloned) { row.appendChild(track.cloneNode(true)); row._cloned = true; } const ds = m.dataset, axis = ds.axis === 'y' ? 'y' : 'x', dir = ds.direction === 'reverse' ? -1 : 1, hoverPause = ds.hoverPause === 'true', scrollOnly = ds.scrollOnly === 'true', scrollSync = ds.scrollSync === 'true'; const rot = parseInt(ds.rot || '720', 10), speedPx = Number(ds.speed) || 120; let tween, st, ro; const icons = gsap.utils.toArray(m.querySelectorAll('.marquee-sep__icon')); if (icons.length) gsap.set(icons, { transformOrigin: '50% 50%', force3D: true }); const measure = () => { const r = track.getBoundingClientRect(); return Math.max(1, axis === 'y' ? r.height : r.width); }; const build = (keep = 0) => { const dist = measure(), dur = dist / Math.max(20, speedPx); const from = { x: 0, y: 0 }, 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); }; if (scrollOnly) { const factor = Number(ds.speed) || 1; // 1 = default, <1 slower, >1 faster const dist = () => measure() * dir * -1 * factor; const f = {}; const t = { ease: 'none', scrollTrigger: { trigger: m, start: 'top bottom', end: 'bottom top', scrub: 0.7 } }; f[axis] = 0; t[axis] = () => dist(); tween = gsap.fromTo(row, f, t); st = ScrollTrigger.create({ trigger: m, start: 'top bottom', end: 'bottom top', scrub: 0.7, onUpdate: s => { if (icons.length) { gsap.set(icons, { rotation: dir * rot * -s.progress }); } } }); } else { build(0); if (hoverPause) { const enter = () => tween?.pause(), 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 => tween && tween.timeScale(gsap.utils.clamp(.3, 3, 1 + s.getVelocity() / 300)) }); } const rebuild = () => tween && build(tween.progress()); ro = new ResizeObserver(() => rebuild()); ro.observe(track); addEventListener('load', rebuild, { once: true }); } const io = new IntersectionObserver(es => { const vis = es.some(e => e.isIntersecting); vis ? tween?.resume() : tween?.pause(); }, { threshold: .05 }); io.observe(m); addCleanup(() => { tween?.kill(); st?.kill(); ro?.disconnect?.(); io.disconnect(); }); }); } // ===== stacked cards ===== function initStickyStackedCards() { document.querySelectorAll('[data-stack-group]').forEach(group => { const ds = group.dataset, base = +(ds.base ?? 40), header = +(ds.header ?? 80), bp = +(ds.bp ?? 0), pinT = ds.pinType || ''; if (bp && !matchMedia(`(min-width:${bp}px)`).matches) return; const cards = gsap.utils.toArray(group.querySelectorAll('[data-stack-card]')); if (!cards.length) return; const totalOffset = base * (cards.length + 1); let spacer = group.querySelector(':scope > .stack-spacer'); if (!spacer) { spacer = document.createElement('div'); spacer.className = 'stack-spacer'; group.appendChild(spacer); } spacer.style.height = totalOffset + 'px'; group._stackKill?.forEach(fn => { try { fn() } catch { } }); const killers = []; cards.forEach((card, i) => { const extra = +(card.dataset.top ?? 0), offset = base * (i + 1) + header + extra; gsap.set(card, { top: offset + 'px' }); const st = ScrollTrigger.create({ trigger: card, start: `top-=${offset} top`, end: 'max', pin: true, pinSpacing: false, pinType: pinT || undefined, anticipatePin: 1, invalidateOnRefresh: true, id: `pin-${i}` }); killers.push(() => st.kill()); }); const ro = new ResizeObserver(() => { spacer.style.height = (base * (cards.length + 1)) + 'px'; ScrollTrigger.refresh(); }); ro.observe(group); killers.push(() => ro.disconnect()); group._stackKill = killers; }); } // ===== 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; if (!list) return; list.style.display = 'flex'; list.style.flexWrap = 'nowrap'; root.style.overflow = root.style.overflow || 'hidden'; const GAP = Number(root.getAttribute('data-pillar-gap')) || 500; const BP = Number(root.getAttribute('data-pillar-bp')) || 992; if (prefers) return; // поважаємо prefers-reduced-motion const tween = gsap.to(list, { x: () => { const vw = document.documentElement.clientWidth; if (vw < BP) return 0; const total = list.scrollWidth; const dist = total - vw + GAP; return dist > 0 ? -dist : 0; }, ease: 'none', scrollTrigger: { trigger: section, start: 'top top', end: () => { const vw = document.documentElement.clientWidth; if (vw < BP) return '+=0'; const total = list.scrollWidth; const dist = total - vw + GAP; return '+=' + (dist > 0 ? dist : 0); }, scrub: 1, pin: true, pinSpacing: true, anticipatePin: 1, invalidateOnRefresh: true, onRefresh(self) { const pin = self.pin || 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 => { if (c) spacer.classList.add(c); }); } } }); cleanups.push(() => { try { 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 text = el.textContent; const parts = text.split(/(\s+)/); el.textContent = ''; const words = []; parts.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); } }); 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, initStickyStackedCards, initTilt, initHeaderTween, initPillarSlider, initWordCycler, showOverlay, hideOverlay, gatePageReady, destroyAll, routeTransitionGo }); })();