gsap.config({ nullTargetWarn: false }); // Lenis Setup gsap.registerPlugin(ScrollTrigger); const lenis = new Lenis({ syncTouch: true, }); lenis.on("scroll", ScrollTrigger.update); gsap.ticker.add((time) => { lenis.raf(time * 1000); }); gsap.ticker.lagSmoothing(0); window.addEventListener("resize", () => { setTimeout(() => { ScrollTrigger.refresh(); }, 250); }); // Hover & Click if (window.matchMedia("(hover: none)").matches) { const buttons = document.querySelectorAll(".glide-button, .dark_button"); buttons.forEach((btn) => { btn.addEventListener("click", () => { btn.classList.toggle("is-active"); }); }); } // Nav Image Hover Effect let navLinks = $(".nav_dropdown_link"); let navImages = $(".nav_dropdown_img"); let navIcons = $(".nav-dropdown_icon"); let dropdownList = $(".nav_dropdown_list"); // Set initial styles navImages.css({ opacity: "0", transition: "opacity 200ms ease", }); navIcons.css("transition", "opacity 200ms ease"); // Show first image by default let firstImage = $('.nav_dropdown_img[data-image-id="1"]'); if (firstImage.length) { firstImage.css("opacity", "1"); } // Link hover interactions navLinks.each(function () { let link = $(this); let icon = link.find(".nav-dropdown_icon"); link.on("mouseenter", function () { let imageNumber = link.attr("data-image"); navImages.css("opacity", "0"); let matchingImage = $(`.nav_dropdown_img[data-image-id="${imageNumber}"]`); if (matchingImage.length) { matchingImage.css("opacity", "1"); } if (icon.length) { icon.css("opacity", "1"); } }); link.on("mouseleave", function () { if (icon.length) { icon.css("opacity", "0"); } }); }); // Reset to first image when leaving dropdown if (dropdownList.length) { dropdownList.on("mouseleave", function () { navImages.css("opacity", "0"); if (firstImage.length) { firstImage.css("opacity", "1"); } }); } // Multi-Instance Dots Cursor Repel (function () { class DotsInstance { constructor(containerSelector, customConfig = {}) { this.config = { repelStrength: 1000, effectRadius: 350, springStrength: 0.1, damping: 0.65, updateInterval: 16, minSpacing: 60, edgePadding: 40, ...customConfig, }; this.containerSelector = containerSelector; this.mouse = { x: 0, y: 0 }; this.dots = []; this.animationFrame = null; this.container = null; } init() { this.container = $( `[data-dots-container="${this.containerSelector}"]` )[0]; if (!this.container) { console.warn( `No container found with data-dots-container="${this.containerSelector}"` ); return; } let dotElements = $(this.container).find("[data-dots-item]"); if (dotElements.length === 0) { console.warn( `No [data-dots-item] elements found in container "${this.containerSelector}"` ); return; } this.randomizeDots(dotElements); setTimeout(() => { this.initializePhysics(dotElements); this.animate(); }, 10); $(document).on("mousemove", this.handleMouseMove.bind(this)); } resize() { let dotElements = $(this.container).find("[data-dots-item]"); if (dotElements.length === 0) return; // Cancel current animation if (this.animationFrame) { cancelAnimationFrame(this.animationFrame); } // Reset dots array this.dots = []; // Re-randomize and recalculate this.randomizeDots(dotElements); setTimeout(() => { this.initializePhysics(dotElements); this.animate(); }, 10); } initializePhysics(dotElements) { dotElements.each((index, el) => { let $el = $(el); let offset = $el.offset(); let width = $el.outerWidth(); let height = $el.outerHeight(); this.dots.push({ element: el, homeX: offset.left + width / 2, homeY: offset.top + height / 2, currentX: offset.left + width / 2, currentY: offset.top + height / 2, velocityX: 0, }); }); } randomizeDots(dots) { if (!this.container || dots.length === 0) return; let $container = $(this.container); let containerWidth = $container.width(); let containerHeight = $container.height(); let availableWidth = containerWidth - this.config.edgePadding * 2; let positions = []; let maxAttempts = 100; for (let i = 0; i < dots.length; i++) { let attempts = 0; let validPosition = false; let x, y; while (!validPosition && attempts < maxAttempts) { x = this.config.edgePadding + Math.random() * availableWidth; y = containerHeight / 2; validPosition = true; for (let pos of positions) { let distance = Math.abs(x - pos.x); if (distance < this.config.minSpacing) { validPosition = false; break; } } attempts++; } positions.push({ x, y }); } positions.sort((a, b) => a.x - b.x); dots.each((index, dot) => { let $dot = $(dot); let pos = positions[index]; $dot.css({ position: "absolute", left: `${pos.x}px`, top: `${pos.y}px`, transform: "translate(-50%, -50%)", }); }); $container.css("position", "relative"); } handleMouseMove(e) { this.mouse.x = e.pageX; this.mouse.y = e.pageY; } animate() { this.dots.forEach((dot) => { let dx = this.mouse.x - dot.currentX; let dy = this.mouse.y - dot.currentY; let distance = Math.sqrt(dx * dx + dy * dy); if (distance < this.config.effectRadius && distance > 0) { let force = (this.config.effectRadius - distance) / this.config.effectRadius; let repelX = -(dx / distance) * this.config.repelStrength * force; dot.velocityX += repelX * 0.01; } let homeForceX = (dot.homeX - dot.currentX) * this.config.springStrength; dot.velocityX += homeForceX; dot.velocityX *= this.config.damping; dot.currentX += dot.velocityX; let offsetX = dot.currentX - dot.homeX; $(dot.element).css("transform", `translate(${offsetX}px, 0px)`); }); this.animationFrame = requestAnimationFrame(this.animate.bind(this)); } destroy() { if (this.animationFrame) { cancelAnimationFrame(this.animationFrame); } } } function initDots() { let instances = []; $("[data-dots-container]").each(function () { let instanceName = $(this).attr("data-dots-container"); let instance = new DotsInstance(instanceName); instance.init(); instances.push(instance); }); // Debounced resize handler let resizeTimer; $(window).on("resize", function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { instances.forEach((instance) => instance.resize()); }, 150); }); $(window).on("beforeunload", function () { instances.forEach((instance) => instance.destroy()); }); } $(window).on("load", function () { initDots(); }); window.DotsInstance = DotsInstance; })(); // Page Load Pause if ($(".page_wrap").length) { let pageLoad = gsap.timeline(); pageLoad.set(".page_wrap", { opacity: 1, }); } // Swiper Setup - Logo Marquee try { $("[swiper-container='logo-marquee']").each(function () { try { let swiperContainer = $(this); let swiperEl = swiperContainer.find(".swiper")[0]; if (!swiperEl) return; let swiper = new Swiper(swiperEl, { loop: true, speed: 600, slidesPerView: 2.3, centeredSlides: true, spaceBetween: 5, autoplay: { delay: 1000, disableOnInteraction: false, }, breakpoints: { 480: { slidesPerView: 3.3, spaceBetween: 5 }, 768: { slidesPerView: 4.3, spaceBetween: 5 }, 1024: { slidesPerView: 6.3, spaceBetween: 5 }, }, watchSlidesProgress: true, on: { progress(swiper) { try { swiper.slides.forEach((slide) => { let progress = Math.abs(slide.progress); let opacity = 1 - Math.min(progress, 1) * 0.7; slide.style.opacity = opacity; }); } catch (e) { console.warn("[logo-marquee] progress handler error:", e); } }, setTransition(swiper, duration) { try { swiper.slides.forEach((slide) => { slide.style.transition = `opacity ${duration}ms ease-in-out`; }); } catch (e) { console.warn("[logo-marquee] setTransition handler error:", e); } }, }, }); } catch (e) { console.warn("[logo-marquee] Swiper init error on instance:", e); } }); } catch (e) { console.warn("[logo-marquee] Swiper setup failed:", e); } // Swiper Setup - Testimonials try { $("[swiper-container='testimonials']").each(function () { try { let swiperContainer = $(this); let swiperEl = swiperContainer.find(".swiper")[0]; if (!swiperEl) return; let swiper = new Swiper(swiperEl, { loop: true, speed: 600, slidesPerView: 4, centeredSlides: true, autoplay: { delay: 1500, disableOnInteraction: false, }, }); } catch (e) { console.warn("[testimonials] Swiper init error on instance:", e); } }); } catch (e) { console.warn("[testimonials] Swiper setup failed:", e); } // Intro Animation if ($(".section_tag").length) { gsap.fromTo( ".section_tag", { left: "0%" }, { left: "50%", xPercent: -50, ease: "none", scrollTrigger: { trigger: ".section_tag", start: "top bottom", end: "top 21%", scrub: true, once: false, }, } ); } if ($(".intro_bot").length) { gsap .timeline({ scrollTrigger: { trigger: ".intro_bot", start: "top bottom", end: "center center", scrub: true, once: false, }, }) .fromTo( ".intro-img_wrap", { width: "20%" }, { width: "45%", ease: "none" } ); } if ($(".intro-body_wrap").length) { gsap .timeline({ scrollTrigger: { trigger: ".intro-body_wrap", start: "top bottom", end: "top center", scrub: true, once: false, }, }) .fromTo(".intro-body_wrap", { opacity: 0 }, { opacity: 1, ease: "none" }); } // Unified Custom Cursor function initCustomTextCursor(config) { // Guard: skip entirely on touch/no-hover devices if (!window.matchMedia("(hover: hover) and (pointer: fine)").matches) return; let cursorItem = $(config.cursorSelector); let targets = $(config.targetSelector); // Guard: skip if cursor element or targets don't exist if (!cursorItem.length || !targets.length) return; let cursorParagraph = cursorItem.find("p"); let currentTarget = null; let lastText = ""; gsap.set(cursorItem[0], config.initialSet); let xTo = gsap.quickTo(cursorItem[0], "x", { ease: "power3" }); let yTo = gsap.quickTo(cursorItem[0], "y", { ease: "power3" }); const getCursorEdgeThreshold = () => cursorItem.outerWidth() + 16; $(window).on("mousemove", function (e) { let windowWidth = $(window).width(); let windowHeight = $(window).height(); let scrollY = $(window).scrollTop(); let cursorX = e.clientX; let cursorY = e.clientY + scrollY; let xPercent = config.xOffset; let yPercent = config.yOffset; if (config.adjustPosition) { let cursorEdgeThreshold = getCursorEdgeThreshold(); if (cursorX > windowWidth - cursorEdgeThreshold) { xPercent = -100; } if (cursorY > scrollY + windowHeight * 0.9) { yPercent = -120; } if (currentTarget) { let newText = currentTarget.attr(config.dataAttr); if (newText !== lastText) { cursorParagraph.html(newText); lastText = newText; } } gsap.to(cursorItem[0], { xPercent: xPercent, yPercent: yPercent, duration: 0.9, ease: "power3", }); } else { if (currentTarget) { let newText = currentTarget.attr(config.dataAttr); if (newText !== lastText) { cursorParagraph.html(newText); lastText = newText; } } } xTo(cursorX); yTo(cursorY - scrollY); }); targets.each(function () { let target = $(this); target.on("mouseenter", function () { currentTarget = target; let newText = target.attr(config.dataAttr); if (newText !== lastText) { cursorParagraph.html(newText); lastText = newText; } if (config.showOnHover) { gsap.to(cursorItem[0], { opacity: 1, scale: 1, duration: 0.3, ease: "back.out(1.7)", }); } }); if (config.hideOnLeave) { target.on("mouseleave", function () { if (currentTarget && currentTarget[0] === target[0]) { currentTarget = null; cursorParagraph.html(""); lastText = ""; gsap.to(cursorItem[0], { opacity: 0, scale: 0, duration: 0.3, ease: "power2.in", }); } }); } }); } // Initialize all cursor types initCustomTextCursor({ cursorSelector: ".cursor", targetSelector: "[data-cursor]", dataAttr: "data-cursor", xOffset: 6, yOffset: 90, adjustPosition: true, initialSet: { xPercent: 6, yPercent: 90 }, }); initCustomTextCursor({ cursorSelector: ".cursor-dark", targetSelector: "[data-cursor-dark]", dataAttr: "data-cursor-dark", xOffset: 6, yOffset: 90, adjustPosition: true, initialSet: { xPercent: 6, yPercent: 90 }, }); initCustomTextCursor({ cursorSelector: ".cursor-center", targetSelector: "[data-cursor-center]", dataAttr: "data-cursor-center", xOffset: -50, yOffset: -50, adjustPosition: false, showOnHover: true, hideOnLeave: true, initialSet: { xPercent: -50, yPercent: -50, opacity: 0, scale: 0 }, }); // ----------- Portfolio Filter ----------- // Filter Counter Function function updateItemCount() { $("[count-items=wrap]").each(function () { let countWrap = $(this); let itemCount = countWrap .find("[count-items=item][data-filter-status='active']") .filter(function () { return !$(this).closest(".is-hidden").length; }).length; countWrap .find("[count-items=total]") .text(itemCount.toString().padStart(2, "0")); }); lenis.resize(); } // Filtering with Search function initMutliFilterSetupMultiMatch() { const transitionDelay = 300; $("[data-filter-group]").each(function () { let group = $(this); let targetMatch = (group.attr("data-filter-target-match") || "multi") .trim() .toLowerCase(); let nameMatch = (group.attr("data-filter-name-match") || "single") .trim() .toLowerCase(); let buttons = group.find("[data-filter-target]"); let items = group.find("[data-filter-name]"); let searchInput = group.find("[data-filter-search='input']"); let searchTerm = ""; // Collect tokens from children if present items.each(function () { let item = $(this); let collectors = item.find("[data-filter-name-collect]"); if (!collectors.length) return; let seen = new Set(); let tokens = []; collectors.each(function () { let v = ($(this).attr("data-filter-name-collect") || "") .trim() .toLowerCase(); if (v && !seen.has(v)) { seen.add(v); tokens.push(v); } }); if (tokens.length) { item.attr("data-filter-name", tokens.join(" ")); } }); // Cache item tokens and searchable text const itemTokens = new Map(); const itemSearchText = new Map(); items.each(function () { let el = $(this); let raw = (el.attr("data-filter-name") || "").trim().toLowerCase(); let tokens = raw ? raw.split(/\s+/).filter(Boolean) : []; itemTokens.set(el[0], new Set(tokens)); let customSearchText = el.attr("data-filter-search-text"); let searchText = customSearchText ? customSearchText.toLowerCase().trim() : el.text().toLowerCase().trim(); itemSearchText.set(el[0], searchText); }); const setItemState = (el, on) => { let $el = $(el); let next = on ? "active" : "not-active"; if ($el.attr("data-filter-status") !== next) { $el.attr({ "data-filter-status": next, "aria-hidden": on ? "false" : "true", }); } }; const setButtonState = (btn, on) => { let $btn = $(btn); let next = on ? "active" : "not-active"; if ($btn.attr("data-filter-status") !== next) { $btn.attr({ "data-filter-status": next, "aria-pressed": on ? "true" : "false", }); } }; let activeTags = targetMatch === "single" ? null : new Set(["all"]); const hasRealActive = () => { if (targetMatch === "single") return activeTags !== null; return activeTags.size > 0 && !activeTags.has("all"); }; const resetAll = () => { if (targetMatch === "single") { activeTags = null; } else { activeTags.clear(); activeTags.add("all"); } }; const itemMatchesSearch = (el) => { if (!searchTerm) return true; let text = itemSearchText.get(el); return text.includes(searchTerm); }; const itemMatchesFilter = (el) => { if (!hasRealActive()) return true; let tokens = itemTokens.get(el); if (targetMatch === "single") { return tokens.has(activeTags); } else { let selected = [...activeTags]; if (nameMatch === "single") { for (let i = 0; i < selected.length; i++) { if (!tokens.has(selected[i])) return false; } return true; } else { for (let i = 0; i < selected.length; i++) { if (tokens.has(selected[i])) return true; } return false; } } }; const itemMatches = (el) => { return itemMatchesFilter(el) && itemMatchesSearch(el); }; const paint = (rawTarget) => { let target = (rawTarget || "").trim().toLowerCase(); if ((target === "all" || target === "reset") && !hasRealActive()) return; if (target === "all" || target === "reset") { resetAll(); } else if (targetMatch === "single") { activeTags = target; } else { if (activeTags.has("all")) activeTags.delete("all"); if (activeTags.has(target)) activeTags.delete(target); else activeTags.add(target); if (activeTags.size === 0) resetAll(); } items.each(function () { let el = this; let $el = $(el); if (el._ft) clearTimeout(el._ft); let next = itemMatches(el); let cur = $el.attr("data-filter-status"); if (cur === "active" && transitionDelay > 0) { $el.attr("data-filter-status", "transition-out"); el._ft = setTimeout(() => { setItemState(el, next); el._ft = null; updateItemCount(); }, transitionDelay); } else if (transitionDelay > 0) { el._ft = setTimeout(() => { setItemState(el, next); el._ft = null; updateItemCount(); }, transitionDelay); } else { setItemState(el, next); updateItemCount(); } }); buttons.each(function () { let btn = $(this); let t = (btn.attr("data-filter-target") || "").trim().toLowerCase(); let on = false; if (t === "all") on = !hasRealActive(); else if (t === "reset") on = hasRealActive(); else on = targetMatch === "single" ? activeTags === t : activeTags.has(t); setButtonState(btn[0], on); }); }; // Search input handler if (searchInput.length) { let searchTimeout; searchInput.on("input", function (e) { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { searchTerm = $(e.target).val().toLowerCase().trim(); items.each(function () { let el = this; if (el._ft) clearTimeout(el._ft); let next = itemMatches(el); setItemState(el, next); }); updateItemCount(); }, 300); }); searchInput.on("keydown", function (e) { if (e.key === "Enter" || e.keyCode === 13) { e.preventDefault(); e.stopPropagation(); return false; } }); } // Button click handler group.on("click", "[data-filter-target]", function (e) { let btn = $(this); paint(btn.attr("data-filter-target")); // Resize lenis after filter change setTimeout(function () { lenis.resize(); if (window.ScrollTrigger) { ScrollTrigger.refresh(); } }, transitionDelay + 50); }); }); } // ========== DROPDOWN HOVER FUNCTIONALITY ========== let filterCategories = $("[data-dropdown-target]"); let filterDropdowns = $("[data-dropdown-id]"); const isFilterTouchDevice = window.matchMedia("(hover: none)").matches; filterCategories.each(function () { let category = $(this); if (isFilterTouchDevice) { category.on("click", function () { let targetId = category.attr("data-dropdown-target"); let matchingDropdown = $(`[data-dropdown-id="${targetId}"]`); let isCurrentlyVisible = matchingDropdown.hasClass("is-visible"); filterDropdowns.removeClass("is-visible"); if (!isCurrentlyVisible) { matchingDropdown.addClass("is-visible"); } }); } else { category.on("mouseenter", function () { let targetId = category.attr("data-dropdown-target"); filterDropdowns.removeClass("is-visible"); let matchingDropdown = $(`[data-dropdown-id="${targetId}"]`); if (matchingDropdown.length) { matchingDropdown.addClass("is-visible"); } }); category.on("mouseleave", function () { setTimeout(() => { let targetId = category.attr("data-dropdown-target"); let matchingDropdown = $(`[data-dropdown-id="${targetId}"]`); if (matchingDropdown.length && !matchingDropdown.is(":hover")) { matchingDropdown.removeClass("is-visible"); } }, 100); }); } }); filterDropdowns.each(function () { let dropdown = $(this); if (!isFilterTouchDevice) { dropdown.on("mouseenter", function () { dropdown.addClass("is-visible"); }); dropdown.on("mouseleave", function () { dropdown.removeClass("is-visible"); }); } }); let filterDropdownWrap = $(".filter-dropdown_wrap"); if (filterDropdownWrap.length) { filterDropdownWrap.on("mouseleave", function () { filterDropdowns.removeClass("is-visible"); }); } // ========== END DROPDOWN FUNCTIONALITY ========== // ========== RESET FILTER BUTTONS ON PAGE LOAD ========== function resetFilterButtons() { $("[data-filter-target]").each(function () { let button = $(this); let target = (button.attr("data-filter-target") || "").trim().toLowerCase(); if (target === "all") { button.attr({ "data-filter-status": "active", "aria-pressed": "true", }); } else { button.attr({ "data-filter-status": "not-active", "aria-pressed": "false", }); } }); } resetFilterButtons(); // ========== END RESET ========== // Prevent form submission for search $("[data-filter-search='input']").each(function () { let input = $(this); let form = input.closest("form"); if (form.length) { form.on("submit", function (e) { e.preventDefault(); e.stopPropagation(); return false; }); form.removeAttr("action"); form[0].onsubmit = () => false; } }); // Initialize filter and counter initMutliFilterSetupMultiMatch(); updateItemCount(); // View toggle let currentView = 1; $(".view-btn").on("click", function () { $(`[data-view="${currentView}"]`).addClass("is-hidden"); currentView = currentView === 3 ? 1 : currentView + 1; $(`[data-view="${currentView}"]`).removeClass("is-hidden"); setTimeout(function () { lenis.resize(); ScrollTrigger.refresh(); }, 100); }); // Image Section Hover — desktop only (function () { // Bail before touching any styles. On mobile/tablet this script is inert, // so nothing is measured or recalculated. Mobile state lives in CSS. var isDesktop = window.matchMedia( "(min-width: 992px) and (hover: hover) and (pointer: fine)" ).matches; if (!isDesktop) return; var RADIUS = "0.625rem"; var VISIBLE = "inset(0 0 0 0 round " + RADIUS + ")"; var HIDE_LEFT = "inset(0 0 0 100% round " + RADIUS + ")"; // collapses to right edge var HIDE_RIGHT = "inset(0 100% 0 0 round " + RADIUS + ")"; // collapses to left edge var EASING = "clip-path 0.7s cubic-bezier(0.65, 0, 0.35, 1)"; /** * @param {string[]} itemSelectors - ordered left-to-right * @param {string} imageSelector - image inside each item * @param {string} containerSelector - resets to default on mouseleave * @param {number} defaultIndex - which image shows at rest */ function initImageHover( itemSelectors, imageSelector, containerSelector, defaultIndex ) { var items = itemSelectors.map(function (sel) { return $(sel); }); var hasAny = items.some(function (item) { return item.length > 0; }); if (!hasAny) return; var images = items.map(function (item) { return item.find(imageSelector); }); images.forEach(function (img) { img.css("transition", EASING); }); function setActive(activeIndex) { images.forEach(function (img, i) { if (i === activeIndex) { img.css("clip-path", VISIBLE); } else if (i < activeIndex) { img.css("clip-path", HIDE_LEFT); } else { img.css("clip-path", HIDE_RIGHT); } }); } setActive(defaultIndex); items.forEach(function (item, i) { item.on("mouseenter", function () { setActive(i); }); }); var container = $(containerSelector); if (container.length) { container.on("mouseleave", function () { setActive(defaultIndex); }); } } initImageHover( [".services-item_left", ".services-item_center", ".services-item_right"], ".services-img", ".services_layout", 1 // center ); initImageHover( [ ".bencards-item_wrap.is-1", ".bencards-item_wrap.is-2", ".bencards-item_wrap.is-3", ".bencards-item_wrap.is-4", ], ".services-img", ".bencards_layout", 0 ); initImageHover( [ ".locations-item_wrap.is-1", ".locations-item_wrap.is-2", ".locations-item_wrap.is-3", ".locations-item_wrap.is-4", ], ".contact-image", ".locations-row_layout", 0 ); })(); // Sticky Scroll Section let stickyParagraph = ".process-item_bot"; if ($(".processs-item_wrap").length) { $(".processs-item_wrap").each(function () { let itemWrap = $(this); let myParagraph = itemWrap.find(stickyParagraph); myParagraph.insertAfter(itemWrap); }); if ($(stickyParagraph).length) { gsap.set($(stickyParagraph).first(), { height: "100%" }); } if ($(".process_section").length) { let stickyTimeline = gsap.timeline({ scrollTrigger: { trigger: ".process_section", start: "top top", end: "bottom bottom", scrub: true, }, defaults: { ease: "none", }, }); $(stickyParagraph) .not(":last-child") .each(function () { let currentParagraph = $(this); let nextParagraph = currentParagraph.nextAll(stickyParagraph).eq(0); stickyTimeline.to(currentParagraph, { height: "0%" }); stickyTimeline.to(nextParagraph, { height: "100%" }, "<"); }); } } // Nav Hide & Show if ($(".navi_desktop_wrap, .mobi_component").length) { let menuHide = gsap.timeline({ paused: true }); menuHide .fromTo( ".nav_banner_wrap", { yPercent: 0 }, { yPercent: -110, duration: 0.4, ease: "sine.inOut" } ) .fromTo( ".navi_desktop_wrap", { yPercent: 0 }, { yPercent: -40, duration: 0.4, ease: "sine.inOut" }, "<" ) .fromTo( ".mobi_component", { yPercent: 0 }, { yPercent: -25, duration: 0.4, ease: "sine.inOut" }, "<" ) .fromTo( ".progress-wrap", { yPercent: 0 }, { yPercent: -40, duration: 0.4, ease: "sine.inOut" }, "<" ) .fromTo( ".contact-top_wrap.is-home", { yPercent: 0 }, { yPercent: -3, duration: 0.4, ease: "sine.inOut" } ); lenis.on("scroll", ({ scroll, direction }) => { if (direction === 1) { menuHide.play(); } else { menuHide.reverse(); } }); } // Testimonials Swiper Setup gsap.registerPlugin(CustomEase, ScrollTrigger, Draggable, InertiaPlugin); CustomEase.create("osmo-ease", "0.625, 0.05, 0, 1"); function initSliders() { const sliderWrappers = gsap.utils.toArray( document.querySelectorAll('[data-centered-slider="wrapper"]') ); sliderWrappers.forEach((sliderWrapper) => { const slides = gsap.utils.toArray( sliderWrapper.querySelectorAll('[data-centered-slider="slide"]') ); const bullets = gsap.utils.toArray( sliderWrapper.querySelectorAll('[data-centered-slider="bullet"]') ); const prevButton = sliderWrapper.querySelector( '[data-centered-slider="prev-button"]' ); const nextButton = sliderWrapper.querySelector( '[data-centered-slider="next-button"]' ); let activeElement; let activeBullet; let currentIndex = 0; let targetIndex = null; let autoplay; const isMobile = window.innerWidth <= 768; if (isMobile) { if (prevButton) prevButton.style.display = "none"; if (nextButton) nextButton.style.display = "none"; } const autoplayEnabled = sliderWrapper.getAttribute("data-slider-autoplay") === "false"; const autoplayDuration = autoplayEnabled ? parseFloat( sliderWrapper.getAttribute("data-slider-autoplay-duration") ) || 0 : 0; slides.forEach((slide, i) => { slide.setAttribute("id", `slide-${i}`); }); if (bullets && bullets.length > 0) { bullets.forEach((bullet, i) => { bullet.setAttribute("aria-controls", `slide-${i}`); bullet.setAttribute( "aria-selected", i === currentIndex ? "true" : "false" ); }); } const loop = horizontalLoop(slides, { paused: true, draggable: isMobile, center: true, onChange: (element, index) => { currentIndex = index; // Skip intermediate slides when animating to a target if (targetIndex !== null && index !== targetIndex) return; targetIndex = null; if (activeElement) activeElement.classList.remove("active"); element.classList.add("active"); activeElement = element; if (bullets && bullets.length > 0) { if (activeBullet) activeBullet.classList.remove("active"); if (bullets[index]) { bullets[index].classList.add("active"); activeBullet = bullets[index]; } bullets.forEach((bullet, i) => { bullet.setAttribute( "aria-selected", i === index ? "true" : "false" ); }); } }, }); loop.toIndex(2, { duration: 0.01 }); function startAutoplay() { if (autoplayDuration > 0 && !autoplay) { const repeat = () => { loop.next({ ease: "osmo-ease", duration: 0.725 }); autoplay = gsap.delayedCall(autoplayDuration, repeat); }; autoplay = gsap.delayedCall(autoplayDuration, repeat); } } function stopAutoplay() { if (autoplay) { autoplay.kill(); autoplay = null; } } sliderWrapper.addEventListener("mouseenter", stopAutoplay); sliderWrapper.addEventListener("mouseleave", () => { if (ScrollTrigger.isInViewport(sliderWrapper)) startAutoplay(); }); if (!isMobile) { slides.forEach((slide, i) => { slide.addEventListener("click", () => { targetIndex = i; loop.toIndex(i, { ease: "osmo-ease", duration: 0.725 }); }); }); } if (bullets && bullets.length > 0) { bullets.forEach((bullet, i) => { bullet.addEventListener("click", () => { targetIndex = i; loop.toIndex(i, { ease: "osmo-ease", duration: 0.725 }); if (activeBullet) activeBullet.classList.remove("active"); bullet.classList.add("active"); activeBullet = bullet; bullets.forEach((b, j) => { b.setAttribute("aria-selected", j === i ? "true" : "false"); }); }); }); } if (!isMobile) { if (prevButton) { prevButton.addEventListener("click", () => { let newIndex = currentIndex - 1; if (newIndex < 0) newIndex = slides.length - 1; targetIndex = newIndex; loop.toIndex(newIndex, { ease: "osmo-ease", duration: 0.725 }); }); } if (nextButton) { nextButton.addEventListener("click", () => { let newIndex = currentIndex + 1; if (newIndex >= slides.length) newIndex = 0; targetIndex = newIndex; loop.toIndex(newIndex, { ease: "osmo-ease", duration: 0.725 }); }); } } }); } document.addEventListener("DOMContentLoaded", () => { initSliders(); }); // GSAP Looping Swiper function horizontalLoop(items, config) { let timeline; items = gsap.utils.toArray(items); config = config || {}; gsap.context(() => { let onChange = config.onChange, lastIndex = 0, tl = gsap.timeline({ repeat: config.repeat, onUpdate: onChange && function () { let i = tl.closestIndex(); if (lastIndex !== i) { lastIndex = i; onChange(items[i], i); } }, paused: config.paused, defaults: { ease: "none" }, onReverseComplete: () => tl.totalTime(tl.rawTime() + tl.duration() * 100), }), length = items.length, startX = items[0].offsetLeft, times = [], widths = [], spaceBefore = [], xPercents = [], curIndex = 0, indexIsDirty = false, center = config.center, pixelsPerSecond = (config.speed || 1) * 100, snap = config.snap === false ? (v) => v : gsap.utils.snap(config.snap || 1), timeOffset = 0, container = center === true ? items[0].parentNode : gsap.utils.toArray(center)[0] || items[0].parentNode, totalWidth, getTotalWidth = () => items[length - 1].offsetLeft + (xPercents[length - 1] / 100) * widths[length - 1] - startX + spaceBefore[0] + items[length - 1].offsetWidth * gsap.getProperty(items[length - 1], "scaleX") + (parseFloat(config.paddingRight) || 0), populateWidths = () => { let b1 = container.getBoundingClientRect(), b2; items.forEach((el, i) => { widths[i] = parseFloat(gsap.getProperty(el, "width", "px")); xPercents[i] = snap( (parseFloat(gsap.getProperty(el, "x", "px")) / widths[i]) * 100 + gsap.getProperty(el, "xPercent") ); b2 = el.getBoundingClientRect(); spaceBefore[i] = b2.left - (i ? b1.right : b1.left); b1 = b2; }); gsap.set(items, { xPercent: (i) => xPercents[i], }); totalWidth = getTotalWidth(); }, timeWrap, populateOffsets = () => { timeOffset = center ? (tl.duration() * (container.offsetWidth / 2)) / totalWidth : 0; center && times.forEach((t, i) => { times[i] = timeWrap( tl.labels["label" + i] + (tl.duration() * widths[i]) / 2 / totalWidth - timeOffset ); }); }, getClosest = (values, value, wrap) => { let i = values.length, closest = 1e10, index = 0, d; while (i--) { d = Math.abs(values[i] - value); if (d > wrap / 2) { d = wrap - d; } if (d < closest) { closest = d; index = i; } } return index; }, populateTimeline = () => { let i, item, curX, distanceToStart, distanceToLoop; tl.clear(); for (i = 0; i < length; i++) { item = items[i]; curX = (xPercents[i] / 100) * widths[i]; distanceToStart = item.offsetLeft + curX - startX + spaceBefore[0]; distanceToLoop = distanceToStart + widths[i] * gsap.getProperty(item, "scaleX"); tl.to( item, { xPercent: snap(((curX - distanceToLoop) / widths[i]) * 100), duration: distanceToLoop / pixelsPerSecond, }, 0 ) .fromTo( item, { xPercent: snap( ((curX - distanceToLoop + totalWidth) / widths[i]) * 100 ), }, { xPercent: xPercents[i], duration: (curX - distanceToLoop + totalWidth - curX) / pixelsPerSecond, immediateRender: false, }, distanceToLoop / pixelsPerSecond ) .add("label" + i, distanceToStart / pixelsPerSecond); times[i] = distanceToStart / pixelsPerSecond; } timeWrap = gsap.utils.wrap(0, tl.duration()); }, refresh = (deep) => { let progress = tl.progress(); tl.progress(0, true); populateWidths(); deep && populateTimeline(); populateOffsets(); deep && tl.draggable ? tl.time(times[curIndex], true) : tl.progress(progress, true); }, onResize = () => refresh(true), proxy; gsap.set(items, { x: 0 }); populateWidths(); populateTimeline(); populateOffsets(); window.addEventListener("resize", onResize); function toIndex(index, vars) { vars = vars || {}; Math.abs(index - curIndex) > length / 2 && (index += index > curIndex ? -length : length); let newIndex = gsap.utils.wrap(0, length, index), time = times[newIndex]; if (time > tl.time() !== index > curIndex && index !== curIndex) { time += tl.duration() * (index > curIndex ? 1 : -1); } if (time < 0 || time > tl.duration()) { vars.modifiers = { time: timeWrap }; } curIndex = newIndex; vars.overwrite = true; gsap.killTweensOf(proxy); return vars.duration === 0 ? tl.time(timeWrap(time)) : tl.tweenTo(time, vars); } tl.toIndex = (index, vars) => toIndex(index, vars); tl.closestIndex = (setCurrent) => { let index = getClosest(times, tl.time(), tl.duration()); if (setCurrent) { curIndex = index; indexIsDirty = false; } return index; }; tl.current = () => (indexIsDirty ? tl.closestIndex(true) : curIndex); tl.next = (vars) => toIndex(tl.current() + 1, vars); tl.previous = (vars) => toIndex(tl.current() - 1, vars); tl.times = times; tl.progress(1, true).progress(0, true); if (config.reversed) { tl.vars.onReverseComplete(); tl.reverse(); } if (config.draggable && typeof Draggable === "function") { proxy = document.createElement("div"); let wrap = gsap.utils.wrap(0, 1), ratio, startProgress, draggable, dragSnap, lastSnap, initChangeX, wasPlaying, align = () => tl.progress( wrap(startProgress + (draggable.startX - draggable.x) * ratio) ), syncIndex = () => tl.closestIndex(true); typeof InertiaPlugin === "undefined" && console.warn( "InertiaPlugin required for momentum-based scrolling and snapping. https://greensock.com/club" ); draggable = Draggable.create(proxy, { trigger: items[0].parentNode, type: "x", onPressInit() { let x = this.x; gsap.killTweensOf(tl); wasPlaying = !tl.paused(); tl.pause(); startProgress = tl.progress(); refresh(); ratio = 1 / totalWidth; initChangeX = startProgress / -ratio - x; gsap.set(proxy, { x: startProgress / -ratio }); }, onDrag: align, onThrowUpdate: align, overshootTolerance: 0, inertia: true, snap(value) { if (Math.abs(startProgress / -ratio - this.x) < 10) { return lastSnap + initChangeX; } let time = -(value * ratio) * tl.duration(), wrappedTime = timeWrap(time), snapTime = times[getClosest(times, wrappedTime, tl.duration())], dif = snapTime - wrappedTime; Math.abs(dif) > tl.duration() / 2 && (dif += dif < 0 ? tl.duration() : -tl.duration()); lastSnap = (time + dif) / tl.duration() / -ratio; return lastSnap; }, onRelease() { syncIndex(); draggable.isThrowing && (indexIsDirty = true); }, onThrowComplete: () => { syncIndex(); wasPlaying && tl.play(); }, })[0]; tl.draggable = draggable; } tl.closestIndex(true); lastIndex = curIndex; onChange && onChange(items[curIndex], curIndex); timeline = tl; return () => window.removeEventListener("resize", onResize); }); return timeline; } document.addEventListener("DOMContentLoaded", () => { // Function to apply cursor follow effect const applyCursorFollowEffect = (parentSelector) => { const parents = document.querySelectorAll(parentSelector); parents.forEach((parent) => { const imgWrap = parent.querySelector(".swiper-img_wrap"); if (!imgWrap) return; // Track smooth movement let targetX = 0, targetY = 0; let currentX = 0, currentY = 0; let rafId; const follow = () => { currentX += (targetX - currentX) * 0.15; currentY += (targetY - currentY) * 0.15; imgWrap.style.transform = `translate(${currentX}px, ${currentY}px)`; rafId = requestAnimationFrame(follow); }; parent.addEventListener("mouseenter", () => { imgWrap.style.opacity = "1"; follow(); }); parent.addEventListener("mousemove", (e) => { const rect = parent.getBoundingClientRect(); targetX = e.clientX - rect.left; targetY = e.clientY - rect.top; }); parent.addEventListener("mouseleave", () => { cancelAnimationFrame(rafId); imgWrap.style.opacity = "0"; }); }); }; applyCursorFollowEffect(".centered-slider-slide"); applyCursorFollowEffect(".comp_item"); }); // Nav Light & Dark $(".page_wrap").each(function () { let pageEl = $(this); let darkTriggers = pageEl.find("[nav-dark]"); let lightTriggers = pageEl.find("[nav-light]"); let initialTrigger = null; let initialIsDark = false; darkTriggers.each(function () { let triggerTop = $(this).offset().top; let triggerBottom = triggerTop + $(this).outerHeight(); if (triggerTop <= 0 && triggerBottom > 0) { initialTrigger = $(this); initialIsDark = true; return false; } }); if (!initialTrigger) { lightTriggers.each(function () { let triggerTop = $(this).offset().top; let triggerBottom = triggerTop + $(this).outerHeight(); if (triggerTop <= 0 && triggerBottom > 0) { initialTrigger = $(this); initialIsDark = false; return false; } }); } if (initialIsDark) { pageEl.addClass("dark"); } else { pageEl.removeClass("dark"); } darkTriggers.each(function () { ScrollTrigger.create({ trigger: $(this), start: "top top", end: "bottom top", onEnter: () => { pageEl.addClass("dark"); }, onEnterBack: () => { pageEl.addClass("dark"); }, }); }); lightTriggers.each(function () { ScrollTrigger.create({ trigger: $(this), start: "top top", end: "bottom top", onEnter: () => { pageEl.removeClass("dark"); }, onEnterBack: () => { pageEl.removeClass("dark"); }, }); }); }); // Parallax Effects $("[data-parallax]").each(function () { let parallaxEl = $(this); let speed = parallaxEl.attr("data-parallax") || 15; let parallaxTimeline = gsap.timeline({ scrollTrigger: { trigger: parallaxEl, start: "top bottom", end: "bottom top", scrub: true, }, }); parallaxTimeline.fromTo( parallaxEl, { yPercent: +speed }, { yPercent: -speed, ease: "linear" } ); }); // Image Parallax $("[img-scroll]").each(function () { let parallaxImgWrap = $(this); let parallaxImg = parallaxImgWrap.find("img"); const imgScrollTimeline = gsap.timeline({ scrollTrigger: { trigger: $(this), start: "top bottom", end: "bottom top", scrub: true, }, }); imgScrollTimeline.fromTo( parallaxImg, { yPercent: -10 }, { yPercent: 10, ease: "linear" } ); }); // Progress Tracker Setup // === Shared Setup Function === function initProgressTracker(options) { let { wrap, circle, svg, scrollContainer } = options; if (!wrap.length || !circle.length) return; const radius = 20.5; const circumference = 2 * Math.PI * radius; svg.css("transform", "rotate(-90deg)"); circle.css({ strokeDasharray: circumference, strokeDashoffset: circumference, }); wrap.css({ opacity: "1", pointerEvents: "auto", }); function updateProgress() { let progress; if (scrollContainer) { let el = scrollContainer[0]; let scrollTop = el.scrollTop; let scrollHeight = el.scrollHeight - el.clientHeight; progress = scrollHeight > 0 ? Math.max(0, Math.min(1, scrollTop / scrollHeight)) : 0; } else { let scrollTop = $(window).scrollTop(); let docHeight = $(document).height() - $(window).height(); progress = docHeight > 0 ? Math.max(0, Math.min(1, scrollTop / docHeight)) : 0; } let offset = circumference - progress * circumference; circle.css("strokeDashoffset", offset); } if (scrollContainer) { scrollContainer[0].addEventListener("scroll", updateProgress, { passive: true, }); } else { $(window).on("scroll", updateProgress); } updateProgress(); } // === 1. Page Progress Tracker === initProgressTracker({ wrap: $(".progress-wrap").not(".is-contact").not(".is-portfolio"), circle: $( ".progress-wrap:not(.is-contact):not(.is-portfolio) .progress-tracker_icon circle" ), svg: $( ".progress-wrap:not(.is-contact):not(.is-portfolio) .progress-tracker_icon" ), scrollContainer: null, }); // === 2. contact Progress Tracker (.contact-wrap scroll) === initProgressTracker({ wrap: $(".progress-wrap.is-contact"), circle: $(".progress-wrap.is-contact .progress-tracker_icon circle"), svg: $(".progress-wrap.is-contact .progress-tracker_icon"), scrollContainer: $(".contact-top_wrap"), }); // === 3. Portfolio 41. Progress Tracker (.success-top_wrap scroll) === initProgressTracker({ wrap: $(".progress-wrap.is-portfolio"), circle: $(".progress-wrap.is-portfolio .progress-tracker_icon circle"), svg: $(".progress-wrap.is-portfolio .progress-tracker_icon"), scrollContainer: $(".success-top_wrap"), }); // === 4. Home AQ Modal Progress Tracker (.contact-wrap.is-home scroll) === initProgressTracker({ wrap: $(".progress-wrap.is-home"), circle: $(".progress-wrap.is-home .progress-tracker_icon circle"), svg: $(".progress-wrap.is-home .progress-tracker_icon"), scrollContainer: $(".contact-top_wrap.is-home"), }); // Full Image ScrollTrigger if ($(".full-img_wrap").length) { gsap .timeline({ scrollTrigger: { trigger: ".full-img_wrap", start: "top bottom", end: "top top", scrub: true, once: false, onComplete: () => { lenis.resize(); ScrollTrigger.refresh(); }, }, }) .fromTo( ".full-img_wrap", { width: "70vw", borderRadius: "1.25rem", }, { width: "100vw", borderRadius: "0px", ease: "none", } ); } // Full Image ScrollTrigger $("[data-full-img='true']").each(function () { const wrap = this; gsap .timeline({ scrollTrigger: { trigger: wrap, start: "top 15%", end: "bottom 20%", scrub: true, once: false, onComplete: () => { lenis.resize(); ScrollTrigger.refresh(); }, }, }) .fromTo( wrap, { width: "97vw", }, { width: "100vw", // borderRadius: 0, ease: "none", } ); }); // Footer Scroll Animation if ($(".footer_wrap").length && $(".footer-cta_wrap").length) { const footerTimeline = gsap.timeline({ scrollTrigger: { trigger: ".footer_wrap", start: "top top", endTrigger: ".footer-cta_wrap", end: "top top", scrub: true, markers: false, once: false, }, }); footerTimeline .to( ".footer-cta_header_wrap", { y: -3, ease: "none", }, 0 ) .to( ".footer-bg_block_front", { height: 0, ease: "none", }, 0 ) .to( ".footer-bg_block_back", { height: 0, ease: "none", }, 0 ); if ($(".text-white").length) { footerTimeline.to( ".text-white", { clipPath: "inset(0% 0% 0% 0%)", ease: "none", }, 0 ); } } function animateCardsIn(cardSelector, triggerSelector) { if (!$(cardSelector).length || !$(triggerSelector).length) return; gsap.set(cardSelector, { opacity: 0, y: 100, }); gsap.to(cardSelector, { opacity: 1, y: 0, duration: 0.8, stagger: 0.2, ease: "power2.out", scrollTrigger: { trigger: triggerSelector, start: "top bottom", toggleActions: "play none none none", }, }); } $("[data-cards-trigger]").each(function () { const trigger = $(this); const id = trigger.attr("data-cards-trigger"); const triggerSelector = "[data-cards-trigger='" + id + "']"; const cardSelector = "[data-cards-item='" + id + "']"; animateCardsIn(cardSelector, triggerSelector); }); if ($(".nav_dropdown_component").length) { $(".nav_dropdown_component").hover( function () { if ($(window).width() > 991) { $(".nav_dropdown_backdrop, .nav_menu_backdrop").css("opacity", "1"); } }, function () { if ($(window).width() > 991) { $(".nav_dropdown_backdrop, .nav_menu_backdrop").css("opacity", ""); } } ); } // Featured Blog Posts functionality $(document).ready(function () { if (window.innerWidth <= 767) return; $(".featured-item").each(function () { var $toggleElement = $(this).find("[data-featured-toggle]"); var toggleText = $toggleElement.text().trim().toLowerCase(); if (toggleText === "true") { $(this).addClass("is-featured"); $(this).find(".featured-post_img_wrap").addClass("is-featured"); } }); }); // Cursor Mask $(".circ_section").each(function () { const $container = $(this); const $cursorMask = $container.find(".cursor-mask"); let mouseX = $container.width() / 2; let mouseY = $container.height() / 2; let animatedMouse = { x: mouseX, y: mouseY }; function updateMask(x, y) { const mask = `radial-gradient(circle 400px at ${x}px ${y}px, rgba(0, 59, 255, 0.8) 0%, rgba(0, 59, 255, 0.8) 40%, transparent 100%)`; $cursorMask.css({ background: mask, }); } updateMask(mouseX, mouseY); $container.on("mousemove", function (e) { const rect = this.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; gsap.to(animatedMouse, { x: x, y: y, duration: 0.4, ease: "power2.out", onUpdate: function () { updateMask(animatedMouse.x, animatedMouse.y); }, }); gsap.to($cursorMask, { opacity: 1, duration: 0.3, ease: "power2.out", }); }); $container.on("mouseleave", function () { gsap.to($cursorMask, { opacity: 0, duration: 0.3, ease: "power2.out", }); }); }); // Blob Lottie ScrollTrigger if ($(".hero-home_wrap").length) { gsap.registerPlugin(ScrollTrigger); const $lottieEl = $(".blob_lottie"); const lottieAnim = lottie.loadAnimation({ container: $lottieEl[0], renderer: "svg", loop: false, autoplay: false, path: "https://cdn.prod.website-files.com/68da56fe116a50cd209ccdc5/69c3ace9aca48db2f312a7ed_blobv2.json", }); const lottieScrub = { frame: 0 }; lottieAnim.addEventListener("DOMLoaded", function () { const totalFrames = lottieAnim.totalFrames; const tl = gsap.timeline({ scrollTrigger: { trigger: ".hero-home_wrap", start: "top top", end: "bottom top", scrub: true, }, }); tl.to(lottieScrub, { frame: totalFrames - 1, duration: 1, ease: "none", onUpdate: function () { lottieAnim.goToAndStop(lottieScrub.frame, true); }, }); }); } // How it works scroll timeline if ( $( ".step-item_right.is-1, .step-item_right.is-2, .step-item_right.is-3, .step-item_right.is-4" ).length ) { gsap.set( ".step-item_right.is-1, .step-item_right.is-2, .step-item_right.is-3, .step-item_right.is-4", { clipPath: "inset(0% 0% 0% 0%)", } ); const $h3 = $(".step_h3"); if ($h3.length) { $h3.text("01"); } let currentNumber = 1; function tickerNumber(newNumber) { if (currentNumber === newNumber) return; if (!$h3.length) return; const direction = newNumber > currentNumber ? -20 : 20; gsap.to($h3, { y: direction, opacity: 0, duration: 0.2, ease: "power2.in", onComplete: () => { $h3.text("0" + newNumber); gsap.fromTo( $h3, { y: -direction, opacity: 0 }, { y: 0, opacity: 1, duration: 0.2, ease: "power2.out" } ); }, }); currentNumber = newNumber; } if ($(".item_wrap.is-2").length) { ScrollTrigger.create({ trigger: ".item_wrap.is-2", start: "top bottom-=40px", end: "bottom bottom-=40px", scrub: true, onUpdate: (self) => { gsap.set(".step-item_right.is-1", { clipPath: "inset(0% 0% " + self.progress * 100 + "% 0%)", }); tickerNumber(self.progress > 0 ? 2 : 1); }, }); } if ($(".item_wrap.is-3").length) { ScrollTrigger.create({ trigger: ".item_wrap.is-3", start: "top bottom-=40px", end: "bottom bottom-=40px", scrub: true, onUpdate: (self) => { gsap.set(".step-item_right.is-2", { clipPath: "inset(0% 0% " + self.progress * 100 + "% 0%)", }); tickerNumber(self.progress > 0 ? 3 : 2); }, }); } if ($(".item_wrap.is-4").length) { ScrollTrigger.create({ trigger: ".item_wrap.is-4", start: "top bottom-=40px", end: "bottom bottom-=40px", scrub: true, onUpdate: (self) => { gsap.set(".step-item_right.is-3", { clipPath: "inset(0% 0% " + self.progress * 100 + "% 0%)", }); tickerNumber(self.progress > 0 ? 4 : 3); }, }); } } // ====== Locale-aware "Close" label ====== // Detects locale from the URL path (same pattern as the locale switcher) // and exposes the correctly translated "Close" text for all modal/menu // animations below. const modalCloseLabel = (function () { const match = window.location.pathname.match(/^\/(fr|de)/); const currentLocale = match ? match[1] : "en"; const closeLabels = { en: "Close", fr: "Fermer", de: "Schliessen", }; return closeLabels[currentLocale] || closeLabels.en; })(); // ====== Locale-aware "Enquire Now" label ====== const modalEnquireLabel = (function () { const match = window.location.pathname.match(/^\/(fr|de)/); const currentLocale = match ? match[1] : "en"; // NOTE: pick the exact wording you'd prefer here — these are reasonable // defaults, easy to swap for your preferred phrasing. const enquireLabels = { en: "Enquire Now", fr: "Contactez-nous", de: "Jetzt anfragen", }; return enquireLabels[currentLocale] || enquireLabels.en; })(); // Contact Animation $(document).ready(function () { if (!$(".nav_links_contact").length) return; let contactTimeline; let isContactMenuOpen = false; gsap.set(".progress-wrap.is-contact", { display: "none", opacity: 0, pointerEvents: "none", }); $(".nav_links_contact").each(function () { $(this).on("click", function (e) { e.preventDefault(); if (window.isAqModalOpen || window.isPortfolioModalOpen) return; if (isContactMenuOpen) { closeContactMenu(); return; } isContactMenuOpen = true; lenis.stop(); contactTimeline = gsap.timeline(); contactTimeline.set([".nav_dropdown_backdrop", ".nav_menu_backdrop"], { pointerEvents: "auto", }); contactTimeline.to([".nav_dropdown_backdrop", ".nav_menu_backdrop"], { opacity: 1, duration: 0.4, ease: "power2.out", }); contactTimeline.to( [".nav_links_wrap:not(.is-contact)", ".nav_banner_wrap"], { opacity: 0, duration: 0.4, ease: "power2.out", onComplete: function () { $(".nav_links_wrap").not(".is-contact").css({ visibility: "hidden", pointerEvents: "none", }); $(".nav_banner_wrap").css({ visibility: "hidden", pointerEvents: "none", }); }, }, "<" ); contactTimeline.to( ".nav_links_text.is-contact", { duration: 0.2, onStart: function () { $(".nav_links_text.is-contact").text(modalCloseLabel); }, }, "-=0.3" ); contactTimeline.to( ".contact-wrap.is-contact", { x: "0%", duration: 0.6, ease: "power3.out", }, "-=0.2" ); contactTimeline.to( ".progress-wrap.is-contact", { display: "flex", opacity: 1, duration: 0.4, ease: "power2.out", onComplete: function () { gsap.set(".progress-wrap.is-contact", { pointerEvents: "auto", cursor: "pointer", }); }, }, "-=0.4" ); }); }); function closeContactMenu() { if (!isContactMenuOpen) return; isContactMenuOpen = false; const closeTimeline = gsap.timeline({ onComplete: function () { $(".nav_links_wrap").not(".is-contact").css({ visibility: "", pointerEvents: "", }); $(".nav_banner_wrap").css({ visibility: "", pointerEvents: "", }); if ($(".contact-wrap.is-home").length) { $(".contact-wrap.is-home").css("overflow-y", ""); } gsap.set(".progress-wrap.is-contact", { display: "none", opacity: 0, pointerEvents: "none", cursor: "", }); lenis.start(); }, }); closeTimeline.to(".progress-wrap.is-contact", { opacity: 0, duration: 0.3, ease: "power2.in", onStart: function () { gsap.set(".progress-wrap.is-contact", { pointerEvents: "none", cursor: "", }); }, onComplete: function () { gsap.set(".progress-wrap.is-contact", { display: "none" }); }, }); closeTimeline.to( ".contact-wrap.is-contact", { x: "130%", duration: 0.6, ease: "power3.in", }, "-=0.2" ); closeTimeline.to( ".nav_links_text.is-contact", { duration: 0.2, onStart: function () { $(".nav_links_text.is-contact").text(modalEnquireLabel); }, }, "-=0.5" ); closeTimeline.to( [".nav_dropdown_backdrop", ".nav_menu_backdrop"], { opacity: 0, duration: 0.4, ease: "power2.in", }, "-=0.4" ); closeTimeline.to( [".nav_links_wrap:not(.is-contact)", ".nav_banner_wrap"], { opacity: 1, duration: 0.4, ease: "power2.in", onStart: function () { $(".nav_links_wrap").not(".is-contact").css({ visibility: "", pointerEvents: "", }); $(".nav_banner_wrap").css({ visibility: "", pointerEvents: "", }); }, }, "<" ); closeTimeline.set([".nav_dropdown_backdrop", ".nav_menu_backdrop"], { pointerEvents: "none", }); } $(document).on("click", ".nav_menu_backdrop", function (e) { e.preventDefault(); closeContactMenu(); }); $(document).on("click", ".progress-wrap.is-contact", function () { closeContactMenu(); }); $(document).on("click", ".popup-close_btn", function (e) { e.preventDefault(); if (isContactMenuOpen) closeContactMenu(); }); }); // Portfolio Modal Animation $(document).ready(function () { if (!$(".portfolio-btn").length) return; let portfolioTimeline; window.isPortfolioModalOpen = false; $(".portfolio-btn").each(function () { $(this).on("click", function (e) { e.preventDefault(); e.stopPropagation(); if (window.isPortfolioModalOpen) { closePortfolioModal(); return; } isPortfolioModalOpen = true; lenis.stop(); portfolioTimeline = gsap.timeline(); portfolioTimeline.set([".nav_dropdown_backdrop", ".nav_menu_backdrop"], { opacity: 0, pointerEvents: "none", }); portfolioTimeline.set([".aq-blur", ".aq-backdrop"], { pointerEvents: "auto", }); portfolioTimeline.to([".aq-blur", ".aq-backdrop"], { opacity: 1, duration: 0.4, ease: "power2.out", }); portfolioTimeline.to( [".nav_links_wrap:not(.is-contact)", ".nav_banner_wrap"], { opacity: 0, duration: 0.4, ease: "power2.out", onComplete: function () { $(".nav_links_wrap").not(".is-contact").css({ visibility: "hidden", pointerEvents: "none", }); $(".nav_banner_wrap").css({ visibility: "hidden", pointerEvents: "none", }); }, }, "<" ); portfolioTimeline.to( ".nav_links_text.is-contact", { duration: 0.2, onStart: function () { $(".nav_links_text.is-contact").text(modalCloseLabel); }, }, "-=0.3" ); portfolioTimeline.to( ".contact-wrap.is-home", { x: "0%", duration: 0.6, ease: "power3.out", }, "-=0.2" ); }); }); function closePortfolioModal() { if (!window.isPortfolioModalOpen) return; window.isPortfolioModalOpen = false; const closeTimeline = gsap.timeline({ onComplete: function () { $(".nav_links_wrap").not(".is-contact").css({ visibility: "", pointerEvents: "", }); $(".nav_banner_wrap").css({ visibility: "", pointerEvents: "", }); if ($(".contact-wrap.is-home").length) { $(".contact-wrap.is-home").css("overflow-y", ""); } lenis.start(); }, }); closeTimeline.to(".contact-wrap.is-home", { x: "130%", duration: 0.6, ease: "power3.in", }); closeTimeline.to( ".nav_links_text.is-contact", { duration: 0.2, onStart: function () { $(".nav_links_text.is-contact").text(modalEnquireLabel); }, }, "-=0.5" ); closeTimeline.to( [".aq-blur", ".aq-backdrop"], { opacity: 0, duration: 0.4, ease: "power2.in", onStart: function () { $(".aq-blur, .aq-backdrop").css("pointerEvents", "none"); }, onComplete: function () { gsap.set([".nav_dropdown_backdrop", ".nav_menu_backdrop"], { opacity: 0, pointerEvents: "none", }); }, }, "-=0.4" ); closeTimeline.to( [".nav_links_wrap:not(.is-contact)", ".nav_banner_wrap"], { opacity: 1, duration: 0.4, ease: "power2.in", onStart: function () { $(".nav_links_wrap").not(".is-contact").css({ visibility: "", pointerEvents: "", }); $(".nav_banner_wrap").css({ visibility: "", pointerEvents: "", }); }, }, "<" ); } $(document).on("click", ".nav_links_contact.is-enquire", function (e) { e.preventDefault(); if (window.isPortfolioModalOpen) closePortfolioModal(); }); $(document).on("click", ".aq-blur, .aq-backdrop", function () { closePortfolioModal(); }); $(document).on("click", ".popup-close_btn", function (e) { e.preventDefault(); if (window.isPortfolioModalOpen) closePortfolioModal(); }); }); // AQ Modal Animation $(document).ready(function () { if (!$(".aq-modal_trigger, .aq_link").length) return; let aqTimeline; window.isAqModalOpen = false; $(".aq-modal_trigger, .aq_link").each(function () { $(this).on("click", function (e) { e.preventDefault(); e.stopPropagation(); if (window.isAqModalOpen) { closeAqModal(); return; } isAqModalOpen = true; lenis.stop(); aqTimeline = gsap.timeline(); aqTimeline.set([".nav_dropdown_backdrop", ".nav_menu_backdrop"], { opacity: 0, pointerEvents: "none", }); aqTimeline.set([".aq-blur", ".aq-backdrop"], { pointerEvents: "auto", }); aqTimeline.to([".aq-blur", ".aq-backdrop"], { opacity: 1, duration: 0.4, ease: "power2.out", }); aqTimeline.to( [".nav_links_wrap:not(.is-contact)", ".nav_banner_wrap"], { opacity: 0, duration: 0.4, ease: "power2.out", onComplete: function () { $(".nav_links_wrap").not(".is-contact").css({ visibility: "hidden", pointerEvents: "none", }); $(".nav_banner_wrap").css({ visibility: "hidden", pointerEvents: "none", }); }, }, "<" ); aqTimeline.to( ".nav_links_text.is-contact", { duration: 0.2, onStart: function () { $(".nav_links_text.is-contact").text(modalCloseLabel); }, }, "-=0.3" ); aqTimeline.to( ".contact-wrap.is-home", { x: "0%", duration: 0.6, ease: "power3.out", }, "-=0.2" ); }); }); function closeAqModal() { if (!window.isAqModalOpen) return; window.isAqModalOpen = false; const closeTimeline = gsap.timeline({ onComplete: function () { $(".nav_links_wrap").not(".is-contact").css({ visibility: "", pointerEvents: "", }); $(".nav_banner_wrap").css({ visibility: "", pointerEvents: "", }); if ($(".contact-wrap.is-home").length) { $(".contact-wrap.is-home").css("overflow-y", ""); } lenis.start(); }, }); closeTimeline.to(".contact-wrap.is-home", { x: "130%", duration: 0.6, ease: "power3.in", }); closeTimeline.to( ".nav_links_text.is-contact", { duration: 0.2, onStart: function () { $(".nav_links_text.is-contact").text(modalEnquireLabel); }, }, "-=0.5" ); closeTimeline.to( [".aq-blur", ".aq-backdrop"], { opacity: 0, duration: 0.4, ease: "power2.in", onStart: function () { $(".aq-blur, .aq-backdrop").css("pointerEvents", "none"); }, onComplete: function () { gsap.set([".nav_dropdown_backdrop", ".nav_menu_backdrop"], { opacity: 0, pointerEvents: "none", }); }, }, "-=0.4" ); closeTimeline.to( [".nav_links_wrap:not(.is-contact)", ".nav_banner_wrap"], { opacity: 1, duration: 0.4, ease: "power2.in", onStart: function () { $(".nav_links_wrap").not(".is-contact").css({ visibility: "", pointerEvents: "", }); $(".nav_banner_wrap").css({ visibility: "", pointerEvents: "", }); }, }, "<" ); } $(document).on("click", ".nav_links_contact.is-enquire", function (e) { e.preventDefault(); if (window.isAqModalOpen) closeAqModal(); }); $(document).on("click", ".aq-blur, .aq-backdrop", function () { closeAqModal(); }); $(document).on("click", ".popup-close_btn", function (e) { e.preventDefault(); if (window.isAqModalOpen) closeAqModal(); }); }); // Hide Nav when approaching footer $(document).ready(function () { const $nav = $( ".nav_links_wrap, .nav_banner_wrap, .mobi_component, .nav_desktop_logo" ); const $footer = $(".global_footer_section"); if (!$nav.length || !$footer.length) return; const navHeight = $nav.outerHeight(); gsap.set($nav, { y: 0 }); $(window).on("scroll", function () { const footerTop = $footer.offset().top; const scrollTop = $(window).scrollTop(); const windowHeight = $(window).height(); const distanceToFooter = footerTop - (scrollTop + windowHeight); const startOffset = 150; const triggerDistance = 200; const progress = Math.min( Math.max((startOffset - distanceToFooter) / triggerDistance, 0), 1 ); gsap.to($nav, { y: -(navHeight + 60) * progress, duration: 0.1, ease: "none", }); }); }); // Competitors scroll animation - about page (function () { const items = gsap.utils.toArray(".comp_item"); const images = gsap.utils.toArray(".sticky-comp_item"); if (!items.length || !images.length) return; // Stack images: first on top, last on bottom images.forEach((img, i) => { gsap.set(img, { zIndex: images.length - i }); }); // Clip each image away as you scroll to the next logo items.forEach((item, i) => { if (i === 0) return; // first item = default state, no trigger needed ScrollTrigger.create({ trigger: item, start: "top center", end: "bottom center", scrub: true, onUpdate: (self) => { gsap.set(images[i - 1], { clipPath: `inset(0% 0% ${self.progress * 100}% 0%)`, }); }, }); }); })(); // Competitor logos fade in/out as they pass through center (function () { const logos = gsap.utils.toArray(".comp-logo_layout"); const sticky = document.querySelector(".sticky-comp_list"); if (!logos.length || !sticky) return; // Only run on mobile (portrait and landscape) const mm = gsap.matchMedia(); mm.add("(max-width: 767px)", () => { logos.forEach((logo) => { // Fade in: from bottom of sticky to center of sticky gsap.fromTo( logo, { opacity: 0.12 }, { opacity: 1, scrollTrigger: { trigger: logo, start: "top bottom", end: "center center", scrub: true, }, } ); // Fade out: from center of sticky to top of sticky gsap.fromTo( logo, { opacity: 1 }, { opacity: 0.12, scrollTrigger: { trigger: logo, start: "center center", end: "bottom top", scrub: true, }, } ); }); }); })(); // Air Quality Indicator $(function () { const svgEl = document.querySelector(".aq-indicator_lvl"); const scoreEl = document.getElementById("air-score"); const categoryEl = document.getElementById("air-category"); const sentenceEl = document.getElementById("air-sentence"); const indicator = document.querySelector(".aq_indicator"); const infoWrap = document.querySelector(".aq-indicator_info_wrap"); const temporary = document.querySelector(".aq-indicator_temporary"); if (!svgEl || !indicator || !infoWrap || !temporary) return; // === Locale detection (same pattern as elsewhere on the site) === const localeMatch = window.location.pathname.match(/^\/(fr|de)/); const currentLocale = localeMatch ? localeMatch[1] : "en"; // Category heading text, per locale const categoryLabels = { en: { healthy: "Healthy Air Quality", moderate: "Moderate Air Quality" }, de: { healthy: "Gute Luftqualität", moderate: "Mässige Luftqualität" }, fr: { healthy: "Bonne qualité de l'air", moderate: "Qualité de l'air modérée", }, }; // Sentence builders, per locale (word order differs per language) const sentenceBuilders = { en: (score, region) => "Your air quality is " + score + "% in " + region, de: (score, region) => "Die Luftqualität in " + region + " liegt bei " + score + "%.", fr: (score, region) => "La qualité de l'air à " + region + " est de " + score + "%.", }; // === Build a clean circle element, ignoring the broken Figma path === const radius = 34; const cx = 49; const cy = 49; const circumference = 2 * Math.PI * radius; // If a background "track" circle already exists in the markup, match its // exact geometry so our overlay arc lines up pixel-perfectly with it — // a radius/stroke-width mismatch here is a common cause of a visible // hairline seam at the edges. const existingCircle = svgEl.querySelector("circle"); const trackStrokeWidth = existingCircle ? existingCircle.getAttribute("stroke-width") : svgEl.querySelector("path") ? svgEl.querySelector("path").getAttribute("stroke-width") || "2" : "2"; const trackRadius = existingCircle ? parseFloat(existingCircle.getAttribute("r")) || radius : radius; const trackCx = existingCircle ? parseFloat(existingCircle.getAttribute("cx")) || cx : cx; const trackCy = existingCircle ? parseFloat(existingCircle.getAttribute("cy")) || cy : cy; const circle = document.createElementNS( "http://www.w3.org/2000/svg", "circle" ); circle.setAttribute("cx", trackCx); circle.setAttribute("cy", trackCy); circle.setAttribute("r", trackRadius); circle.setAttribute("fill", "none"); // Slightly thicker than the track so the overlay's edge overlaps it by a // fraction of a pixel instead of butting up exactly against it — this // absorbs the anti-aliasing seam that shows as a hairline of the // background color around the arc. circle.setAttribute("stroke-width", parseFloat(trackStrokeWidth) + 0.75); // Forces sharper, more precise SVG rendering (less browser anti-aliasing // blur at the stroke edges). circle.setAttribute("shape-rendering", "geometricPrecision"); circle.style.strokeDasharray = circumference; circle.style.strokeDashoffset = circumference; circle.style.transition = "stroke-dashoffset 1.2s ease, stroke 0.5s ease"; circle.style.transformOrigin = "center"; circle.style.transform = "rotate(-90deg)"; // Hide the original path and inject our circle svgEl.querySelectorAll("path").forEach((p) => (p.style.display = "none")); svgEl.appendChild(circle); function fadeIn(el) { el.style.transition = "opacity 0.6s ease"; el.style.opacity = "1"; } function fadeOut(el) { el.style.transition = "opacity 0.6s ease"; el.style.opacity = "0"; } // Two tiers only now: 75%+ is "healthy" (green), below that is "moderate" (blue) function getCategory(score) { if (score >= 75) return { key: "healthy", color: "#35EC72" }; return { key: "moderate", color: "#003BFF" }; } function updateWidget(score, regionName) { const { key, color } = getCategory(score); const offset = circumference - (score / 100) * circumference; circle.style.strokeDashoffset = offset; circle.style.stroke = color; if (scoreEl) scoreEl.textContent = score + "%"; if (categoryEl) categoryEl.textContent = categoryLabels[currentLocale][key]; if (sentenceEl) sentenceEl.textContent = sentenceBuilders[currentLocale]( score, regionName ); fadeOut(temporary); setTimeout(() => { fadeIn(indicator); fadeIn(infoWrap); }, 400); } function getLocationName(lat, lng) { return fetch( "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lng + "&format=json" ) .then((r) => r.json()) .then((data) => { const city = data.address.city || data.address.town || data.address.village || data.address.county || "your area"; const country = data.address.country_code ? data.address.country_code.toUpperCase() : ""; return city + (country ? ", " + country : ""); }) .catch(() => "your area"); } if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( async function (position) { const lat = position.coords.latitude; const lng = position.coords.longitude; try { const [airRes, locationName] = await Promise.all([ fetch( "https://air-quality-api.vercel.app/api/air-quality?lat=" + lat + "&lng=" + lng ).then((r) => r.json()), getLocationName(lat, lng), ]); updateWidget(airRes.score, locationName); } catch (e) { console.error("Air quality fetch failed", e); } }, function (err) { console.warn("Geolocation denied", err); } ); } }); // Mobile menu $(function () { $(".nav_dropdown_item.is-green").on("click", function () { $(".nav_dropdown_main_content").css("transform", "translateX(-51%)"); }); $(".back-link").on("click", function () { $(".nav_dropdown_main_content").css("transform", "translateX(0%)"); }); }); gsap.registerPlugin(ScrollTrigger); ScrollTrigger.config({ ignoreMobileResize: true, }); // Circular Infographic ScrollTrigger $(".circ_graphic_wrap").each(function () { const $wrap = $(this); const $lottieEl = $wrap.find(".circ_graphic_item"); const $state1 = $wrap.find(".circ-item_layout.is-1"); const $state2 = $wrap.find(".circ-item_layout.is-2"); const $state3 = $wrap.find(".circ-item_layout.is-3"); const lottieScrub = { frame: 0 }; // Create the timeline + ScrollTrigger IMMEDIATELY, in DOM order. // Start/end/pin don't depend on the Lottie file, so there's no reason // to wait — creating pinned triggers async causes stale positions. const tl = gsap.timeline({ scrollTrigger: { trigger: $wrap[0], start: "center center", end: "+=100%", pin: true, pinType: "transform", scrub: true, anticipatePin: 1, invalidateOnRefresh: true, }, }); // State fades don't depend on Lottie either — add them now tl.to($state1, { opacity: 0, duration: 0.05, ease: "none" }, 0.25); tl.to($state2, { opacity: 1, duration: 0.05, ease: "none" }, 0.3); tl.to($state2, { opacity: 0, duration: 0.05, ease: "none" }, 0.66); tl.to($state3, { opacity: 1, duration: 0.05, ease: "none" }, 0.71); const lottieAnim = lottie.loadAnimation({ container: $lottieEl[0], renderer: "svg", loop: false, autoplay: false, path: $lottieEl.attr("data-lottie-url"), }); lottieAnim.addEventListener("DOMLoaded", function () { // Render frame 0 immediately so it's never invisible lottieAnim.goToAndStop(0, true); // Slot the frame scrub in at position 0 alongside the fades tl.to( lottieScrub, { frame: lottieAnim.totalFrames - 1, duration: 1, ease: "none", onUpdate: function () { lottieAnim.goToAndStop(lottieScrub.frame, true); }, }, 0 ); // Recalculate positions now that content is fully loaded/rendered ScrollTrigger.refresh(); }); }); // Nav Logo lottie $(".logo_lot").each(function () { const $lottieEl = $(this); const lottieAnim = lottie.loadAnimation({ container: $lottieEl[0], renderer: "svg", loop: false, autoplay: false, path: $lottieEl.attr("data-lottie-url"), }); lottieAnim.addEventListener("DOMLoaded", function () { const totalFrames = lottieAnim.totalFrames; let currentFrame = 0; let lastScrollY = window.scrollY; let isScrolling = false; let scrollTimeout = null; function wrap(frame, total) { return ((frame % total) + total) % total; } function onScroll() { const currentScrollY = window.scrollY; const delta = currentScrollY - lastScrollY; lastScrollY = currentScrollY; currentFrame = wrap(currentFrame + delta * 0.3, totalFrames); lottieAnim.goToAndStop(currentFrame, true); clearTimeout(scrollTimeout); scrollTimeout = setTimeout(function () { isScrolling = false; }, 150); isScrolling = true; } window.addEventListener("scroll", onScroll, { passive: true }); }); }); // CTA Blob Lottie Animation const mm = gsap.matchMedia(); mm.add("(min-width: 1400px)", () => { const instances = []; $(".cta-lottie_top, .cta-lottie_bot").each(function () { const $lottieEl = $(this); const lottieAnim = lottie.loadAnimation({ container: $lottieEl[0], renderer: "svg", loop: false, autoplay: false, path: $lottieEl.attr("data-lottie-url"), }); const lottieScrub = { frame: 0 }; let scrollTriggerInstance = null; lottieAnim.addEventListener("DOMLoaded", function () { const totalFrames = lottieAnim.totalFrames; lottieAnim.goToAndStop(0, true); const tl = gsap.timeline({ scrollTrigger: { trigger: $lottieEl[0], start: "top bottom", end: "bottom top", scrub: true, }, }); tl.to(lottieScrub, { frame: totalFrames - 1, ease: "none", onUpdate: function () { lottieAnim.goToAndStop(lottieScrub.frame, true); }, }); scrollTriggerInstance = tl.scrollTrigger; }); instances.push({ lottieAnim, getScrollTrigger: () => scrollTriggerInstance, }); }); return () => { instances.forEach((instance) => { instance.lottieAnim.destroy(); const st = instance.getScrollTrigger(); if (st) st.kill(); }); }; }); // AQ Modal Animation // $(".aq-modal_trigger, .portfolio-btn").each(function () { // if (!$(".aq-modal_trigger").length && !$(".portfolio-btn").length) return; // let aqTimeline; // let isAqOpen = false; // $(".aq-modal_trigger").each(function () { // $(this).on("click", function (e) { // e.preventDefault(); // if (isAqOpen) { // closeAqModal(); // return; // } // lenis.stop(); // $(".success-top_wrap").css("overflow-y", "auto"); // aqTimeline = gsap.timeline({ // onComplete: function () { // isAqOpen = true; // }, // }); // aqTimeline.set([".nav_dropdown_backdrop", ".nav_menu_backdrop"], { // pointerEvents: "auto", // }); // aqTimeline.to([".nav_dropdown_backdrop", ".nav_menu_backdrop"], { // opacity: 1, // duration: 0.4, // ease: "power2.out", // }); // aqTimeline.to( // [".nav_links_wrap:not(.is-contact)", ".nav_banner_wrap"], // { // opacity: 0, // duration: 0.4, // ease: "power2.out", // onComplete: function () { // $(".nav_links_wrap").not(".is-contact").css({ // visibility: "hidden", // pointerEvents: "none", // }); // $(".nav_banner_wrap").css({ // visibility: "hidden", // pointerEvents: "none", // }); // }, // }, // "<" // ); // aqTimeline.to( // ".success-top_wrap", // { // opacity: 1, // visibility: "visible", // duration: 0.5, // ease: "power3.out", // pointerEvents: "auto", // }, // "-=0.2" // ); // }); // }); // function closeAqModal() { // if (!isAqOpen) return; // let closeTimeline = gsap.timeline({ // onComplete: function () { // isAqOpen = false; // $(".progress-wrap").not(".is-portfolio").not(".is-contact").show(); // lenis.start(); // }, // }); // closeTimeline.to(".success-top_wrap", { // opacity: 0, // duration: 0.5, // ease: "power3.in", // }); // closeTimeline.set(".success-top_wrap", { // visibility: "hidden", // pointerEvents: "none", // }); // closeTimeline.to( // [".nav_dropdown_backdrop", ".nav_menu_backdrop"], // { // opacity: 0, // duration: 0.4, // ease: "power2.in", // }, // "-=0.4" // ); // closeTimeline.to( // [".nav_links_wrap:not(.is-contact)", ".nav_banner_wrap"], // { // opacity: 1, // duration: 0.4, // ease: "power2.in", // onStart: function () { // $(".nav_links_wrap").not(".is-contact").css({ // visibility: "", // pointerEvents: "", // }); // $(".nav_banner_wrap").css({ // visibility: "", // pointerEvents: "", // }); // }, // }, // "<" // ); // closeTimeline.set([".nav_dropdown_backdrop", ".nav_menu_backdrop"], { // pointerEvents: "none", // }); // } // $(".nav_menu_backdrop, .success-top_wrap, .progress-wrap.is-portfolio").each( // function () { // $(this).on("click", function (e) { // e.preventDefault(); // closeAqModal(); // }); // } // ); // }); // Locale Switch Functionality gsap.registerPlugin(); const localeDropdown = document.querySelector(".locale-dropdown"); const navWrap = document.querySelector(".nav_banner_wrap"); const localeWrap = document.querySelector(".locale-options_wrap"); let tl = gsap.timeline({ paused: true, reversed: true }); tl.set(localeWrap, { opacity: 1, pointerEvents: "auto" }).to(navWrap, { height: "7rem", duration: 0.35, ease: "power2.out", }); const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 0; if (isTouchDevice) { let isOpen = false; localeDropdown.addEventListener("click", (e) => { // If the click is on a locale link, let it navigate and don't toggle if (e.target.closest(".locale-link")) return; e.stopPropagation(); isOpen = !isOpen; isOpen ? tl.play() : tl.reverse(); }); document.addEventListener("click", () => { if (isOpen) { isOpen = false; tl.reverse(); } }); } else { localeDropdown.addEventListener("mouseenter", () => tl.play()); localeDropdown.addEventListener("mouseleave", () => tl.reverse()); } $(function () { const localeDropdown = $(".locale-dropdown"); const navWrap = $(".nav_banner_wrap"); const localeWrap = $(".locale-options_wrap"); if (!localeDropdown.length || !navWrap.length || !localeWrap.length) return; let isOpen = false; localeDropdown.on("click touchstart", function (e) { // If the click is on a locale link, let it navigate and don't toggle if ($(e.target).closest(".locale-link").length) return; e.preventDefault(); e.stopPropagation(); isOpen = !isOpen; if (isOpen) { navWrap.css("height", "7rem"); localeWrap.css({ opacity: "1", pointerEvents: "auto" }); } else { navWrap.css("height", ""); localeWrap.css({ opacity: "0", pointerEvents: "none" }); } }); }); // Preloader & Loading Animations // const hasShownPreloader = sessionStorage.getItem("preloaderShown"); // if (!hasShownPreloader) { // document.documentElement.classList.add("no-scroll"); // } // const style = document.createElement("style"); // style.textContent = ` // html.no-scroll, // html.no-scroll body { // overflow: hidden !important; // height: 100% !important; // } // `; // document.head.appendChild(style); // function unlockScroll() { // document.documentElement.classList.remove("no-scroll"); // document.body.style.overflow = ""; // document.documentElement.style.overflow = ""; // if (window.lenis) { // window.lenis.start(); // window.lenis.resize(); // } // setTimeout(() => { // ScrollTrigger.refresh(); // }, 300); // } // // Wait for page to load // window.addEventListener("load", () => { // const preloader = document.querySelector(".preloader_wrap"); // const loadingLottie = document.querySelector(".loading-lottie"); // const lineBreak = document.querySelector(".line_break_full"); // const heroText = document.querySelector(".hero_text"); // // If preloader was already shown this session // if (hasShownPreloader) { // if (preloader) { // preloader.remove(); // } // if (lineBreak) { // lineBreak.style.width = "100%"; // lineBreak.style.transition = "none"; // } // if (heroText) { // heroText.style.visibility = "visible"; // } // unlockScroll(); // return; // } // // Safety fallback — unlock scroll after 8 seconds no matter what // setTimeout(unlockScroll, 8000); // if (preloader) { // if (loadingLottie) { // gsap.set(loadingLottie, { // position: "fixed", // pointerEvents: "none", // xPercent: -50, // yPercent: -50, // left: "50%", // top: "50%", // }); // const onMouseMove = (e) => { // gsap.set(loadingLottie, { left: e.clientX, top: e.clientY }); // }; // window.addEventListener("mousemove", onMouseMove); // // Set sessionStorage and clean up when preloader fades out // preloader.addEventListener("transitionend", () => { // sessionStorage.setItem("preloaderShown", "true"); // moved here // window.removeEventListener("mousemove", onMouseMove); // preloader.remove(); // }); // } // setTimeout(() => { // if (loadingLottie) { // loadingLottie.classList.add("fade-out"); // } // setTimeout(() => { // preloader.classList.add("fade-out"); // if (lineBreak) { // lineBreak.classList.add("grow"); // } // if (heroText) { // typeText(heroText, unlockScroll); // } else { // unlockScroll(); // } // }, 500); // }, 1000); // } else { // // No preloader found, unlock immediately // unlockScroll(); // } // }); // function typeText(element, onComplete) { // const originalText = element.textContent; // const cursor = ""; // element.textContent = ""; // element.style.visibility = "visible"; // let index = 0; // const speed = 50; // const typeInterval = setInterval(() => { // if (index < originalText.length) { // element.textContent = originalText.substring(0, index + 1) + cursor; // index++; // } else { // let flashes = 0; // const flashInterval = setInterval(() => { // element.textContent = // flashes % 2 === 0 ? originalText : originalText + cursor; // flashes++; // if (flashes > 1) { // clearInterval(flashInterval); // element.textContent = originalText; // if (onComplete) onComplete(); // } // }, 300); // clearInterval(typeInterval); // } // }, speed); // } // ============================================================ // PAGE TRANSITIONS // ============================================================ // let currentMouseX = window.innerWidth / 2; // let currentMouseY = window.innerHeight / 2; // window.addEventListener("mousemove", (e) => { // currentMouseX = e.clientX; // currentMouseY = e.clientY; // }); // function initPageTransitions() { // const bg = document.querySelector(".transition_bg"); // const lottie = document.querySelector(".transition-lottie"); // if (!bg || !lottie) { // console.warn("Page transitions: missing element, skipping."); // return; // } // const preloaderActive = // document.querySelector(".preloader_wrap") && // !sessionStorage.getItem("preloaderShown"); // // ---- Lottie cursor tracking ---- // let mouseMoveHandler = null; // function startLottieFollow() { // gsap.set(lottie, { // position: "fixed", // pointerEvents: "none", // xPercent: -50, // yPercent: -50, // left: currentMouseX, // top: currentMouseY, // zIndex: 10000, // }); // mouseMoveHandler = (e) => { // gsap.to(lottie, { // left: e.clientX, // top: e.clientY, // duration: 0.4, // ease: "power3.out", // }); // }; // window.addEventListener("mousemove", mouseMoveHandler); // } // function stopLottieFollow() { // if (mouseMoveHandler) { // window.removeEventListener("mousemove", mouseMoveHandler); // mouseMoveHandler = null; // } // } // // ---- EXIT ---- // function exit(targetUrl) { // if (window.lenis) window.lenis.stop(); // gsap.set(lottie, { autoAlpha: 0 }); // gsap.fromTo( // bg, // { autoAlpha: 0 }, // { // autoAlpha: 1, // duration: 1, // ease: "power2.inOut", // onComplete: () => { // window.location.href = targetUrl; // }, // } // ); // } // // ---- ENTER ---- // function enter() { // document.documentElement.classList.remove("is-transitioning"); // // If preloader is handling this page load, step aside // if (preloaderActive) { // gsap.set(bg, { autoAlpha: 0 }); // gsap.set(lottie, { autoAlpha: 0 }); // stopLottieFollow(); // return; // } // gsap.set(bg, { autoAlpha: 1 }); // gsap.set(lottie, { autoAlpha: 0 }); // startLottieFollow(); // gsap // .timeline() // .to(lottie, { // autoAlpha: 1, // duration: 0.3, // ease: "power2.out", // }) // .to({}, { duration: 1 }) // .to([bg, lottie], { // autoAlpha: 0, // duration: 1, // ease: "power2.inOut", // onComplete: () => { // stopLottieFollow(); // if (window.lenis) { // window.lenis.start(); // window.lenis.resize(); // } // setTimeout(() => ScrollTrigger.refresh(), 300); // }, // }); // } // // ---- INTERCEPT ALL INTERNAL LINKS ---- // document.addEventListener("click", (e) => { // const link = e.target.closest("a"); // if (!link) return; // const href = link.getAttribute("href"); // if (!href) return; // if (href.startsWith("#")) return; // if (href.startsWith("mailto:")) return; // if (href.startsWith("tel:")) return; // if (link.target === "_blank") return; // if (link.hostname !== window.location.hostname) return; // if (link.href === window.location.href) return; // e.preventDefault(); // exit(link.href); // }); // // ---- RUN ENTER IF ARRIVING VIA TRANSITION ---- // if (document.documentElement.classList.contains("is-transitioning")) { // enter(); // } else { // if (!preloaderActive) { // gsap.set(bg, { autoAlpha: 0 }); // gsap.set(lottie, { autoAlpha: 0 }); // } // } // // ---- HANDLE BACK/FORWARD CACHE ---- // window.addEventListener("pageshow", (e) => { // if (e.persisted) { // enter(); // } // }); // } // window.addEventListener("load", initPageTransitions); // FAQ $(".faq-item_wrap").each(function () { $(this) .find(".faq-item_question") .on("click", function () { var $item = $(this).closest(".faq-item_wrap"); var isActive = $item.hasClass("is-active"); $(".faq-item_wrap").removeClass("is-active"); if (!isActive) { $item.addClass("is-active"); } }); }); // Greening Elements Swiper function initCascadingSlider() { const duration = 0.65; const ease = "power3.inOut"; const breakpoints = [ { maxWidth: 479, activeWidth: 0.78, siblingWidth: 0.08 }, { maxWidth: 767, activeWidth: 0.7, siblingWidth: 0.1 }, { maxWidth: 991, activeWidth: 0.6, siblingWidth: 0.1 }, { maxWidth: Infinity, activeWidth: 0.6, siblingWidth: 0.13 }, ]; const wrappers = document.querySelectorAll("[data-cascading-slider-wrap]"); wrappers.forEach(setupInstance); function setupInstance(wrapper) { const viewport = wrapper.querySelector("[data-cascading-viewport]"); const prevButton = wrapper.querySelector("[data-cascading-slider-prev]"); const nextButton = wrapper.querySelector("[data-cascading-slider-next]"); const slides = Array.from( viewport.querySelectorAll("[data-cascading-slide]") ); let totalSlides = slides.length; if (totalSlides === 0) return; if (totalSlides < 9) { const originalSlides = slides.slice(); while (slides.length < 9) { originalSlides.forEach(function (original) { const clone = original.cloneNode(true); clone.setAttribute("data-clone", ""); viewport.appendChild(clone); slides.push(clone); }); } totalSlides = slides.length; } let activeIndex = 0; let isAnimating = false; let slideWidth = 0; let slotCenters = {}; let slotWidths = {}; function readGap() { const raw = getComputedStyle(viewport).getPropertyValue("--gap").trim(); if (!raw) return 0; const temp = document.createElement("div"); temp.style.width = raw; temp.style.position = "absolute"; temp.style.visibility = "hidden"; viewport.appendChild(temp); const px = temp.offsetWidth; viewport.removeChild(temp); return px; } function getSettings() { const windowWidth = window.innerWidth; for (let i = 0; i < breakpoints.length; i++) { if (windowWidth <= breakpoints[i].maxWidth) return breakpoints[i]; } return breakpoints[breakpoints.length - 1]; } function getOffset(slideIndex, fromIndex) { if (fromIndex === undefined) fromIndex = activeIndex; let distance = slideIndex - fromIndex; const half = totalSlides / 2; if (distance > half) distance -= totalSlides; if (distance < -half) distance += totalSlides; return distance; } function measure() { const settings = getSettings(); const viewportWidth = viewport.offsetWidth; const gap = readGap(); const activeSlideWidth = viewportWidth * settings.activeWidth; const siblingSlideWidth = viewportWidth * settings.siblingWidth; const farSlideWidth = Math.max( 0, (viewportWidth - activeSlideWidth - 2 * siblingSlideWidth - 4 * gap) / 2 ); slideWidth = activeSlideWidth; const visibleSlots = [ { slot: -2, width: farSlideWidth }, { slot: -1, width: siblingSlideWidth }, { slot: 0, width: activeSlideWidth }, { slot: 1, width: siblingSlideWidth }, { slot: 2, width: farSlideWidth }, ]; // Don't charge a gap next to a slot that collapsed to 0 width — // otherwise total row width exceeds viewportWidth once far slides vanish. let contentWidth = 0; visibleSlots.forEach(function (def, i) { contentWidth += def.width; const next = visibleSlots[i + 1]; if (next && def.width > 0 && next.width > 0) contentWidth += gap; }); // Center the row instead of assuming it always fills viewportWidth // (only true when farSlideWidth > 0). let x = (viewportWidth - contentWidth) / 2; visibleSlots.forEach(function (def, i) { slotCenters[String(def.slot)] = x + def.width / 2; slotWidths[String(def.slot)] = def.width; const next = visibleSlots[i + 1]; if (next) { x += def.width + (def.width > 0 && next.width > 0 ? gap : 0); } }); slotCenters["-3"] = slotCenters["-2"] - farSlideWidth / 2 - gap - farSlideWidth / 2; slotWidths["-3"] = farSlideWidth; slotCenters["3"] = slotCenters["2"] + farSlideWidth / 2 + gap + farSlideWidth / 2; slotWidths["3"] = farSlideWidth; slides.forEach(function (slide) { slide.style.width = slideWidth + "px"; }); } function getSlideProps(offset) { const clamped = Math.max(-3, Math.min(3, offset)); const slotWidth = slotWidths[String(clamped)]; const clipAmount = Math.max(0, (slideWidth - slotWidth) / 2); const translateX = slotCenters[String(clamped)] - slideWidth / 2; return { x: translateX, "--clip": clipAmount, zIndex: 10 - Math.abs(clamped), }; } function layout(animate, previousIndex) { slides.forEach(function (slide, index) { const offset = getOffset(index); if (offset < -3 || offset > 3) { if (animate && previousIndex !== undefined) { const previousOffset = getOffset(index, previousIndex); if (previousOffset >= -2 && previousOffset <= 2) { const exitSlot = previousOffset < 0 ? -3 : 3; gsap.to( slide, Object.assign({}, getSlideProps(exitSlot), { duration: duration, ease: ease, overwrite: true, }) ); return; } } const parkSlot = offset < 0 ? -3 : 3; gsap.set(slide, getSlideProps(parkSlot)); return; } const props = getSlideProps(offset); slide.setAttribute("data-status", offset === 0 ? "active" : "inactive"); if (animate) { gsap.to( slide, Object.assign({}, props, { duration: duration, ease: ease, overwrite: true, }) ); } else { gsap.set(slide, props); } }); } function goTo(targetIndex) { const normalizedTarget = ((targetIndex % totalSlides) + totalSlides) % totalSlides; if (isAnimating || normalizedTarget === activeIndex) return; isAnimating = true; const previousIndex = activeIndex; const travelDirection = getOffset(normalizedTarget, previousIndex) > 0 ? 1 : -1; slides.forEach(function (slide, index) { const currentOffset = getOffset(index, previousIndex); const nextOffset = getOffset(index, normalizedTarget); const wasInRange = currentOffset >= -3 && currentOffset <= 3; const willBeVisible = nextOffset >= -2 && nextOffset <= 2; if (!wasInRange && willBeVisible) { const entrySlot = travelDirection > 0 ? 3 : -3; gsap.set(slide, getSlideProps(entrySlot)); } const wasInvisible = Math.abs(currentOffset) >= 3; const willBeStaging = Math.abs(nextOffset) === 3; const crossesSides = currentOffset * nextOffset < 0; if (wasInvisible && willBeStaging && crossesSides) { gsap.set(slide, getSlideProps(nextOffset > 0 ? 3 : -3)); } }); activeIndex = normalizedTarget; layout(true, previousIndex); gsap.delayedCall(duration + 0.05, function () { isAnimating = false; }); } if (prevButton) prevButton.addEventListener("click", function () { goTo(activeIndex - 1); }); if (nextButton) nextButton.addEventListener("click", function () { goTo(activeIndex + 1); }); slides.forEach(function (slide, index) { slide.addEventListener("click", function () { if (index !== activeIndex) goTo(index); }); }); document.addEventListener("keydown", function (event) { if (event.key === "ArrowLeft") goTo(activeIndex - 1); if (event.key === "ArrowRight") goTo(activeIndex + 1); }); let resizeTimer; window.addEventListener("resize", function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { measure(); layout(false); }, 100); }); measure(); layout(false); } } // Initialize Cascading Slider document.addEventListener("DOMContentLoaded", function () { initCascadingSlider(); }); // Mobile menu text toggle document.addEventListener("DOMContentLoaded", function () { const menuToggle = document.querySelector( ".nav_dropdown_component.is-mobi .w-dropdown-toggle" ); const menuText = document.querySelector(".mobi-nav_links_text"); if (menuToggle && menuText) { const observer = new MutationObserver(() => { if (menuToggle.classList.contains("w--open")) { menuText.textContent = "Close"; } else { menuText.textContent = "Menu"; } }); observer.observe(menuToggle, { attributes: true, attributeFilter: ["class"], }); } }); // Project return functionality (function () { // ====== CONFIG ====== const PROJECT_LINK_SELECTOR = ".projects_item"; const CLOSE_BUTTON_SELECTOR = ".progress-wrap.is-page"; const STORAGE_KEY = "lastPageData"; const FALLBACK_URL = "/"; // homepage fallback when there's nothing stored // Safe helper: lenis may not exist on every page (e.g. direct landing on a // project page). A bare `lenis` reference throws a ReferenceError if it's // never been declared, which is what was originally crashing the page. function getLenis() { return typeof lenis !== "undefined" ? lenis : null; } // ====== SAVE PAGE + SCROLL POSITION ====== const projectLinks = document.querySelectorAll(PROJECT_LINK_SELECTOR); projectLinks.forEach((link) => { link.addEventListener("click", () => { // Only save if we don't already have a stored URL // This prevents project pages overwriting the original referrer if (!sessionStorage.getItem(STORAGE_KEY)) { const l = getLenis(); const data = { url: window.location.pathname + window.location.search, scroll: l ? l.scroll : window.scrollY, }; sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } }); }); // ====== RESTORE SCROLL IF RETURNING ====== function restoreScroll() { const stored = sessionStorage.getItem(STORAGE_KEY); if (!stored) return; try { const data = JSON.parse(stored); // Only restore if we're back on the same page const currentUrl = window.location.pathname + window.location.search; if (currentUrl === data.url) { setTimeout(() => { const l = getLenis(); if (l) { l.scrollTo(parseInt(data.scroll, 10) || 0, { immediate: true, }); } else { window.scrollTo(0, parseInt(data.scroll, 10) || 0); } }, 300); sessionStorage.removeItem(STORAGE_KEY); } } catch (e) { console.warn("Scroll restore error:", e); } } window.addEventListener("load", restoreScroll); // ====== CLOSE BUTTON ====== const closeButton = document.querySelector(CLOSE_BUTTON_SELECTOR); if (closeButton) { closeButton.addEventListener("click", (e) => { e.preventDefault(); const stored = sessionStorage.getItem(STORAGE_KEY); if (stored) { try { const data = JSON.parse(stored); if (data && data.url) { // Real back-navigation gets native, instant scroll restoration // from the browser itself (and bfcache in supporting browsers) // — no visible reload flash. sessionStorage is left intact as // a fallback for restoreScroll() in case a fresh load happens // instead of a bfcache restore. window.history.back(); return; } } catch (e) {} } // Invalid or nothing stored — safe to clear before falling back. sessionStorage.removeItem(STORAGE_KEY); // Nothing stored — send them home. window.location.href = FALLBACK_URL; }); } })(); // Blog return functionality (function () { // ====== CONFIG ====== const BLOG_LINK_SELECTOR = ".ft-primary-img_wrap, .ft-secondary-img_wrap, .featured-post_item"; // Scoped so this can never also match the project or greening close // buttons (plain ".progress-wrap" would match both, causing multiple // click handlers to fire on the same button). const CLOSE_BUTTON_SELECTOR = ".progress-wrap:not(.is-page):not(.is-greening)"; const STORAGE_KEY = "lastBlogPageData"; const FALLBACK_URL = "/"; // homepage fallback when there's nothing stored // ====== SAVE PAGE + SCROLL POSITION ====== const blogLinks = document.querySelectorAll(BLOG_LINK_SELECTOR); blogLinks.forEach((link) => { link.addEventListener("click", () => { const data = { url: window.location.pathname + window.location.search, scroll: window.scrollY, }; sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data)); }); }); // ====== RESTORE SCROLL IF RETURNING ====== function restoreScroll() { const stored = sessionStorage.getItem(STORAGE_KEY); if (!stored) return; try { const data = JSON.parse(stored); const currentUrl = window.location.pathname + window.location.search; if (currentUrl === data.url) { setTimeout(() => { window.scrollTo(0, parseInt(data.scroll, 10) || 0); }, 100); sessionStorage.removeItem(STORAGE_KEY); } } catch (e) { console.warn("Scroll restore error:", e); } } window.addEventListener("load", restoreScroll); // ====== CLOSE BUTTON ====== const closeButton = document.querySelector(CLOSE_BUTTON_SELECTOR); if (closeButton) { closeButton.addEventListener("click", (e) => { e.preventDefault(); const stored = sessionStorage.getItem(STORAGE_KEY); if (stored) { try { const data = JSON.parse(stored); if (data && data.url) { // Real back-navigation gets native, instant scroll restoration // from the browser itself (and bfcache in supporting browsers) // — no visible reload flash. sessionStorage is left intact as // a fallback for restoreScroll() in case a fresh load happens // instead of a bfcache restore. window.history.back(); return; } } catch (e) {} } // Invalid or nothing stored — safe to clear before falling back. sessionStorage.removeItem(STORAGE_KEY); // Nothing stored — send them home. window.location.href = FALLBACK_URL; }); } })(); // Greening Elements return functionality (function () { // ====== CONFIG ====== const GREENING_LINK_SELECTOR = ".elements-item_layout"; const CLOSE_BUTTON_SELECTOR = ".progress-wrap.is-greening"; const STORAGE_KEY = "lastGreeningPageData"; const FALLBACK_URL = "/"; // homepage fallback when there's nothing stored function getLenis() { return typeof lenis !== "undefined" ? lenis : null; } // ====== SAVE PAGE + SCROLL POSITION ====== const greeningLinks = document.querySelectorAll(GREENING_LINK_SELECTOR); greeningLinks.forEach((link) => { link.addEventListener("click", () => { if (!sessionStorage.getItem(STORAGE_KEY)) { const l = getLenis(); const data = { url: window.location.pathname + window.location.search, scroll: l ? l.scroll : window.scrollY, }; sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } }); }); // ====== RESTORE SCROLL IF RETURNING ====== function restoreScroll() { const stored = sessionStorage.getItem(STORAGE_KEY); if (!stored) return; try { const data = JSON.parse(stored); const currentUrl = window.location.pathname + window.location.search; if (currentUrl === data.url) { setTimeout(() => { const l = getLenis(); if (l) { l.scrollTo(parseInt(data.scroll, 10) || 0, { immediate: true, }); } else { window.scrollTo(0, parseInt(data.scroll, 10) || 0); } }, 300); sessionStorage.removeItem(STORAGE_KEY); } } catch (e) { console.warn("Scroll restore error:", e); } } window.addEventListener("load", restoreScroll); // ====== CLOSE BUTTON ====== const closeButton = document.querySelector(CLOSE_BUTTON_SELECTOR); if (closeButton) { closeButton.addEventListener("click", (e) => { e.preventDefault(); const stored = sessionStorage.getItem(STORAGE_KEY); if (stored) { try { const data = JSON.parse(stored); if (data && data.url) { // Real back-navigation gets native, instant scroll restoration // from the browser itself (and bfcache in supporting browsers) // — no visible reload flash. sessionStorage is left intact as // a fallback for restoreScroll() in case a fresh load happens // instead of a bfcache restore. window.history.back(); return; } } catch (e) {} } // Invalid or nothing stored — safe to clear before falling back. sessionStorage.removeItem(STORAGE_KEY); // Nothing stored — send them home. window.location.href = FALLBACK_URL; }); } })(); // Locale Switcher Function document.addEventListener("DOMContentLoaded", function () { setTimeout(function () { const match = window.location.pathname.match(/^\/(fr|de)/); const currentLocale = match ? match[1] : "en"; // Translation table: labels[uiLocale][targetLocale] = label text shown // while browsing in `uiLocale`, for the link that switches to `targetLocale`. const labels = { en: { en: "English", fr: "French", de: "German" }, fr: { en: "Anglais", fr: "Français", de: "Allemand" }, de: { en: "Englisch", fr: "Französisch", de: "Deutsch" }, }; const hideMap = { en: ".locale-link.is-3", fr: ".locale-link.is-1", de: ".locale-link.is-2", }; // Maps each dropdown link's class to the locale it switches to const linkLocaleMap = { "is-1": "fr", "is-2": "de", "is-3": "en", }; const currentLabels = labels[currentLocale]; // Update the visible label (shows the current locale, in its own language) document .querySelectorAll(".nav_banner_link.is-locale .nav_banner_text") .forEach(function (el) { el.textContent = currentLabels[currentLocale] + " ↓"; }); // Update each dropdown link's label to match the current UI language // NOTE: assumes the label text sits directly on the .locale-link element. // If it's actually in a nested span, swap `el.textContent` for // `el.querySelector('.your-text-class').textContent` below. Object.keys(linkLocaleMap).forEach(function (linkClass) { const targetLocale = linkLocaleMap[linkClass]; document .querySelectorAll(".locale-link." + linkClass) .forEach(function (el) { el.textContent = currentLabels[targetLocale]; }); }); // Remove the current locale from the dropdown document.querySelectorAll(hideMap[currentLocale]).forEach(function (el) { el.remove(); }); // French document.querySelectorAll(".locale-link.is-1").forEach(function (el) { el.addEventListener("click", function (e) { e.preventDefault(); var path = window.location.pathname.replace(/^\/(de|fr)/, ""); window.location.href = "/fr" + path; }); }); // German document.querySelectorAll(".locale-link.is-2").forEach(function (el) { el.addEventListener("click", function (e) { e.preventDefault(); var path = window.location.pathname.replace(/^\/(de|fr)/, ""); window.location.href = "/de" + path; }); }); // English document.querySelectorAll(".locale-link.is-3").forEach(function (el) { el.addEventListener("click", function (e) { e.preventDefault(); var path = window.location.pathname.replace(/^\/(de|fr)/, ""); window.location.href = path || "/"; }); }); }, 500); }); // Image Trail Effect document.addEventListener("DOMContentLoaded", () => { // Only run on devices with a mouse if (!window.matchMedia("(hover: hover) and (pointer: fine)").matches) return; const lerp = (a, b, n) => (1 - n) * a + n * b; let mousePos = { x: 0, y: 0 }; let lastMousePos = { x: 0, y: 0 }; let cacheMousePos = { x: 0, y: 0 }; let isIdle = true; const handleMouseMove = (e) => { const x = (e.clientX ?? e.touches[0].clientX) + window.scrollX; const y = (e.clientY ?? e.touches[0].clientY) + window.scrollY; mousePos = { x, y }; }; document.body.addEventListener("mousemove", handleMouseMove); document.body.addEventListener("touchmove", handleMouseMove); const getMouseDistance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y); class TrailImage { constructor(el) { this.DOM = { el: el, inner: el.querySelector(".content__img-inner") }; this.rect = this.DOM.el.getBoundingClientRect(); this.clipRows = 3; this.clipColumns = 3; this.clipCount = this.clipRows * this.clipColumns; for (let i = 1; i < this.clipCount; ++i) { this.DOM.el.appendChild(this.DOM.inner.cloneNode(true)); } this.DOM.el.classList.add("content__img--clip"); this.DOM.clipInnerElements = this.DOM.el.children; this.setClipPath(); this.initEvents(); } setClipPath() { for (let i = 0; i < this.clipRows; i++) { for (let j = 0; j < this.clipColumns; j++) { const idx = i * this.clipColumns + j; const top = (100 / this.clipRows) * i + "%"; const bottom = (100 / this.clipRows) * (i + 1) + "%"; const left = (100 / this.clipColumns) * j + "%"; const right = (100 / this.clipColumns) * (j + 1) + "%"; this.DOM.clipInnerElements[ idx ].style.clipPath = `polygon(${left} ${top}, ${right} ${top}, ${right} ${bottom}, ${left} ${bottom})`; } } } initEvents() { window.addEventListener("resize", this.resize.bind(this)); } resize() { gsap.set(this.DOM.el, { scale: 1, x: 0, y: 0, opacity: 0 }); this.rect = this.DOM.el.getBoundingClientRect(); } getRect() { this.rect = this.DOM.el.getBoundingClientRect(); } } class ImageTrail { constructor(DOM_el) { this.DOM = { el: DOM_el }; this.zone = document.querySelector(".main-wrapper"); this.images = [...this.DOM.el.querySelectorAll(".content__img")].map( (img) => new TrailImage(img) ); this.imagesTotal = this.images.length; this.imgPosition = 0; this.zIndexVal = 1; this.activeImagesCount = 0; this.threshold = 80; const onPointerMoveEv = () => { cacheMousePos = { ...mousePos }; requestAnimationFrame(() => this.render()); window.removeEventListener("mousemove", onPointerMoveEv); window.removeEventListener("touchmove", onPointerMoveEv); }; window.addEventListener("mousemove", onPointerMoveEv); window.addEventListener("touchmove", onPointerMoveEv); } render() { const distance = getMouseDistance(mousePos, lastMousePos); cacheMousePos.x = lerp(cacheMousePos.x, mousePos.x, 0.1); cacheMousePos.y = lerp(cacheMousePos.y, mousePos.y, 0.1); if (distance > this.threshold) { const rect = this.zone.getBoundingClientRect(); const inBounds = mousePos.x - window.scrollX >= rect.left && mousePos.x - window.scrollX <= rect.right && mousePos.y - window.scrollY >= rect.top && mousePos.y - window.scrollY <= rect.bottom; if (inBounds) { this.showNextImage(); lastMousePos = { ...mousePos }; } } if (isIdle && this.zIndexVal !== 1) this.zIndexVal = 1; requestAnimationFrame(this.render.bind(this)); } showNextImage() { ++this.zIndexVal; this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0; const img = this.images[this.imgPosition]; gsap.killTweensOf(img.DOM.el); img.timeline = gsap .timeline({ onStart: this.onImageActivated.bind(this), onComplete: this.onImageDeactivated.bind(this), }) .fromTo( img.DOM.el, { opacity: 1, scale: 1, zIndex: this.zIndexVal, x: cacheMousePos.x - img.rect.width / 2, y: cacheMousePos.y - img.rect.height / 2, }, { duration: 0.4, ease: "power1", x: mousePos.x - img.rect.width / 2, y: mousePos.y - img.rect.height / 2, }, 0 ) .to( img.DOM.el, { duration: 0.4, ease: "power3", opacity: 0, scale: 0.2, }, 0.4 ); } onImageActivated() { this.activeImagesCount++; isIdle = false; } onImageDeactivated() { this.activeImagesCount--; if (this.activeImagesCount === 0) isIdle = true; } } window.addEventListener("load", () => { const imgInners = document.querySelectorAll(".content__img-inner"); const total = imgInners.length; let loaded = 0; const onLoaded = () => { loaded++; if (loaded < total) return; const wrapper = document.querySelector(".image-wrap"); if (wrapper) { new ImageTrail(wrapper); } else { console.warn("Image trail: .image-wrap element not found."); } }; if (total === 0) { const wrapper = document.querySelector(".image-wrap"); if (wrapper) new ImageTrail(wrapper); return; } imgInners.forEach((img) => { if (img.complete) { onLoaded(); } else { img.addEventListener("load", onLoaded); img.addEventListener("error", onLoaded); } }); }); }); // end DOMContentLoaded