gsap.registerPlugin(InertiaPlugin); gsap.registerPlugin(SplitText); gsap.registerPlugin(ScrollTrigger); let lenis; function initLenis() { lenis = new Lenis({ duration: 1.25, easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), }); lenis.on("scroll", ScrollTrigger.update); gsap.ticker.add((time) => { lenis.raf(time * 1000); }); gsap.ticker.lagSmoothing(0); } function initFOUC() { const loadEls = document.querySelectorAll("[data-anim-load]"); loadEls.forEach((el) => { if (el.hasAttribute("data-fouc-disabled")) { return; } else { gsap.set(el, { visibility: "visible", }); } }); } function initTextSplit() { $("[data-split=heading]").each(function () { const toSplit = $(this); SplitText.create(toSplit, { type: "words, lines", // mask: "lines", wordsClass: "word", linesClass: "line", }); }); $("[data-split=words]").each(function () { const toSplit = $(this); SplitText.create(toSplit, { type: "words, lines", mask: "lines", wordsClass: "word", linesClass: "line", }); }); $("[data-split=lines]").each(function () { const toSplit = $(this); SplitText.create(toSplit, { type: "lines", mask: "lines", linesClass: "line", aria: "none", }); }); $("[data-split=rich-lines]").each(function () { const toSplit = $(this).children(); SplitText.create(toSplit, { type: "lines", mask: "lines", linesClass: "line", }); }); } function initGlobalParallax() { const mm = gsap.matchMedia(); mm.add( { isMobile: "(max-width:479px)", isMobileLandscape: "(max-width:767px)", isTablet: "(max-width:991px)", isDesktop: "(min-width:992px)", }, (context) => { const { isMobile, isMobileLandscape, isTablet } = context.conditions; const ctx = gsap.context(() => { document .querySelectorAll('[data-parallax="trigger"]') .forEach((trigger) => { // Check if this trigger has to be disabled on smaller breakpoints const disable = trigger.getAttribute("data-parallax-disable"); if ( (disable === "mobile" && isMobile) || (disable === "mobileLandscape" && isMobileLandscape) || (disable === "tablet" && isTablet) ) { return; } // Optional: you can target an element inside a trigger if necessary const target = trigger.querySelector('[data-parallax="target"]') || trigger; // Get the direction value to decide between xPercent or yPercent tween const direction = trigger.getAttribute("data-parallax-direction") || "vertical"; const prop = direction === "horizontal" ? "xPercent" : "yPercent"; // Get the scrub value, our default is 'true' because that feels nice with Lenis const scrubAttr = trigger.getAttribute("data-parallax-scrub"); const scrub = scrubAttr ? parseFloat(scrubAttr) : true; // Get the start position in % const startAttr = trigger.getAttribute("data-parallax-start"); const startVal = startAttr !== null ? parseFloat(startAttr) : 20; // Get the end position in % const endAttr = trigger.getAttribute("data-parallax-end"); const endVal = endAttr !== null ? parseFloat(endAttr) : -20; // Get the start value of the ScrollTrigger const scrollStartRaw = trigger.getAttribute("data-parallax-scroll-start") || "top bottom"; const scrollStart = `clamp(${scrollStartRaw})`; // Get the end value of the ScrollTrigger const scrollEndRaw = trigger.getAttribute("data-parallax-scroll-end") || "bottom top"; const scrollEnd = `clamp(${scrollEndRaw})`; gsap.fromTo( target, { [prop]: startVal }, { [prop]: endVal, ease: "none", scrollTrigger: { trigger, start: scrollStart, end: scrollEnd, scrub, }, } ); }); }); return () => ctx.revert(); } ); } function initLoadAnimations() { const isMobile = window.innerWidth <= 768; if (isMobile) return; const globalIntroDelay = 0; $("[data-anim-load=heading]").each(function () { let tl = gsap.timeline({ delay: globalIntroDelay, }); const heading = $(this); const words = $(this).find(".word"); const shuffled = gsap.utils.shuffle([...words]); tl.set(words, { opacity: 0 }); tl.set($(this), { autoAlpha: 1 }); tl.from(shuffled, { yPercent: () => gsap.utils.random(-50, 50, 50), xPercent: () => gsap.utils.random(-50, 50, 50), duration: 3, ease: "expo.out", stagger: { each: 0.125, ease: "power1.out", }, }); tl.from( heading, { y: "25vh", ease: "expo.inOut", duration: 2.75, }, 1.75 ); tl.set( shuffled, { opacity: 1, stagger: { each: 0.125, ease: "power1.out", }, }, 0.55 ); }); $("[data-anim-load=words]").each(function () { const elDelay = $(this).attr("data-anim-load-delay") || 0; gsap.from($(this).find(".word"), { delay: globalIntroDelay + elDelay, yPercent: 100, duration: 1.5, ease: "expo.out", stagger: { each: 0.05, }, }); }); $("[data-anim-load=lines]").each(function () { const elDelay = $(this).attr("data-anim-load-delay") || 0; gsap.from($(this).find(".line"), { yPercent: 100, scale: 0.5, delay: globalIntroDelay + elDelay, duration: 1.75, ease: "expo.out", stagger: { each: 0.125, }, }); }); $("[data-anim-load=fade]").each(function () { const elDelay = $(this).attr("data-anim-load-delay") || 0; gsap.from($(this), { opacity: 0, delay: globalIntroDelay + elDelay, duration: 1.66, ease: "power3.out", }); }); $("[data-anim-load=slide-up-fade]").each(function () { const elDelay = $(this).attr("data-anim-load-delay") || 0; gsap.from($(this), { yPercent: 50, opacity: 0, delay: globalIntroDelay + elDelay, duration: 2, ease: "expo.out", }); }); $("[data-anim-load=children-slide-up-fade]").each(function () { const elDelay = $(this).attr("data-anim-load-delay") || 0; gsap.from($(this).children(), { yPercent: 50, opacity: 0, delay: globalIntroDelay + elDelay, duration: 2, ease: "expo.out", stagger: { each: 0.125, }, }); }); $("[data-anim-load=pip-heading]").each(function () { let tl = gsap.timeline(); tl.from($(this).find(".line"), { xPercent: -50, duration: 1.66, ease: "expo.out", opacity: 0, stagger: { each: 0.125, }, }); tl.from( $(this).find(".pip-highlight"), { "--scaleX": "0%", duration: 1.66, ease: "expo.out", }, "<12.5%" ); }); // $("[data-anim-load=nav").each(function () { // gsap.from($(this), { // yPercent: -125, // opacity: 0, // // scale: 0.75, // ease: "expo.out", // duration: 1.5, // delay: globalIntroDelay + 3.125, // }); // }); } function initScrollAnimations() { $("[data-anim-scroll=words]").each(function () { gsap.from($(this).find(".word"), { yPercent: 115, duration: 1.5, ease: "expo.out", stagger: { each: 0.05, }, scrollTrigger: { trigger: $(this), start: "top bottom", end: "top 90%", toggleActions: "none play none reset", }, }); }); $("[data-anim-scroll=lines]").each(function () { gsap.from($(this).find(".line"), { yPercent: 100, duration: 1.75, ease: "expo.out", stagger: { each: 0.1, }, scrollTrigger: { trigger: $(this), start: "top bottom", end: "top 90%", toggleActions: "none play none reset", }, }); }); $("[data-anim-scroll=fade]").each(function () { gsap.from($(this), { opacity: 0, duration: 1.66, ease: "expo.out", scrollTrigger: { trigger: $(this), start: "top bottom", end: "top 90%", toggleActions: "none play none reset", }, }); }); $("[data-anim-scroll=pip-highlight]").each(function () { gsap.from($(this), { "--scaleX": "0%", duration: 1.66, ease: "expo.out", delay: 0.33, scrollTrigger: { trigger: $(this), start: "top bottom", end: "top 90%", toggleActions: "none play none reset", }, }); }); $("[data-anim-scroll=mask-diagonal]").each(function () { gsap.fromTo( $(this), { clipPath: "polygon(0% 0%, 0% 0%, 0% 0%)", }, { clipPath: "polygon(0% 0%, 250% 0%, 0% 250%)", duration: 2, ease: "power1.inOut", scrollTrigger: { trigger: $(this), start: "top bottom", end: "top 90%", toggleActions: "none play none reset", }, } ); }); $("[data-anim-scroll=children-fade]").each(function () { const children = $(this).children(); const childTarget = $(this).find("[data-anim-target]"); const animTarget = childTarget.length ? childTarget : children; gsap.from(animTarget, { opacity: 0, duration: 1.66, yPercent: 15, ease: "power3.out", stagger: { each: 0.075, }, scrollTrigger: { trigger: $(this), start: "top bottom", end: "top 90%", toggleActions: "none play none reset", }, }); }); $("[data-anim-scroll=children-scale]").each(function () { const children = $(this).children(); const childTarget = $(this).find("[data-anim-target]"); const animTarget = childTarget.length ? childTarget : children; gsap.from(animTarget, { scale: 0, duration: 1.66, yPercent: 15, ease: "power3.out", stagger: { each: 0.125, }, scrollTrigger: { trigger: $(this), start: "top bottom", end: "top 90%", toggleActions: "none play none reset", }, }); }); } function initMagneticEffect() { const magnets = document.querySelectorAll("[data-magnetic-strength]"); if (window.innerWidth <= 991) return; // Helper to kill tweens and reset an element. const resetEl = (el, immediate) => { if (!el) return; gsap.killTweensOf(el); (immediate ? gsap.set : gsap.to)(el, { x: "0em", y: "0em", rotate: "0deg", clearProps: "all", ...(!immediate && { ease: "elastic.out(1, 0.3)", duration: 1.6 }), }); }; const resetOnEnter = (e) => { const m = e.currentTarget; resetEl(m, true); resetEl(m.querySelector("[data-magnetic-inner-target]"), true); }; const moveMagnet = (e) => { const m = e.currentTarget, b = m.getBoundingClientRect(), strength = parseFloat(m.getAttribute("data-magnetic-strength")) || 25, inner = m.querySelector("[data-magnetic-inner-target]"), innerStrength = parseFloat(m.getAttribute("data-magnetic-strength-inner")) || strength, offsetX = ((e.clientX - b.left) / m.offsetWidth - 0.5) * (strength / 16), offsetY = ((e.clientY - b.top) / m.offsetHeight - 0.5) * (strength / 16); gsap.to(m, { x: offsetX + "em", y: offsetY + "em", rotate: "0.001deg", ease: "power4.out", duration: 1.6, }); if (inner) { const innerOffsetX = ((e.clientX - b.left) / m.offsetWidth - 0.5) * (innerStrength / 16), innerOffsetY = ((e.clientY - b.top) / m.offsetHeight - 0.5) * (innerStrength / 16); gsap.to(inner, { x: innerOffsetX + "em", y: innerOffsetY + "em", rotate: "0.001deg", ease: "power4.out", duration: 2, }); } }; const resetMagnet = (e) => { const m = e.currentTarget, inner = m.querySelector("[data-magnetic-inner-target]"); gsap.to(m, { x: "0em", y: "0em", ease: "elastic.out(1, 0.3)", duration: 1.6, clearProps: "all", }); if (inner) { gsap.to(inner, { x: "0em", y: "0em", ease: "elastic.out(1, 0.3)", duration: 2, clearProps: "all", }); } }; magnets.forEach((m) => { m.addEventListener("mouseenter", resetOnEnter); m.addEventListener("mousemove", moveMagnet); m.addEventListener("mouseleave", resetMagnet); }); } function initLogoWallCycle() { const loopDelay = 1.5; // Loop Duration const duration = 0.9; // Animation Duration document.querySelectorAll("[data-logo-wall-cycle-init]").forEach((root) => { const list = root.querySelector("[data-logo-wall-list]"); const items = Array.from(list.querySelectorAll("[data-logo-wall-item]")); const shuffleFront = root.getAttribute("data-logo-wall-shuffle") !== "false"; const originalTargets = items .map((item) => item.querySelector("[data-logo-wall-target]")) .filter(Boolean); let visibleItems = []; let visibleCount = 0; let pool = []; let pattern = []; let patternIndex = 0; let tl; function isVisible(el) { return window.getComputedStyle(el).display !== "none"; } function shuffleArray(arr) { const a = arr.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } function setup() { if (tl) { tl.kill(); } visibleItems = items.filter(isVisible); visibleCount = visibleItems.length; pattern = shuffleArray(Array.from({ length: visibleCount }, (_, i) => i)); patternIndex = 0; // remove all injected targets items.forEach((item) => { item .querySelectorAll("[data-logo-wall-target]") .forEach((old) => old.remove()); }); pool = originalTargets.map((n) => n.cloneNode(true)); let front, rest; if (shuffleFront) { const shuffledAll = shuffleArray(pool); front = shuffledAll.slice(0, visibleCount); rest = shuffleArray(shuffledAll.slice(visibleCount)); } else { front = pool.slice(0, visibleCount); rest = shuffleArray(pool.slice(visibleCount)); } pool = front.concat(rest); for (let i = 0; i < visibleCount; i++) { const parent = visibleItems[i].querySelector("[data-logo-wall-target-parent]") || visibleItems[i]; parent.appendChild(pool.shift()); } tl = gsap.timeline({ repeat: -1, repeatDelay: loopDelay }); tl.call(swapNext); tl.play(); } function swapNext() { const nowCount = items.filter(isVisible).length; if (nowCount !== visibleCount) { setup(); return; } if (!pool.length) return; const idx = pattern[patternIndex % visibleCount]; patternIndex++; const container = visibleItems[idx]; const parent = container.querySelector("[data-logo-wall-target-parent]") || container.querySelector("*:has(> [data-logo-wall-target])") || container; const existing = parent.querySelectorAll("[data-logo-wall-target]"); if (existing.length > 1) return; const current = parent.querySelector("[data-logo-wall-target]"); const incoming = pool.shift(); gsap.set(incoming, { yPercent: 50, autoAlpha: 0 }); parent.appendChild(incoming); if (current) { gsap.to(current, { yPercent: -50, autoAlpha: 0, duration, ease: "expo.inOut", onComplete: () => { current.remove(); pool.push(current); }, }); } gsap.to(incoming, { yPercent: 0, autoAlpha: 1, duration, delay: 0.1, ease: "expo.inOut", }); } setup(); ScrollTrigger.create({ trigger: root, start: "top bottom", end: "bottom top", onEnter: () => tl.play(), onLeave: () => tl.pause(), onEnterBack: () => tl.play(), onLeaveBack: () => tl.pause(), }); document.addEventListener("visibilitychange", () => document.hidden ? tl.pause() : tl.play() ); }); } function initMarqueeScrollDirection() { document .querySelectorAll("[data-marquee-scroll-direction-target]") .forEach((marquee) => { // Query marquee elements const marqueeContent = marquee.querySelector( "[data-marquee-collection-target]" ); const marqueeScroll = marquee.querySelector( "[data-marquee-scroll-target]" ); if (!marqueeContent || !marqueeScroll) return; // Get data attributes const { marqueeSpeed: speed, marqueeDirection: direction, marqueeDuplicate: duplicate, marqueeScrollSpeed: scrollSpeed, } = marquee.dataset; // Convert data attributes to usable types const marqueeSpeedAttr = parseFloat(speed); const marqueeDirectionAttr = direction === "right" ? 1 : -1; // 1 for right, -1 for left const duplicateAmount = parseInt(duplicate || 0); const scrollSpeedAttr = parseFloat(scrollSpeed); const speedMultiplier = window.innerWidth < 479 ? 0.25 : window.innerWidth < 991 ? 0.5 : 1; let marqueeSpeed = marqueeSpeedAttr * (marqueeContent.offsetWidth / window.innerWidth) * speedMultiplier; // Precompute styles for the scroll container marqueeScroll.style.marginLeft = `${scrollSpeedAttr * -1}%`; marqueeScroll.style.width = `${scrollSpeedAttr * 2 + 100}%`; // Duplicate marquee content if (duplicateAmount > 0) { const fragment = document.createDocumentFragment(); for (let i = 0; i < duplicateAmount; i++) { const clone = marqueeContent.cloneNode(true); clone.setAttribute("aria-hidden", "true"); fragment.appendChild(clone); } marqueeScroll.appendChild(fragment); } // GSAP animation for marquee content const marqueeItems = marquee.querySelectorAll( "[data-marquee-collection-target]" ); const animation = gsap .to(marqueeItems, { xPercent: -100, // Move completely out of view repeat: -1, duration: marqueeSpeed, ease: "linear", }) .totalProgress(0.5); // Initialize marquee in the correct direction gsap.set(marqueeItems, { xPercent: marqueeDirectionAttr === 1 ? 100 : -100, }); animation.timeScale(marqueeDirectionAttr); // Set correct direction animation.play(); // Start animation immediately // Set initial marquee status marquee.setAttribute("data-marquee-status", "normal"); // ScrollTrigger logic for direction inversion ScrollTrigger.create({ trigger: marquee, start: "top bottom", end: "bottom top", onUpdate: (self) => { const isInverted = self.direction === 1; // Scrolling down const currentDirection = isInverted ? -marqueeDirectionAttr : marqueeDirectionAttr; // Update animation direction and marquee status animation.timeScale(currentDirection); marquee.setAttribute( "data-marquee-status", isInverted ? "normal" : "inverted" ); }, }); // Extra speed effect on scroll const tl = gsap.timeline({ scrollTrigger: { trigger: marquee, start: "0% 100%", end: "100% 0%", scrub: 0.1, }, }); const scrollStart = marqueeDirectionAttr === -1 ? scrollSpeedAttr : -scrollSpeedAttr; const scrollEnd = -scrollStart; tl.fromTo( marqueeScroll, { x: `${scrollStart}vw` }, { x: `${scrollEnd}vw`, ease: "none" } ); }); } function initSwipers() { $("[data-swiper=testimonials]").each(function () { const swiperTarget = $(this)[0]; const swiper = new Swiper(swiperTarget, { grabCursor: true, centeredSlides: true, slidesPerView: 1, loop: true, speed: 600, breakpoints: { 768: { slidesPerView: "auto", }, }, autoplay: { delay: 4000, }, mousewheel: { forceToAxis: true, }, keyboard: true, a11y: { enabled: true, slideRole: "listitem", }, }); }); $("[data-swiper=large-testimonials]").each(function () { const swiperTarget = $(this)[0]; const swiper = new Swiper(swiperTarget, { grabCursor: true, slidesPerView: 1, loop: true, autoHeight: true, speed: 600, autoplay: { delay: 4000, pauseOnMouseEnter: true, }, mousewheel: { forceToAxis: true, }, keyboard: true, effect: "fade", fadeEffect: { crossFade: true, }, navigation: { nextEl: '[data-swiper-next="large-testimonials"]', prevEl: '[data-swiper-prev="large-testimonials"]', disabledClass: "is-disabled", }, a11y: { enabled: true, slideRole: "listitem", }, }); // Pause autoplay until the swiper is in view swiper.autoplay.stop(); const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { swiper.autoplay.start(); } else { swiper.autoplay.stop(); } }, { threshold: 0.2 } ); observer.observe(swiperTarget); }); } function initDynamicCurrentYear() { const currentYear = new Date().getFullYear(); const currentYearElements = document.querySelectorAll("[data-current-year]"); currentYearElements.forEach((currentYearElement) => { currentYearElement.textContent = currentYear; }); } function initMomentumBasedHover() { // If this device can’t hover with a fine pointer, stop here if (!window.matchMedia("(hover: hover) and (pointer: fine)").matches) { return; } // Configuration (tweak these for feel) const xyMultiplier = 30; // multiplies pointer velocity for x/y movement const rotationMultiplier = 20; // multiplies normalized torque for rotation speed const inertiaResistance = 200; // higher = stops sooner // Pre-build clamp functions for performance const clampXY = gsap.utils.clamp(-1080, 1080); const clampRot = gsap.utils.clamp(-60, 60); // Initialize each root container document.querySelectorAll("[data-momentum-hover-init]").forEach((root) => { let prevX = 0, prevY = 0; let velX = 0, velY = 0; let rafId = null; // Track pointer velocity (throttled to RAF) root.addEventListener("mousemove", (e) => { if (rafId) return; rafId = requestAnimationFrame(() => { velX = e.clientX - prevX; velY = e.clientY - prevY; prevX = e.clientX; prevY = e.clientY; rafId = null; }); }); // Attach hover inertia to each child element root.querySelectorAll("[data-momentum-hover-element]").forEach((el) => { el.addEventListener("mouseenter", (e) => { const target = el.querySelector("[data-momentum-hover-target]"); if (!target) return; // Compute offset from center to pointer const { left, top, width, height } = target.getBoundingClientRect(); const centerX = left + width / 2; const centerY = top + height / 2; const offsetX = e.clientX - centerX; const offsetY = e.clientY - centerY; // Compute raw torque (px²/frame) const rawTorque = offsetX * velY - offsetY * velX; // Normalize torque so rotation ∝ pointer speed (deg/sec) const leverDist = Math.hypot(offsetX, offsetY) || 1; const angularForce = rawTorque / leverDist; // Calculate and clamp velocities const velocityX = clampXY(velX * xyMultiplier); const velocityY = clampXY(velY * xyMultiplier); const rotationVelocity = clampRot(angularForce * rotationMultiplier); // Apply GSAP inertia tween gsap.to(target, { inertia: { x: { velocity: velocityX, end: 0 }, y: { velocity: velocityY, end: 0 }, rotation: { velocity: rotationVelocity, end: 0 }, resistance: inertiaResistance, }, }); }); }); }); } function initLottieAnimations() { const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); if (isSafari) return; const reduceMotion = window.matchMedia( "(prefers-reduced-motion: reduce)" ).matches; document.querySelectorAll("[data-lottie]").forEach((target) => { const loadDelay = target.getAttribute("data-lottie-load"); // ── Load-based lotties (delayed autoplay after page load) ── if (loadDelay !== null) { const delaySec = parseFloat(loadDelay) || 0; const anim = lottie.loadAnimation({ container: target, renderer: "svg", loop: false, autoplay: false, // Always start paused; we control the play timing path: target.getAttribute("data-lottie-src"), }); anim.addEventListener("DOMLoaded", () => { // Show first frame immediately so container isn't empty anim.goToAndStop(0, true); if (!reduceMotion) { setTimeout(() => anim.play(), delaySec * 1000); } }); return; // Skip ScrollTrigger setup for this element } // ── Scroll-based lotties (existing behaviour) ── let anim; ScrollTrigger.create({ trigger: target, start: "top bottom+=50%", end: "bottom top-=25%", onEnter: handleEnter, onEnterBack: handleEnter, onLeave: handleLeave, onLeaveBack: handleLeave, }); function handleEnter() { if (!target.hasAttribute("data-lottie-fired")) { target.setAttribute("data-lottie-fired", "true"); anim = lottie.loadAnimation({ container: target, renderer: "svg", loop: false, autoplay: false, path: target.getAttribute("data-lottie-src"), }); anim.addEventListener("DOMLoaded", () => { if (reduceMotion) { const frame = parseInt( target.getAttribute("data-lottie-frame") || "0", 10 ); anim.goToAndStop(frame, true); } }); } if (anim && !reduceMotion) { ScrollTrigger.create({ trigger: target, start: "top bottom", onEnter: () => { anim.goToAndStop(0, true); anim.play(); }, }); } } function handleLeave() { if (anim && !reduceMotion) { anim.pause(); } } }); } function initCheckSectionThemeScroll() { const navBarHeight = document.querySelector("[data-nav-bar-height]"); const offsetPx = navBarHeight ? navBarHeight.offsetHeight / 2 : 0; const themeNavEls = document.querySelectorAll("[data-theme-nav]"); const bgNavEls = document.querySelectorAll("[data-bg-nav]"); function applyTheme(themeSection) { const themeVal = themeSection.getAttribute("data-theme-section"); themeNavEls.forEach(function (elem) { if (elem.getAttribute("data-theme-nav") !== themeVal) { elem.setAttribute("data-theme-nav", themeVal); } }); const bgVal = themeSection.getAttribute("data-bg-section"); bgNavEls.forEach(function (elem) { if (elem.getAttribute("data-bg-nav") !== bgVal) { elem.setAttribute("data-bg-nav", bgVal); } }); } document.querySelectorAll("[data-theme-section]").forEach(function (section) { ScrollTrigger.create({ trigger: section, start: "top " + offsetPx, end: "bottom " + offsetPx, onEnter: () => applyTheme(section), onEnterBack: () => applyTheme(section), }); }); } function initTabSystem() { const wrappers = document.querySelectorAll('[data-tabs="wrapper"]'); wrappers.forEach((wrapper) => { const contentItems = wrapper.querySelectorAll('[data-tabs="content-item"]'); const visualItems = wrapper.querySelectorAll('[data-tabs="visual-item"]'); const autoplay = wrapper.dataset.tabsAutoplay === "true"; const autoplayDuration = parseInt(wrapper.dataset.tabsAutoplayDuration) || 5000; let activeContent = null; let activeVisual = null; let isAnimating = false; let isInView = false; let progressBarTween = null; function startProgressBar(index) { if (progressBarTween) progressBarTween.kill(); const bar = contentItems[index].querySelector( '[data-tabs="item-progress"]' ); if (!bar) return; gsap.set(bar, { scaleX: 0, transformOrigin: "left center" }); progressBarTween = gsap.to(bar, { scaleX: 1, duration: autoplayDuration / 1000, ease: "power1.inOut", onComplete: () => { if (!isAnimating && isInView) { const nextIndex = (index + 1) % contentItems.length; switchTab(nextIndex); } }, }); // If not in view when starting, pause immediately if (!isInView) progressBarTween.pause(); } function switchTab(index) { if (isAnimating || contentItems[index] === activeContent) return; isAnimating = true; if (progressBarTween) progressBarTween.kill(); const outgoingContent = activeContent; const outgoingVisual = activeVisual; const outgoingBar = outgoingContent?.querySelector( '[data-tabs="item-progress"]' ); const incomingContent = contentItems[index]; const incomingVisual = visualItems[index]; const incomingBar = incomingContent.querySelector( '[data-tabs="item-progress"]' ); outgoingContent?.classList.remove("active"); outgoingVisual?.classList.remove("active"); incomingContent.classList.add("active"); incomingVisual.classList.add("active"); const tl = gsap.timeline({ defaults: { duration: 0.65, ease: "power3" }, onComplete: () => { activeContent = incomingContent; activeVisual = incomingVisual; isAnimating = false; if (autoplay) startProgressBar(index); }, }); if (outgoingContent) { outgoingContent.classList.remove("active"); outgoingVisual?.classList.remove("active"); tl.set(outgoingBar, { transformOrigin: "right center" }) .to(outgoingBar, { scaleX: 0, duration: 0.3 }, 0) .to( outgoingVisual, { autoAlpha: 0, yPercent: -25, ease: "expo.out", }, 0 ) .to( outgoingContent.querySelector('[data-tabs="item-details"]'), { height: 0 }, 0 ); } incomingContent.classList.add("active"); incomingVisual.classList.add("active"); tl.fromTo( incomingVisual, { autoAlpha: 0, yPercent: 25, }, { autoAlpha: 1, yPercent: 0, rotate: 0, ease: "expo.out" }, 0 ) .fromTo( incomingContent.querySelector('[data-tabs="item-details"]'), { height: 0 }, { height: "auto" }, 0 ) .set(incomingBar, { scaleX: 0, transformOrigin: "left center" }, 0); } // on page load, set first to active switchTab(0); // Pause/resume autoplay based on viewport visibility ScrollTrigger.create({ trigger: wrapper, start: "top bottom", end: "bottom top", onEnter: () => { isInView = true; if (progressBarTween?.paused()) progressBarTween.resume(); }, onLeave: () => { isInView = false; if (progressBarTween) progressBarTween.pause(); }, onEnterBack: () => { isInView = true; if (progressBarTween?.paused()) progressBarTween.resume(); }, onLeaveBack: () => { isInView = false; if (progressBarTween) progressBarTween.pause(); }, }); // switch tabs on click contentItems.forEach((item, i) => item.addEventListener("click", () => { if (item === activeContent) return; switchTab(i); }) ); }); } function initNumberOdometer() { const prefersReducedMotion = window.matchMedia( "(prefers-reduced-motion: reduce)" ).matches; const initFlag = "data-odometer-initialized"; const activeTweens = new WeakMap(); // Configuration const defaults = { duration: 1, ease: "power3.out", elementStagger: 0.1, digitStagger: 0.04, revealDuration: 0.5, revealEase: "power2.out", triggerStart: "top 80%", staggerOrder: "left", digitCycles: 2, }; // Scroll-triggered groups document.querySelectorAll("[data-odometer-group]").forEach((group) => { if (group.hasAttribute(initFlag)) return; group.setAttribute(initFlag, ""); const elements = Array.from( group.querySelectorAll("[data-odometer-element]") ); if (!elements.length || prefersReducedMotion) return; const staggerOrder = group.getAttribute("data-odometer-stagger-order") || defaults.staggerOrder; const triggerStart = group.getAttribute("data-odometer-trigger-start") || defaults.triggerStart; const elementStagger = parseFloat(group.getAttribute("data-odometer-stagger")) || defaults.elementStagger; const elementData = elements.map((el) => { const originalText = el.textContent.trim(); const hasExplicitStart = el.hasAttribute("data-odometer-start"); const startValue = parseFloat(el.getAttribute("data-odometer-start")) || 0; const duration = parseFloat(el.getAttribute("data-odometer-duration")) || defaults.duration; const step = getLineHeightRatio(el); let segments = parseSegments(originalText); segments = mapStartDigits(segments, startValue); segments = markHiddenSegments(segments, startValue); const grow = shouldGrow(el, hasExplicitStart, startValue, segments); const { rollers, revealEls } = buildRollerDOM(el, segments, step, grow); const fontSize = parseFloat(getComputedStyle(el).fontSize); const revealData = revealEls.map((revealEl) => { const widthEm = revealEl.offsetWidth / fontSize; gsap.set(revealEl, { width: 0, overflow: "hidden" }); return { el: revealEl, widthEm }; }); return { el, rollers, duration, step, revealData, originalText }; }); const ordered = applyStaggerOrder(elementData, staggerOrder); const tl = gsap.timeline({ scrollTrigger: { trigger: group, start: triggerStart, once: true, }, onComplete() { elementData.forEach(({ el, originalText, step }) => { cleanupElement(el, originalText); }); }, }); ordered.forEach((data, orderIdx) => { const { rollers, duration, step, revealData } = data; const offset = orderIdx * elementStagger; revealData.forEach(({ el, widthEm }) => { tl.to( el, { width: widthEm + "em", opacity: 1, duration: defaults.revealDuration, ease: defaults.revealEase, }, offset ); }); rollers.forEach(({ roller, targetPos }, digitIdx) => { const reversedIdx = rollers.length - 1 - digitIdx; tl.to( roller, { y: -targetPos * step + "em", duration, ease: defaults.ease, force3D: true, }, offset + reversedIdx * defaults.digitStagger ); }); }); }); // Programmatic update (optional add-on) return function updateOdometer(el, newText, options = {}) { const currentText = el.textContent.trim(); if (currentText === newText) return; const duration = options.duration || defaults.duration; const ease = options.ease || defaults.ease; const step = getLineHeightRatio(el); // Kill any running animation and clear its inline style locks const existing = activeTweens.get(el); if (existing) { existing.kill(); gsap.set(el, { clearProps: "width,overflow" }); } // Measure current width before rebuilding (in em for responsive scaling) const fontSize = parseFloat(getComputedStyle(el).fontSize); const oldWidthEm = el.getBoundingClientRect().width / fontSize; // Parse current text as start, new text as end const startSegments = parseSegments(currentText); const startDigitsStr = startSegments .filter((s) => s.type === "digit") .map((s) => s.char) .join(""); const startValue = parseInt(startDigitsStr, 10) || 0; let segments = parseSegments(newText); segments = mapStartDigits(segments, startValue); segments = markHiddenSegments(segments, startValue); const { rollers, revealEls } = buildRollerDOM(el, segments, step, true); // Measure new natural width (in em) const newWidthEm = el.getBoundingClientRect().width / fontSize; const widthChanged = Math.abs(oldWidthEm - newWidthEm) > 0.01; // Lock to old width for smooth transition if (widthChanged) { gsap.set(el, { width: oldWidthEm + "em", overflow: "hidden" }); } const tl = gsap.timeline({ onComplete() { cleanupElement(el, newText); activeTweens.delete(el); }, }); activeTweens.set(el, tl); // Animate element width if (widthChanged) { tl.to( el, { width: newWidthEm + "em", duration: defaults.revealDuration, ease: defaults.revealEase, }, 0 ); } // Fade in hidden statics revealEls.forEach((revealEl) => { if (revealEl.getAttribute("data-odometer-part") === "static") { tl.to(revealEl, { opacity: 1, duration: 0.2 }, 0); } }); // Roll digits rollers.forEach(({ roller, targetPos }, digitIdx) => { const reversedIdx = rollers.length - 1 - digitIdx; tl.to( roller, { y: -targetPos * step + "em", duration, ease, force3D: true, }, reversedIdx * defaults.digitStagger ); }); }; // Helpers function getLineHeightRatio(el) { const cs = getComputedStyle(el); const lh = cs.lineHeight; if (lh === "normal") return 1.2; return parseFloat(lh) / parseFloat(cs.fontSize); } function parseSegments(text) { return [...text].map((char) => ({ type: /\d/.test(char) ? "digit" : "static", char, })); } function mapStartDigits(segments, startValue) { const digitSlots = segments.filter((s) => s.type === "digit"); const padded = String(Math.floor(Math.abs(startValue))) .padStart(digitSlots.length, "0") .slice(-digitSlots.length); let di = 0; return segments.map((s) => s.type === "digit" ? { ...s, startDigit: parseInt(padded[di++], 10) } : s ); } function markHiddenSegments(segments, startValue) { const totalDigits = segments.filter((s) => s.type === "digit").length; const absStart = Math.floor(Math.abs(startValue)); const startDigitCount = absStart === 0 ? 1 : String(absStart).length; const leadingZeros = Math.max(0, totalDigits - startDigitCount); if (leadingZeros === 0) return segments; let digitsSeen = 0; let firstDigitSeen = false; let prevDigitHidden = false; return segments.map((seg) => { if (seg.type === "digit") { firstDigitSeen = true; const hidden = digitsSeen < leadingZeros; prevDigitHidden = hidden; digitsSeen++; return { ...seg, hidden }; } const hidden = firstDigitSeen && prevDigitHidden; return { ...seg, hidden }; }); } function shouldGrow(el, hasExplicitStart, startValue, segments) { if (el.hasAttribute("data-odometer-grow")) { return el.getAttribute("data-odometer-grow") !== "false"; } if (!hasExplicitStart) return false; const absStart = Math.floor(Math.abs(startValue)); const startDigitCount = absStart === 0 ? 1 : String(absStart).length; const endDigitCount = segments.filter((s) => s.type === "digit").length; return startDigitCount < endDigitCount; } function buildRollerDOM(el, segments, step, grow) { el.innerHTML = ""; el.style.height = ""; const rollers = []; const revealEls = []; const totalCells = 10 * defaults.digitCycles; segments.forEach((seg) => { if (seg.type === "static") { const span = document.createElement("span"); span.setAttribute("data-odometer-part", "static"); span.style.height = step + "em"; span.style.lineHeight = step; span.textContent = seg.char; el.appendChild(span); if (grow && seg.hidden) { gsap.set(span, { opacity: 0 }); revealEls.push(span); } return; } const mask = document.createElement("span"); mask.setAttribute("data-odometer-part", "mask"); mask.style.height = step + "em"; mask.style.lineHeight = step; const roller = document.createElement("span"); roller.setAttribute("data-odometer-part", "roller"); roller.style.lineHeight = step; const digits = []; for (let d = 0; d < totalCells; d++) { digits.push(d % 10); } roller.textContent = digits.join("\n"); mask.appendChild(roller); el.appendChild(mask); const startDigit = seg.startDigit || 0; const isReveal = grow && seg.hidden; gsap.set(roller, { y: isReveal ? step + "em" : -startDigit * step + "em", }); const endDigit = parseInt(seg.char, 10); const targetPos = endDigit > startDigit ? endDigit : 10 + endDigit; rollers.push({ roller, targetPos }); if (isReveal) revealEls.push(mask); }); return { rollers, revealEls }; } function cleanupElement(el, originalText) { el.style.overflow = ""; el.style.height = ""; // Remove rollers, set final digit, clear inline bloat (but preserve width) const digits = [...originalText].filter((c) => /\d/.test(c)); let di = 0; el.querySelectorAll('[data-odometer-part="mask"]').forEach((mask) => { const roller = mask.querySelector('[data-odometer-part="roller"]'); if (roller) roller.remove(); mask.textContent = digits[di++] || ""; mask.style.opacity = ""; mask.style.overflow = ""; }); el.querySelectorAll('[data-odometer-part="static"]').forEach((stat) => { stat.style.opacity = ""; }); } function recalcOnResize() { document.querySelectorAll("[data-odometer-element]").forEach((el) => { // Force-complete any running programmatic animation const running = activeTweens.get(el); if (running) { running.progress(1); activeTweens.delete(el); } const hasRollers = el.querySelector('[data-odometer-part="roller"]'); if (hasRollers) { // Pre-triggered: recalculate step-based inline styles const step = getLineHeightRatio(el); el.querySelectorAll('[data-odometer-part="mask"]').forEach((mask) => { mask.style.height = step + "em"; mask.style.lineHeight = step; }); el.querySelectorAll('[data-odometer-part="roller"]').forEach( (roller) => { roller.style.lineHeight = step; } ); el.querySelectorAll('[data-odometer-part="static"]').forEach((stat) => { stat.style.lineHeight = step; }); } // Completed elements: width is em-based, scales automatically, don't touch }); ScrollTrigger.refresh(); } let resizeTimer; let lastWidth = window.innerWidth; window.addEventListener("resize", () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { if (window.innerWidth === lastWidth) return; lastWidth = window.innerWidth; recalcOnResize(); }, 250); }); function applyStaggerOrder(items, order) { const arr = [...items]; if (order === "right") return arr.reverse(); if (order === "random") return shuffleArray(arr); return arr; } function shuffleArray(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; } } function initVettingProcess() { $("[data-section=vetting-process]").each(function () { const section = $(this); const steps = section.find("[data-vetting-process-step]"); let lastSide = 1; // tracks previous sign: 1 or -1 steps.each(function (i) { const step = $(this); const isFirst = i === 0; // Flip side, then pick a random magnitude lastSide *= -1; const xStart = isFirst ? 0 : lastSide * gsap.utils.random(25, 100, 25); gsap.fromTo( step, { rotate: gsap.utils.random(-15, 15, 5), scale: 0.25, xPercent: xStart, yPercent: isFirst ? 0 : -50, zIndex: -1, }, { rotate: gsap.utils.random(-5, 5, 1), zIndex: 1, xPercent: 0, scale: 1, yPercent: isFirst ? 0 : i * 10, scrollTrigger: { trigger: step, start: "top 110%", end: "center center", scrub: true, }, } ); }); }); } function initMouseMove() { var MAX_REM = 10; var maxPx = MAX_REM * parseFloat(getComputedStyle(document.documentElement).fontSize); // Bail on touch devices if ("ontouchstart" in window) return; var targets = []; $("[data-mouse-move-strength]").each(function () { var el = $(this)[0]; var strength = parseFloat($(this).attr("data-mouse-move-strength")) || 0; targets.push({ strength: strength, xTo: gsap.quickTo(el, "x", { duration: 1.5, ease: "power3" }), yTo: gsap.quickTo(el, "y", { duration: 1.5, ease: "power3" }), }); }); if (!targets.length) return; $(window).on("mousemove", function (e) { // -1 … 1 from viewport center var nx = (e.clientX / window.innerWidth - 0.5) * 2; var ny = (e.clientY / window.innerHeight - 0.5) * 2; targets.forEach(function (t) { t.xTo(nx * -maxPx * t.strength); t.yTo(ny * -maxPx * t.strength); }); }); } function initVimeoPlayer() { // Select all elements that have [data-vimeo-player-init] const vimeoPlayers = document.querySelectorAll("[data-vimeo-player-init]"); vimeoPlayers.forEach(function (vimeoElement, index) { // Add Vimeo URL ID to the iframe [src] // Looks like: https://player.vimeo.com/video/1019191082 const vimeoVideoID = vimeoElement.getAttribute("data-vimeo-video-id"); if (!vimeoVideoID) return; const vimeoVideoURL = `https://player.vimeo.com/video/${vimeoVideoID}?api=1&background=1&autoplay=0&loop=0&muted=1`; vimeoElement.querySelector("iframe").setAttribute("src", vimeoVideoURL); // Assign an ID to each element const videoIndexID = "vimeo-player-advanced-index-" + index; vimeoElement.setAttribute("id", videoIndexID); const iframeID = vimeoElement.id; const player = new Vimeo.Player(iframeID); // Update Aspect Ratio if [data-vimeo-update-size="true"] if (vimeoElement.getAttribute("data-vimeo-update-size") === "true") { player.getVideoWidth().then(function (width) { player.getVideoHeight().then(function (height) { const beforeEl = vimeoElement.querySelector(".vimeo-player__before"); if (beforeEl) { beforeEl.style.paddingTop = (height / width) * 100 + "%"; } }); }); } // Update sizing if [data-vimeo-update-size="cover"] let videoAspectRatio; if (vimeoElement.getAttribute("data-vimeo-update-size") === "cover") { player.getVideoWidth().then(function (width) { player.getVideoHeight().then(function (height) { videoAspectRatio = height / width; const beforeEl = vimeoElement.querySelector(".vimeo-player__before"); if (beforeEl) { beforeEl.style.paddingTop = "0%"; } adjustVideoSizing(); }); }); } // Function to adjust video sizing (to cover the video) function adjustVideoSizing() { const containerRatio = vimeoElement.offsetHeight / vimeoElement.offsetWidth; const iframeWrapper = vimeoElement.querySelector(".vimeo-player__iframe"); if (iframeWrapper && videoAspectRatio) { if (containerRatio > videoAspectRatio) { // Container is taller relative to the video const widthFactor = containerRatio / videoAspectRatio; iframeWrapper.style.width = widthFactor * 100 + "%"; iframeWrapper.style.height = "100%"; } else { // Container is wider relative to the video const heightFactor = videoAspectRatio / containerRatio; iframeWrapper.style.height = heightFactor * 100 + "%"; iframeWrapper.style.width = "100%"; } } } // Adjust video sizing on resize if (vimeoElement.getAttribute("data-vimeo-update-size") === "cover") { window.addEventListener("resize", adjustVideoSizing); } // Loaded & play player.on("play", function () { vimeoElement.setAttribute("data-vimeo-loaded", "true"); vimeoElement.setAttribute("data-vimeo-playing", "true"); }); // Autoplay if (vimeoElement.getAttribute("data-vimeo-autoplay") === "false") { // Autoplay = false player.setVolume(1); player.pause(); } else { // Autoplay = true player.setVolume(0); vimeoElement.setAttribute("data-vimeo-muted", "true"); // If paused-by-user === false, do scroll-based autoplay // New — ScrollTrigger handles viewport checks if (vimeoElement.getAttribute("data-vimeo-paused-by-user") === "false") { const vimeoScrollTrigger = ScrollTrigger.create({ trigger: vimeoElement, start: "top bottom", end: "bottom top", onEnter: () => vimeoPlayerPlay(), onLeave: () => vimeoPlayerPause(), onEnterBack: () => vimeoPlayerPlay(), onLeaveBack: () => vimeoPlayerPause(), }); // Store reference so the pause button can kill it vimeoElement._vimeoScrollTrigger = vimeoScrollTrigger; } } // Function: Play Video function vimeoPlayerPlay() { vimeoElement.setAttribute("data-vimeo-activated", "true"); vimeoElement.setAttribute("data-vimeo-playing", "true"); player.play(); } // Function: Pause Video function vimeoPlayerPause() { player.pause(); } // Paused player.on("pause", function () { vimeoElement.setAttribute("data-vimeo-playing", "false"); }); // Click: Play const playBtn = vimeoElement.querySelector('[data-vimeo-control="play"]'); if (playBtn) { playBtn.addEventListener("click", function () { // Always set volume to 0 first to avoid pop player.setVolume(0); vimeoPlayerPlay(); // If muted attribute is 'true', keep volume at 0, else 1 if (vimeoElement.getAttribute("data-vimeo-muted") === "true") { player.setVolume(0); } else { player.setVolume(1); } }); } // Click: Pause const pauseBtn = vimeoElement.querySelector('[data-vimeo-control="pause"]'); if (pauseBtn) { pauseBtn.addEventListener("click", function () { vimeoPlayerPause(); // If paused by user => kill the scroll-based autoplay if (vimeoElement.getAttribute("data-vimeo-autoplay") === "true") { vimeoElement.setAttribute("data-vimeo-paused-by-user", "true"); // Removing scroll listener (if you’d like) if (vimeoElement._vimeoScrollTrigger) { vimeoElement._vimeoScrollTrigger.kill(); vimeoElement._vimeoScrollTrigger = null; } } }); } // Click: Mute const muteBtn = vimeoElement.querySelector('[data-vimeo-control="mute"]'); if (muteBtn) { muteBtn.addEventListener("click", function () { if (vimeoElement.getAttribute("data-vimeo-muted") === "false") { player.setVolume(0); vimeoElement.setAttribute("data-vimeo-muted", "true"); } else { player.setVolume(1); vimeoElement.setAttribute("data-vimeo-muted", "false"); } }); } // Fullscreen // Check if Fullscreen API is supported const fullscreenSupported = !!( document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled ); const fullscreenBtn = vimeoElement.querySelector( '[data-vimeo-control="fullscreen"]' ); // Hide the fullscreen button if not supported if (!fullscreenSupported && fullscreenBtn) { fullscreenBtn.style.display = "none"; } if (fullscreenBtn) { fullscreenBtn.addEventListener("click", () => { const fullscreenElement = document.getElementById(iframeID); if (!fullscreenElement) return; const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement; if (isFullscreen) { // Exit fullscreen vimeoElement.setAttribute("data-vimeo-fullscreen", "false"); ( document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen || document.msExitFullscreen ).call(document); } else { // Enter fullscreen vimeoElement.setAttribute("data-vimeo-fullscreen", "true"); ( fullscreenElement.requestFullscreen || fullscreenElement.webkitRequestFullscreen || fullscreenElement.mozRequestFullScreen || fullscreenElement.msRequestFullscreen ).call(fullscreenElement); } }); } const handleFullscreenChange = () => { const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement; vimeoElement.setAttribute( "data-vimeo-fullscreen", isFullscreen ? "true" : "false" ); }; // Add event listeners for fullscreen changes (with vendor prefixes) [ "fullscreenchange", "webkitfullscreenchange", "mozfullscreenchange", "msfullscreenchange", ].forEach((event) => { document.addEventListener(event, handleFullscreenChange); }); // Convert seconds to mm:ss function secondsTimeSpanToHMS(s) { let h = Math.floor(s / 3600); s -= h * 3600; let m = Math.floor(s / 60); s -= m * 60; return m + ":" + (s < 10 ? "0" + s : s); } // Duration const vimeoDuration = vimeoElement.querySelector("[data-vimeo-duration]"); player.getDuration().then(function (duration) { if (vimeoDuration) { vimeoDuration.textContent = secondsTimeSpanToHMS(duration); } // Update timeline + progress max const timelineAndProgress = vimeoElement.querySelectorAll( '[data-vimeo-control="timeline"], progress' ); timelineAndProgress.forEach((el) => { el.setAttribute("max", duration); }); }); // Timeline const timelineElem = vimeoElement.querySelector( '[data-vimeo-control="timeline"]' ); const progressElem = vimeoElement.querySelector("progress"); function updateTimelineValue() { player.getDuration().then(function () { const timeVal = timelineElem.value; player.setCurrentTime(timeVal); if (progressElem) { progressElem.value = timeVal; } }); } if (timelineElem) { ["input", "change"].forEach((evt) => { timelineElem.addEventListener(evt, updateTimelineValue); }); } // Progress Time & Timeline (timeupdate) player.on("timeupdate", function (data) { if (timelineElem) { timelineElem.value = data.seconds; } if (progressElem) { progressElem.value = data.seconds; } if (vimeoDuration) { vimeoDuration.textContent = secondsTimeSpanToHMS( Math.trunc(data.seconds) ); } }); // Hide controls after hover on Vimeo player let vimeoHoverTimer; vimeoElement.addEventListener("mousemove", function () { if (vimeoElement.getAttribute("data-vimeo-hover") === "false") { vimeoElement.setAttribute("data-vimeo-hover", "true"); } clearTimeout(vimeoHoverTimer); vimeoHoverTimer = setTimeout(vimeoHoverTrue, 3000); }); function vimeoHoverTrue() { vimeoElement.setAttribute("data-vimeo-hover", "false"); } // Video Ended function vimeoOnEnd() { if (vimeoElement.getAttribute("data-vimeo-autoplay") === "false") { vimeoElement.setAttribute("data-vimeo-activated", "false"); vimeoElement.setAttribute("data-vimeo-playing", "false"); player.unload(); } else { player.play(); } } player.on("ended", vimeoOnEnd); }); } function initPIPStatement() { $("[data-section=pip-statement]").each(function () { const section = $(this); const bgEl = section.find("[data-statement-bg]"); const content = section.find("[data-statement-content]"); const statement = section.find("[data-statement]"); const highlight = statement.find("[data-statement-highlight]"); const words = statement.find(".word"); const shuffled = gsap.utils.shuffle([...words]); let tl = gsap.timeline({ scrollTrigger: { trigger: section, start: "top center", end: "bottom center", scrub: true, }, }); gsap.set(words, { opacity: 0 }); gsap.set($(this), { autoAlpha: 1 }); tl.set(words, { opacity: 0 }, 0); tl.set($(this), { autoAlpha: 1 }, 0); tl.from( bgEl, { opacity: 0, scale: 0, duration: 4, ease: "power1.inOut", }, 0 ); tl.fromTo( content, { yPercent: 75, }, { yPercent: -12.5, duration: 4, }, 0 ); tl.from( shuffled, { yPercent: () => gsap.utils.random(-25, 25, 25), xPercent: () => gsap.utils.random(-25, 25, 25), duration: 3, ease: "expo.out", stagger: { each: 0.125, ease: "power1.in", }, }, 0 ); tl.set( shuffled, { opacity: 1, duration: 2, stagger: { each: 0.125, ease: "power1.in", }, }, 0.625 ); tl.from( highlight, { "--scaleX": "0%", duration: 2, }, "<50%" ); }); } function initNav() { $("[data-nav]").each(function () { const nav = $(this); const menu = nav.find("[data-nav-menu-button]"); // scoped to this nav menu.on("click", function () { const isOpen = nav.attr("data-state") === "open"; nav.attr("data-state", isOpen ? "closed" : "open"); }); }); } function initConditionalVisibility() { // Define device types, operating systems, and browsers with their detection methods const detectionTypes = { // Device types mobile: () => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent ), tablet: () => /iPad|Android|Silk/i.test(navigator.userAgent) && !/Mobile/i.test(navigator.userAgent), desktop: () => !/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent ), touchdevice: () => "ontouchstart" in window || navigator.maxTouchPoints > 0, landscape: () => window.matchMedia("(orientation: landscape)").matches, portrait: () => window.matchMedia("(orientation: portrait)").matches, // Operating Systems ios: () => /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream, android: () => /Android/.test(navigator.userAgent), macos: () => /Mac OS X/.test(navigator.userAgent) && !/iPad|iPhone|iPod/.test(navigator.userAgent), windows: () => /Win/.test(navigator.platform), linux: () => /Linux/.test(navigator.platform), // Browsers chrome: () => /Chrome/.test(navigator.userAgent) && !/Chromium/.test(navigator.userAgent), firefox: () => /Firefox/.test(navigator.userAgent), safari: () => /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent), edge: () => /Edg/.test(navigator.userAgent), opera: () => /OPR/.test(navigator.userAgent) || /Opera/.test(navigator.userAgent), ie: () => /Trident/.test(navigator.userAgent), }; function detectTypes() { const detected = []; for (const [type, detector] of Object.entries(detectionTypes)) { if (detector()) { detected.push(type); } } return detected; } function evaluateCondition(condition, currentTypes) { const parts = condition.split("&").map((part) => part.trim()); return parts.every((part) => { const orParts = part.split("|").map((p) => p.trim()); return orParts.some((p) => { if (p.startsWith("!")) { return !currentTypes.includes(p.slice(1)); } return currentTypes.includes(p); }); }); } function updateElementVisibility() { const currentTypes = detectTypes(); const elements = document.querySelectorAll("[data-conditional-visibility]"); elements.forEach((element) => { const showAttribute = element .getAttribute("data-conditional-visibility") .toLowerCase(); const shouldShow = evaluateCondition(showAttribute, currentTypes); element.style.display = shouldShow ? "" : "none"; }); } // Run on page load and window resize window.addEventListener("load", updateElementVisibility); window.addEventListener("resize", updateElementVisibility); } function initModalBasic() { const modalGroup = document.querySelector("[data-modal-group-status]"); const modals = document.querySelectorAll("[data-modal-name]"); const modalTargets = document.querySelectorAll("[data-modal-target]"); // Open modal modalTargets.forEach((modalTarget) => { modalTarget.addEventListener("click", function () { const modalTargetName = this.getAttribute("data-modal-target"); // Close all modals modalTargets.forEach((target) => target.setAttribute("data-modal-status", "not-active") ); modals.forEach((modal) => modal.setAttribute("data-modal-status", "not-active") ); // Activate clicked modal document .querySelector(`[data-modal-target="${modalTargetName}"]`) .setAttribute("data-modal-status", "active"); document .querySelector(`[data-modal-name="${modalTargetName}"]`) .setAttribute("data-modal-status", "active"); // Set group to active if (modalGroup) { modalGroup.setAttribute("data-modal-group-status", "active"); } }); }); // Close modal document.querySelectorAll("[data-modal-close]").forEach((closeBtn) => { closeBtn.addEventListener("click", closeAllModals); }); // Close modal on `Escape` key document.addEventListener("keydown", function (event) { if (event.key === "Escape") { closeAllModals(); } }); // Function to close all modals function closeAllModals() { modalTargets.forEach((target) => target.setAttribute("data-modal-status", "not-active") ); if (modalGroup) { modalGroup.setAttribute("data-modal-group-status", "not-active"); } } function checkUrlParams() { const params = new URLSearchParams(window.location.search); modals.forEach((modal) => { const urlParam = modal.getAttribute("data-modal-url-param"); if (!urlParam) return; if (params.get(urlParam) === "true") { const modalName = modal.getAttribute("data-modal-name"); // Close any others first modalTargets.forEach((target) => target.setAttribute("data-modal-status", "not-active") ); modals.forEach((m) => m.setAttribute("data-modal-status", "not-active") ); // Open the matched modal modal.setAttribute("data-modal-status", "active"); const matchedTarget = document.querySelector( `[data-modal-target="${modalName}"]` ); if (matchedTarget) matchedTarget.setAttribute("data-modal-status", "active"); if (modalGroup) { modalGroup.setAttribute("data-modal-group-status", "active"); } } }); } checkUrlParams(); } function initCookieBanner() { var cookieName = "modalClosed"; var modal = document.querySelector("[data-cookie-modal]"); if (!modal) return; // Check if the cookie is set, if not, show the modal with animation after 1 second delay if (typeof Cookies.get(cookieName) === "undefined") { setTimeout(function () { modal.style.display = "block"; setTimeout(function () { modal.classList.add("show"); }, 10); // Small delay to ensure display: block has taken effect }, 1000); // 1 second delay } // Close modal on button click and set the cookie document .querySelector("[data-cookie-close]") .addEventListener("click", function (event) { event.preventDefault(); Cookies.set(cookieName, "ok", { expires: 7 }); // Set cookie for 7 days modal.classList.remove("show"); setTimeout(function () { modal.style.display = "none"; }, 300); // Match the duration of the fade-out transition }); } function initPIPForm() { const form = document.querySelector('[embed-container="form"]'); const steps = Array.from( document.querySelectorAll('[multistep-form="section"]') ); const nextBtn = document.querySelector('[multistep-form="next"]'); const stepCount = document.querySelector('[multistep-form="step-count"]'); if (!form || !steps.length) return; let currentStep = 0; // --- Step visibility --- function showStep(index) { steps.forEach((step, i) => { step.style.display = i === index ? "" : "none"; }); if (stepCount) { stepCount.textContent = `${index + 1}`; } } showStep(currentStep); // --- Next button --- nextBtn?.addEventListener("click", (e) => { e.preventDefault(); if (currentStep === 0) { const requiredFields = steps[currentStep].querySelectorAll( "input[required], select[required], textarea[required]" ); let isValid = true; requiredFields.forEach((field) => { if (!field.checkValidity()) { field.reportValidity(); isValid = false; } }); if (!isValid) return; } if (currentStep < steps.length - 1) { currentStep++; showStep(currentStep); } }); // --- First-step validation → next button state --- const firstStepRequiredInputs = Array.from( steps[0].querySelectorAll( "input[required], select[required], textarea[required]" ) ); function checkFirstStepValidity() { const allValid = firstStepRequiredInputs.every((input) => input.checkValidity() ); nextBtn.style.opacity = allValid ? "1" : "0.5"; nextBtn.style.pointerEvents = allValid ? "auto" : "none"; } checkFirstStepValidity(); firstStepRequiredInputs.forEach((input) => { input.addEventListener("input", checkFirstStepValidity); input.addEventListener("change", checkFirstStepValidity); }); // --- HubSpot embed on form submit --- form.addEventListener("submit", () => { const fullName = document .querySelector('[multistep-form="full-name"]') .value.trim(); const email = document .querySelector('[multistep-form="email"]') .value.trim(); const [firstName, ...lastParts] = fullName.split(" "); const lastName = lastParts.join(" "); const params = new URLSearchParams({ embed: "true", firstname: firstName || "", lastname: lastName || "", email: email || "", }); const embedContainer = document.querySelector( '[multistep-form="embed-container"]' ); embedContainer.style.display = "block"; const meetingsDiv = document.createElement("div"); meetingsDiv.className = "meetings-iframe-container"; meetingsDiv.setAttribute( "data-src", `https://meetings.hubspot.com/raffi-salama/pip-ai-demo?${params.toString()}` ); const script = document.createElement("script"); script.type = "text/javascript"; script.src = "https://static.hsappstatic.net/MeetingsEmbed/ex/MeetingsEmbedCode.js"; embedContainer.appendChild(meetingsDiv); embedContainer.appendChild(script); const formComponent = document.querySelector( '[embed-container="form-component"]' ); if (formComponent) formComponent.style.display = "none"; }); } document.addEventListener("DOMContentLoaded", function () { initLenis(); initGlobalParallax(); document.fonts.ready.then(() => { initFOUC(); initTextSplit(); initLoadAnimations(); initScrollAnimations(); initPIPStatement(); }); initMagneticEffect(); initLogoWallCycle(); initMarqueeScrollDirection(); initSwipers(); initDynamicCurrentYear(); initMomentumBasedHover(); initLottieAnimations(); initCheckSectionThemeScroll(); initTabSystem(); initNumberOdometer(); initVettingProcess(); initMouseMove(); initVimeoPlayer(); initNav(); initConditionalVisibility(); initModalBasic(); initCookieBanner(); initPIPForm(); });