const onDesktop = (fn) => gsap .matchMedia() .add( "(min-width: 992px) and (any-hover: hover) and (any-pointer: fine)", fn ); const onMobile = (fn) => gsap .matchMedia() .add("(max-width: 1024px) and (hover: none) and (pointer: coarse)", fn); // const onDesktop = (fn) => gsap.matchMedia().add("(min-width: 992px)", fn); // const onMobile = (fn) => gsap.matchMedia().add("(max-width: 991px)", fn); onDesktop(() => {}); onMobile(() => {}); CustomEase.create("ease-1", "M0,0 C0.15,0 0.15,1 1,1"); CustomEase.create( "ease-2", "M0,0 C0.071,0.505 0.192,0.726 0.318,0.852 0.45,0.984 0.504,1 1,1" ); document.addEventListener("click", function (e) { if (e.target.closest(".nav-open")) { e.target .closest("[data-barba='container']") .setAttribute("nav-state", "open"); } if (e.target.closest(".nav-close")) { e.target.closest("[data-barba='container']").setAttribute("nav-state", ""); } }); let muteDelay = 75; let audioCtx = null; let sfxBuffer = null; let audioUnlocked = false; async function initAudio() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); if (sfxBuffer) return; const res = await fetch( "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68d2ccfb234c903955ace821_sfx-trim.mp3" ); const arr = await res.arrayBuffer(); sfxBuffer = await audioCtx.decodeAudioData(arr); } function unlockAudio() { if (audioUnlocked) return; audioUnlocked = true; initAudio().catch(() => {}); if (audioCtx?.state === "suspended") audioCtx.resume(); } const MIN_SOUND_INTERVAL_MS = 60; let lastSoundAt = 0; function tryPlaySfxWA() { const now = performance.now(); if (!audioCtx || !sfxBuffer) return; if (now - lastSoundAt < MIN_SOUND_INTERVAL_MS) return; const src = audioCtx.createBufferSource(); src.buffer = sfxBuffer; const gain = audioCtx.createGain(); gain.gain.value = 1.0; src.connect(gain).connect(audioCtx.destination); src.start(); lastSoundAt = now; } // Wrap your mute button code in a function function initMuteButtons() { $(".mute-btn") .off("click") .on("click", function () { $("body").toggleClass("is-muted"); }); } function distributeByPosition(vars) { var ease = vars.ease && gsap.parseEase(vars.ease), from = vars.from || 0, base = vars.base || 0, axis = vars.axis, ratio = { center: 0.5, end: 1, edges: 0.5 }[from] || 0, distances; return function (i, target, a) { var l = a.length, originX, originY, x, y, d, j, minX, maxX, minY, maxY, positions; if (!distances) { distances = []; minX = minY = Infinity; maxX = maxY = -minX; positions = []; for (j = 0; j < l; j++) { d = a[j].getBoundingClientRect(); x = (d.left + d.right) / 2; //based on the center of each element y = (d.top + d.bottom) / 2; if (x < minX) { minX = x; } if (x > maxX) { maxX = x; } if (y < minY) { minY = y; } if (y > maxY) { maxY = y; } positions[j] = { x: x, y: y }; } originX = isNaN(from) ? minX + (maxX - minX) * ratio : positions[from].x || 0; originY = isNaN(from) ? minY + (maxY - minY) * ratio : positions[from].y || 0; maxX = 0; minX = Infinity; for (j = 0; j < l; j++) { x = positions[j].x - originX; y = originY - positions[j].y; distances[j] = d = !axis ? Math.sqrt(x * x + y * y) : Math.abs(axis === "y" ? y : x); if (d > maxX) { maxX = d; } if (d < minX) { minX = d; } } distances.max = maxX - minX; distances.min = minX; distances.v = l = (vars.amount || vars.each * l || 0) * (from === "edges" ? -1 : 1); distances.b = l < 0 ? base - l : base; } l = (distances[i] - distances.min) / distances.max; return distances.b + (ease ? ease(l) : l) * distances.v; }; } CustomEase.create("page-transition", "M0,0 C0.9,0 1,1 1,1 "); CustomEase.create("workItemEase", "M0,0 C0.9,0 1,1 1,1 "); const globalIcons = [ "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b539a58bd56a0bceafb_Vector-8.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b52d5a2d798b05e757b_Vector-13.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51167afa69c3f41379_Vector-19.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51fd2f60af75c30586_Vector-18.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51b5cbcd93e19f7ed5_Vector.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b5147f23eece99f17f7_Vector-20.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51e5cde3d99e316319_Vector-22.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51d9b9320e0b4107e2_Vector-21.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51d2298aafc5b0feb2_Vector-10.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51289da1f6849bb1f1_Vector-16.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51d2298aafc5b0fe9f_Vector-11.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51f65a5c2c5d3d2956_Vector-17.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b514de716309e088dc8_Vector-14.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51475648ecdcd92a47_Vector-12.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b5155e447626777c12b_Vector-15.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b519f20bad3f36cb0af_Vector-9.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b518d9189faecce6d62_Vector-7.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51ac689f715a76a9d8_Vector-3.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b510cd6bd06f5ac00cd_Vector-4.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51ed0a3f5e661e4914_Vector-6.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51c8e19135fc9e70b0_Vector-1.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b51170938c7d37b7217_Vector-2.svg", "https://cdn.prod.website-files.com/688b167f96eb413c7eacbe05/68ad5b511a84276a30dba399_Vector-5.svg", ]; CustomEase.create("ease-3", "0.65, 0.01, 0.05, 0.99"); const lenis = new Lenis(); lenis.on("scroll", ScrollTrigger.update); gsap.ticker.add((time) => { lenis.raf(time * 1000); }); gsap.ticker.lagSmoothing(0); // const onDesktop = (fn) => gsap.matchMedia().add("(min-width: 992px)", fn); // const onMobile = (fn) => gsap.matchMedia().add("(max-width: 991px)", fn); function singleProjectFunctions() { $("[data-barba-namespace='single-project']").each(function () { let projectSmallTitle = $(this).find("[project-client_name]"); let projectSmallTitleTarget = $(this).find("[project-title_small]"); projectSmallTitleTarget.text(projectSmallTitle.text()); }); $(".project-infinite_section").each(function () { let projInfSection = $(this); let track = projInfSection.find(".project-inf_list"); let progInfItems = projInfSection.find(".project-inf_item"); let spacer = projInfSection.find(".project-infinite_spacer"); let stickyWrapper = projInfSection.find(".project-footer_wrap"); // ----------------------- // Remove items linking to current page // ----------------------- const currentPath = window.location.pathname; const currentUrl = window.location.href; progInfItems.each(function () { const $item = $(this); const $link = $item.find(".project-inf_link"); if ($link.length) { const linkHref = $link.attr("href"); if (linkHref) { // Handle both relative and absolute URLs let linkPath; try { if (linkHref.startsWith("http")) { // Absolute URL const linkUrl = new URL(linkHref); linkPath = linkUrl.pathname; } else { // Relative URL linkPath = linkHref.split("?")[0].split("#")[0]; // Remove query params and hash } // Remove trailing slash for comparison const normalisedCurrentPath = currentPath.replace(/\/$/, "") || "/"; const normalisedLinkPath = linkPath.replace(/\/$/, "") || "/"; if (normalisedCurrentPath === normalisedLinkPath) { $item.remove(); } } catch (e) { // If URL parsing fails, do basic string comparison if (linkHref === currentPath || linkHref === currentUrl) { $item.remove(); } } } } }); // Re-select items after removal progInfItems = projInfSection.find(".project-inf_item"); // If no items remain, exit early if (progInfItems.length === 0) { return; } function updateProjectInfo(item, show = true) { const award = item.attr("project-award") || ""; const name = item.attr("project-name") || ""; if (show) { $(".project-item_award").text(award); $(".project-item_title").text(name); $(".project-inf_item-info").addClass("active"); } else { $(".project-inf_item-info").removeClass("active"); } } // ----------------------- // Desktop functionality // ----------------------- onDesktop(() => { let scrollTriggerInstance = null; let resizeTimer; // Function to set up or refresh the scroll animation function setupScrollAnimation() { // Kill existing ScrollTrigger if it exists if (scrollTriggerInstance) { scrollTriggerInstance.kill(); scrollTriggerInstance = null; } // Reset track position gsap.set(track, { x: 0 }); // Recalculate dimensions const containerWidth = window.innerWidth; let totalItemsWidth = 0; progInfItems.each(function () { totalItemsWidth += $(this).outerWidth(true); }); const scrollDistance = Math.max(0, totalItemsWidth - containerWidth); // If items fit within viewport, no need for scroll if (scrollDistance <= 0) { spacer.css("height", "0px"); return; } // Define consistent speed: pixels moved per 100vh of scroll const SPEED_PX_PER_100VH = 1500; // Adjust this value to set your desired speed // Calculate how much scroll height we need based on the track width and desired speed const scrollHeightNeeded = (scrollDistance / SPEED_PX_PER_100VH) * window.innerHeight; // Set the spacer height to create the required scroll distance spacer.css("height", scrollHeightNeeded + "px"); // Create new ScrollTrigger scrollTriggerInstance = ScrollTrigger.create({ trigger: projInfSection, start: "clamp(top top)", end: "clamp(bottom top)", pin: stickyWrapper, endTrigger: spacer, scrub: 0.4, pinSpacing: false, animation: gsap.to(track, { x: -scrollDistance, ease: "linear", }), }); } // Initial setup setupScrollAnimation(); // Handle resize window.addEventListener("resize", () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { setupScrollAnimation(); // Also refresh Lenis and ScrollTrigger globally if (window.lenis) { lenis.resize(); } ScrollTrigger.refresh(); }, 250); // Debounce resize events }); // Mouse interactions progInfItems.on("mouseenter", function () { const $item = $(this); $item.addClass("project-highlighted"); updateProjectInfo($item, true); }); progInfItems.on("mouseleave", function () { const $item = $(this); $item.removeClass("project-highlighted"); updateProjectInfo($item, false); }); // Cleanup on destroy projInfSection.on("destroy", function () { if (scrollTriggerInstance) { scrollTriggerInstance.kill(); } window.removeEventListener("resize", setupScrollAnimation); }); }); // ----------------------- // Mobile functionality (slider with snap to items) // ----------------------- onMobile(() => { let currentIndex = 0; // logical index 0..n-1 let isAnimating = false; let currentHighlighted = null; // --- Clone originals for infinite loop --- const originalItems = progInfItems.clone(); const cloneBefore = originalItems.clone().addClass("clone-before"); const cloneAfter = originalItems.clone().addClass("clone-after"); track.prepend(cloneBefore); track.append(cloneAfter); // Re-select after cloning const allItems = track.find(".project-inf_item"); const totalOriginalItems = progInfItems.length; // Helpers const norm = (i) => ((i % totalOriginalItems) + totalOriginalItems) % totalOriginalItems; const getViewportCenter = () => { const rect = projInfSection[0].getBoundingClientRect(); return rect.left + rect.width / 2; }; // Measure item positions let itemPositions = []; let totalWidth = 0; const computePositions = () => { itemPositions = []; totalWidth = 0; allItems.each(function () { const itemWidth = $(this).outerWidth(true); itemPositions.push({ element: this, position: totalWidth + itemWidth / 2, width: itemWidth, }); totalWidth += itemWidth; }); }; computePositions(); const getOriginalSetWidth = () => { let w = 0; for (let i = totalOriginalItems; i < totalOriginalItems * 2; i++) { w += itemPositions[i].width; } return w; }; let originalSetWidth = getOriginalSetWidth(); // Wrap x into the middle strip range [-originalSetWidth, 0) const wrapX = (x) => { const w = originalSetWidth; let nx = x % w; if (nx > 0) nx -= w; // keep it negative so we always sit on the middle strip return nx; }; // Quick setter for super-smooth updates const setX = gsap.quickSetter(track[0], "x", "px"); // Start centred on first original item (index 0 in the middle strip) const startPosition = -( itemPositions[totalOriginalItems].position - getViewportCenter() ); setX(wrapX(startPosition)); // Highlight helper (always point at middle/original strip) function updateHighlight(index) { const actualIndex = totalOriginalItems + norm(index); const item = itemPositions[actualIndex].element; if (currentHighlighted !== item) { if (currentHighlighted) $(currentHighlighted).removeClass("project-highlighted"); $(item).addClass("project-highlighted"); currentHighlighted = item; updateProjectInfo($(item), true); } } // Compute the best (nearest) clone for a logical index in {prev, middle, next} const bestActualIndexFor = (logicalIndex) => { const idx = norm(logicalIndex); const candidates = [ idx, // before strip totalOriginalItems + idx, // middle strip totalOriginalItems * 2 + idx, // after strip ]; const center = getViewportCenter(); const current = gsap.getProperty(track[0], "x"); let best = candidates[0]; let bestDist = Infinity; for (const ci of candidates) { const targetPos = -(itemPositions[ci].position - center); const dist = Math.abs(targetPos - current); if (dist < bestDist) { bestDist = dist; best = ci; } } return best; }; // Snap to logical index using a proxy tween; wrap continuously in onUpdate (no post normalise). const proxy = { x: gsap.getProperty(track[0], "x") || 0 }; const snapToLogicalIndex = (logicalIndex, duration = 0.4) => { if (isAnimating) return; isAnimating = true; const actualIndex = bestActualIndexFor(logicalIndex); const targetPos = -( itemPositions[actualIndex].position - getViewportCenter() ); // Update proxy start from current wrapped x to avoid any start jump proxy.x = wrapX(gsap.getProperty(track[0], "x")); gsap.to(proxy, { x: targetPos, duration, ease: "power2.inOut", onUpdate: () => setX(wrapX(proxy.x)), // continuous wrapping prevents blank flashes onComplete: () => { currentIndex = norm(logicalIndex); isAnimating = false; updateHighlight(currentIndex); }, }); }; // Move by delta (-1 or +1) const snapBy = (delta) => snapToLogicalIndex(currentIndex + delta); // Touch swipe (horizontal only), move exactly one item let touchStartX = 0; let touchStartY = 0; let isSwiping = false; projInfSection.on("touchstart", function (e) { if (isAnimating) return; touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; isSwiping = false; }); projInfSection.on("touchmove", function (e) { if (isAnimating) return; const touchCurrentX = e.touches[0].clientX; const touchCurrentY = e.touches[0].clientY; const diffX = Math.abs(touchStartX - touchCurrentX); const diffY = Math.abs(touchStartY - touchCurrentY); if (diffX > diffY && diffX > 10) { isSwiping = true; e.preventDefault(); // lock vertical scroll while swiping horizontally } }); // LEFT swipe = previous (-1). RIGHT swipe = next (+1). projInfSection.on("touchend", function (e) { if (isAnimating || !isSwiping) return; const touchEndX = e.changedTouches[0].clientX; const diff = touchStartX - touchEndX; if (Math.abs(diff) > 50) { snapBy(diff > 0 ? -1 : +1); } isSwiping = false; }); // Initial highlight updateHighlight(0); // Handle resize: recompute positions and keep the same logical index (instant) let resizeTimer; const onResize = () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { computePositions(); originalSetWidth = getOriginalSetWidth(); const ai = bestActualIndexFor(currentIndex); const pos = -(itemPositions[ai].position - getViewportCenter()); proxy.x = pos; // reset proxy setX(wrapX(pos)); // set wrapped x instantly updateHighlight(currentIndex); }, 250); }; window.addEventListener("resize", onResize, { passive: true }); // Cleanup on destroy projInfSection.on("destroy", function () { gsap.killTweensOf(proxy); projInfSection.off("touchstart touchmove touchend"); window.removeEventListener("resize", onResize); }); }); }); setTimeout(() => { onDesktop(() => { lenis.resize(); ScrollTrigger.refresh(); }); }, 50); } function homeFunctions() { $(".page-home").each(function () { let homePage = $(this); let eyeWrap = homePage.find("[eye-toggle]"); eyeWrap.on("click", function () { let workItemBgs = homePage.find(".work-item .work-item_bg"); let toggleTimeline = gsap.timeline({ defaults: { duration: 0.8, ease: "expo.inOut" }, }); if (homePage.attr("work-items-display") !== "show-thumbs") { toggleTimeline .to(workItemBgs, { width: "0%", stagger: distributeByPosition({ amount: 0.75, from: "start", }), ease: "expo.out", // stagger: { amount: 0.75, from: "start" }, }) .call( () => homePage.attr("work-items-display", "show-thumbs"), null, "<" ); } else { toggleTimeline .to(workItemBgs, { width: "100%", stagger: distributeByPosition({ amount: 0.75, from: "start", }), ease: "expo.out", }) .call(() => homePage.removeAttr("work-items-display"), null, "<"); } }); }); class InfiniteScroller { constructor(container) { this.container = container; this.wrap = container.querySelector(".home-infinite_component"); this.items = [...container.querySelectorAll(".work-wrap")]; this.workItems = [...container.querySelectorAll(".work-item")]; if (!this.wrap || !this.items.length) return; // State management this.state = { currentScrollPosition: 0, smoothScrollY: 0, isScrolling: false, currentCenterIndex: -1, velocity: 0, rafId: null, isAnimating: false, mouseX: 0, mouseY: 0, currentHoveredItem: null, frameCount: 0, }; this.config = { maxScrollSpeed: 40, // Maximum pixels per frame - adjust this value scrollMultiplier: 1, // Overall scroll speed multiplier smoothness: 0.12, // Interpolation factor (0.05 = smoother, 0.2 = more responsive) touchMultiplier: 2, // Touch scroll multiplier }; // Cache dimensions this.updateDimensions(); // Device detection this.isMobile = "ontouchstart" in window || navigator.maxTouchPoints > 0; // Bind methods this.animate = this.animate.bind(this); this.handleWheel = this.handleWheel.bind(this); this.handleResize = this.debounce(this.handleResize.bind(this), 50); this.updateHoverState = this.updateHoverState.bind(this); this.init(); } init() { // Set initial positions this.adjustItemsPosition(0); // Add CSS classes for performance this.container.classList.add("infinite-scroll-container"); // Prevent default scroll behaviour on the container this.container.style.overflow = "hidden"; this.wrap.style.overflow = "visible"; // Setup event listeners with proper options this.setupEventListeners(); // Setup Intersection Observer for mobile if (this.isMobile) { this.setupIntersectionObserver(); } // Start animation loop only when needed this.startAnimation(); } updateDimensions() { this.dimensions = { wrapHeight: this.wrap.clientHeight, itemHeight: this.items[0]?.clientHeight || 0, totalHeight: this.items.length * (this.items[0]?.clientHeight || 0), }; } setupEventListeners() { // Wheel events with passive flag for better performance // Listen on the entire container for scrolling anywhere if (!this.isMobile) { this.container.addEventListener("wheel", this.handleWheel, { passive: true, }); this.setupDesktopHovers(); this.setupMouseTracking(); } else { this.setupTouchEvents(); } // Resize observer for better performance than resize event const resizeObserver = new ResizeObserver(this.handleResize); resizeObserver.observe(this.wrap); } setupMouseTracking() { // Track mouse position for dynamic hover updates while scrolling this.container.addEventListener("mousemove", (e) => { this.state.mouseX = e.clientX; this.state.mouseY = e.clientY; }); // Clear hover state when mouse leaves container this.container.addEventListener("mouseleave", () => { this.state.mouseX = 0; this.state.mouseY = 0; if (this.state.currentHoveredItem) { this.handleItemLeave(); this.state.currentHoveredItem = null; } }); } setupIntersectionObserver() { const options = { root: null, rootMargin: "-50% 0px -50% 0px", // Only trigger when item is near centre threshold: 0, }; this.observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const index = this.workItems.indexOf(entry.target); if (index !== -1) { this.updateCenterItem(index); } } }); }, options); this.workItems.forEach((item) => this.observer.observe(item)); } setupDesktopHovers() { this.workItems.forEach((item, index) => { item.addEventListener("mouseenter", (e) => { this.handleItemHover(item, index); }); item.addEventListener("mouseleave", (e) => { const relatedTarget = e.relatedTarget; const isLeavingToAnotherWorkItem = relatedTarget?.closest(".work-item"); if (!isLeavingToAnotherWorkItem && !this.state.isScrolling) { this.handleItemLeave(); } }); }); // Filter items hover functionality this.setupFilterHovers(); // Info panel follow cursor this.setupInfoPanelTracking(); } updateHoverState() { // Check which element is under the cursor during scrolling if (this.state.mouseX === 0 && this.state.mouseY === 0) return; const elementUnderCursor = document.elementFromPoint( this.state.mouseX, this.state.mouseY ); const workItem = elementUnderCursor?.closest(".work-item"); // If we're hovering over a different item (or no item) if (workItem !== this.state.currentHoveredItem) { // Clear previous hover if (this.state.currentHoveredItem) { this.handleItemLeave(); } // Set new hover if over a work item if (workItem && this.workItems.includes(workItem)) { const index = this.workItems.indexOf(workItem); this.handleItemHover(workItem, index); } else { this.state.currentHoveredItem = null; } } } setupFilterHovers() { const filterItems = document.querySelectorAll(".filter-item"); filterItems.forEach((filterItem) => { filterItem.addEventListener("mouseenter", () => { const filterText = filterItem .querySelector(".filter-text") ?.textContent.trim(); if (!filterText) return; this.workItems.forEach((workItem) => { const hasCategory = [ ...workItem.querySelectorAll(".hidden-filter_item"), ].some((el) => el.getAttribute("filter-category") === filterText); if (hasCategory) { workItem.classList.add("filter-highlight"); } }); }); filterItem.addEventListener("mouseleave", () => { this.workItems.forEach((workItem) => { workItem.classList.remove("filter-highlight"); }); }); }); } setupInfoPanelTracking() { const infoPanel = document.querySelector(".work-item_info"); if (!infoPanel) return; let rafId = null; let mouseY = 0; const updatePosition = () => { infoPanel.style.transform = `translate(0, ${mouseY}px) translateY(-50%)`; rafId = null; }; this.container.addEventListener("mousemove", (e) => { mouseY = e.clientY; if (!rafId) { rafId = requestAnimationFrame(updatePosition); } }); } handleItemHover(item, index) { // Store the currently hovered item this.state.currentHoveredItem = item; // Remove all classes first this.workItems.forEach((el) => { el.classList.remove("target-project", "adj-project"); }); // Add classes for CSS transitions item.classList.add("target-project"); const totalItems = this.workItems.length; const nextIndex = (index + 1) % totalItems; const prevIndex = (index - 1 + totalItems) % totalItems; this.workItems[nextIndex]?.classList.add("adj-project"); this.workItems[prevIndex]?.classList.add("adj-project"); // Update info display this.updateWorkItemInfo(item); // Show info panel const infoPanel = document.querySelector(".work-item_info"); if (infoPanel) { infoPanel.classList.add("visible"); } } handleItemLeave() { this.state.currentHoveredItem = null; this.workItems.forEach((item) => { item.classList.remove("target-project", "adj-project"); }); const infoPanel = document.querySelector(".work-item_info"); if (infoPanel) { infoPanel.classList.remove("visible"); } // Clear info this.clearWorkItemInfo(); } setupTouchEvents() { let touchStartY = 0; let lastTouchTime = 0; // Listen on container for touch anywhere on the component this.container.addEventListener( "touchstart", (e) => { touchStartY = e.touches[0].clientY; lastTouchTime = Date.now(); this.state.velocity = 0; }, { passive: true } ); this.container.addEventListener( "touchmove", (e) => { e.preventDefault(); const currentTime = Date.now(); const currentTouchY = e.touches[0].clientY; let deltaY = touchStartY - currentTouchY; const deltaTime = currentTime - lastTouchTime; if (deltaTime > 0) { this.state.velocity = deltaY / deltaTime; } // Apply touch multiplier and clamp to max speed let scrollDelta = deltaY * this.config.touchMultiplier; scrollDelta = Math.max( -this.config.maxScrollSpeed, Math.min(this.config.maxScrollSpeed, scrollDelta) ); this.state.currentScrollPosition -= scrollDelta; touchStartY = currentTouchY; lastTouchTime = currentTime; this.startAnimation(); }, { passive: false } ); this.container.addEventListener( "touchend", () => { this.applyMomentum(); }, { passive: true } ); } applyMomentum() { const decay = () => { if (Math.abs(this.state.velocity) > 0.1) { // Apply velocity with speed limit let momentumDelta = this.state.velocity * 10; momentumDelta = Math.max( -this.config.maxScrollSpeed, Math.min(this.config.maxScrollSpeed, momentumDelta) ); this.state.currentScrollPosition -= momentumDelta; this.state.velocity *= 0.95; requestAnimationFrame(decay); } else { this.state.velocity = 0; } }; if (Math.abs(this.state.velocity) > 0.5) { decay(); } } handleWheel(event) { // Apply scroll multiplier and clamp to max speed let scrollDelta = event.deltaY * this.config.scrollMultiplier; // Clamp the scroll speed to the maximum scrollDelta = Math.max( -this.config.maxScrollSpeed, Math.min(this.config.maxScrollSpeed, scrollDelta) ); this.state.currentScrollPosition -= scrollDelta; this.startAnimation(); } handleResize() { this.updateDimensions(); this.adjustItemsPosition(this.state.smoothScrollY); } adjustItemsPosition(scroll) { const { itemHeight, totalHeight } = this.dimensions; this.items.forEach((item, index) => { const baseY = index * itemHeight + scroll; const wrappedY = this.wrapValue( -itemHeight, totalHeight - itemHeight, baseY ); item.style.transform = `translateY(${wrappedY}px)`; }); } wrapValue(min, max, value) { const range = max - min; return ((((value - min) % range) + range) % range) + min; } updateCenterItem(index) { if (index === this.state.currentCenterIndex) return; this.state.currentCenterIndex = index; // Use CSS classes for transitions this.workItems.forEach((item) => { item.classList.remove("target-project", "adj-project"); }); const totalItems = this.workItems.length; const centerItem = this.workItems[index]; const prevIndex = (index - 1 + totalItems) % totalItems; const nextIndex = (index + 1) % totalItems; centerItem?.classList.add("target-project"); this.workItems[prevIndex]?.classList.add("adj-project"); this.workItems[nextIndex]?.classList.add("adj-project"); this.updateWorkItemInfo(centerItem); } updateWorkItemInfo(item) { if (!item) return; const award = item.getAttribute("work-award") || ""; const name = item.getAttribute("work-name") || ""; const awardEl = document.querySelector(".work-item_award"); const titleEl = document.querySelector(".work-item_title"); if (awardEl) awardEl.textContent = award; if (titleEl) titleEl.textContent = name; // Update filter categories this.updateFilterCategories(item); } updateFilterCategories(item) { const categories = [...item.querySelectorAll(".hidden-filter_item")] .map((el) => el.getAttribute("filter-category")) .filter(Boolean); document.querySelectorAll(".filter-item").forEach((filterItem) => { const filterText = filterItem .querySelector(".filter-text") ?.textContent.trim(); if (categories.includes(filterText)) { filterItem.classList.add("related"); } else { filterItem.classList.remove("related"); } }); } clearWorkItemInfo() { const awardEl = document.querySelector(".work-item_award"); const titleEl = document.querySelector(".work-item_title"); if (awardEl) awardEl.textContent = ""; if (titleEl) titleEl.textContent = ""; document.querySelectorAll(".filter-item").forEach((item) => { item.classList.remove("related"); }); } startAnimation() { if (this.state.isAnimating) return; this.state.isAnimating = true; this.animate(); } animate() { const diff = this.state.currentScrollPosition - this.state.smoothScrollY; // Check if we're still scrolling const wasScrolling = this.state.isScrolling; this.state.isScrolling = Math.abs(diff) > 0.5; // Stop animation when movement is minimal if (Math.abs(diff) < 0.01) { this.state.isAnimating = false; this.state.isScrolling = false; this.state.smoothScrollY = this.state.currentScrollPosition; this.adjustItemsPosition(this.state.smoothScrollY); this.state.frameCount = 0; // Final hover state check when scrolling stops if (!this.isMobile && wasScrolling) { this.updateHoverState(); } return; } // Smooth interpolation with configurable smoothness this.state.smoothScrollY += diff * this.config.smoothness; this.adjustItemsPosition(this.state.smoothScrollY); // Update hover state while scrolling (desktop only) // Check every 3 frames for better performance if (!this.isMobile && this.state.isScrolling) { this.state.frameCount++; if (this.state.frameCount % 3 === 0) { this.updateHoverState(); } } if (this.state.isAnimating) { this.state.rafId = requestAnimationFrame(this.animate); } } disable() { // Store current state this.wasAnimating = this.state.isAnimating; // Stop the animation loop if (this.state.rafId) { cancelAnimationFrame(this.state.rafId); this.state.rafId = null; } this.state.isAnimating = false; this.state.isScrolling = false; // Remove scroll event listeners if (!this.isMobile) { this.container.removeEventListener("wheel", this.handleWheel); } else { this.container.removeEventListener("touchstart", this.handleTouchStart); this.container.removeEventListener("touchmove", this.handleTouchMove); this.container.removeEventListener("touchend", this.handleTouchEnd); } } enable() { // Re-add scroll event listeners if (!this.isMobile) { this.container.addEventListener("wheel", this.handleWheel, { passive: true, }); } else { this.setupTouchEvents(); } // Restart animation if it was previously running if (this.wasAnimating) { this.startAnimation(); } } // Utility function for debouncing debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } destroy() { // Clean up event listeners and observers if (this.observer) { this.observer.disconnect(); } if (this.state.rafId) { cancelAnimationFrame(this.state.rafId); } // Remove event listeners this.container.removeEventListener("wheel", this.handleWheel); // Clean up hover state if (this.state.currentHoveredItem) { this.handleItemLeave(); } } } /** * Main initialization function */ function initHomeFunctions() { const homePages = document.querySelectorAll(".page-home"); if (!homePages.length) return; homePages.forEach((page) => { // Initialize modules only if required elements exist const hasInfiniteScroll = page.querySelector(".home-infinite_component"); const hasEyeToggle = page.querySelector(".eye-wrap"); const modules = {}; if (hasInfiniteScroll) { modules.scroller = new InfiniteScroller(page); } // Store instances for potential cleanup if (Object.keys(modules).length > 0) { page._homeModules = modules; } }); } initHomeFunctions(); } function logoLottie() { $("[lottie-component]").each(function (index) { let lottieFile = $(this).find("[lottie-el]"); let lottieUrl = lottieFile.attr("lottie-url"); let lottieAnimation = bodymovin.loadAnimation({ container: lottieFile[0], renderer: "svg", autoplay: false, loop: false, path: lottieUrl, }); $("[lottie-component]").on("mouseenter", function () { bodymovin.setDirection(1); lottieAnimation.play(); }); $("[lottie-component]").on("mouseleave", function () { lottieAnimation.setDirection(-1); lottieAnimation.play(); }); }); } logoLottie(); function aboutFunctions() { $(".page-about").each(function () { let aboutPage = $(this); const heroElement = aboutPage.find(".about-hero"); const layoutOptions = [null, "layout-1", "layout-2"]; const randomLayout = layoutOptions[Math.floor(Math.random() * layoutOptions.length)]; if (randomLayout) { heroElement.addClass(randomLayout); } function getRandomUniqueImages(array, count) { const shuffled = [...array].sort(() => 0.5 - Math.random()); return shuffled.slice(0, count); } // Get 4 random unique images const selectedImages = getRandomUniqueImages(globalIcons, 4); // Assign them to the floating image elements $(".about-floating_img").each(function (index) { if (selectedImages[index]) { $(this).attr("src", selectedImages[index]); } }); function initializeCube(cubeElement) { let cube = $(cubeElement); let originalFace = cube.find(".title-face").first(); let cubeWrapper = cube.closest(".title-cube_wrap"); // Remove any existing cloned faces cube.find(".title-face.cloned").remove(); // Clone the original face 3 times to create all 4 faces for (let i = 0; i < 3; i++) { let clonedFace = originalFace.clone().addClass("cloned"); cube.append(clonedFace); } // Now get all faces (original + cloned) let cubeFaces = cube.find(".title-face"); // Measure face width and calculate depth let faceWidth = originalFace[0].offsetWidth; let depth = faceWidth / 2; // Set initial positions for all 4 faces gsap.set(cubeFaces[0], { transform: `translateZ(${depth}px)` }); // Front (0°) gsap.set(cubeFaces[1], { transform: `rotateY(90deg) translateZ(${depth}px)`, }); // Right (90°) gsap.set(cubeFaces[2], { transform: `rotateY(180deg) translateZ(${depth}px)`, }); // Back (180°) gsap.set(cubeFaces[3], { transform: `rotateY(-90deg) translateZ(${depth}px)`, }); // Left (270°) return { cube, cubeWrapper }; } // Initialize all cubes within this page-about element let cubes = []; $(this) .find(".title-cube") .each(function () { let { cube, cubeWrapper } = initializeCube(this); // Track current rotation let currentRotation = 0; let isAnimating = false; // Mouse enter event cubeWrapper.on("mouseenter", function () { if (!isAnimating) { isAnimating = true; currentRotation -= 90; // Create new rotation animation gsap.to(cube[0], { rotationY: currentRotation, duration: 0.7, ease: "power2.out", onComplete: function () { isAnimating = false; }, }); } }); // Store cube data for resize handling cubes.push({ element: this, currentRotation: () => currentRotation }); }); // Handle window resize // let resizeTimeout; // $(window).on("resize", function () { // clearTimeout(resizeTimeout); // resizeTimeout = setTimeout(function () { // cubes.forEach((cubeData) => { // initializeCube(cubeData.element); // }); // }, 150); // }); function initializeMouseTriggers() { const floatingElements = aboutPage.find(".about-floating_el"); const cubeElements = aboutPage.find(".title-cube_wrap"); // Using cube wrappers as trigger elements let elementData = []; let cubeData = []; let lastTriggeredElement = null; let isThrottled = false; // Initialize floating element data floatingElements.each(function (index) { const $element = $(this); const rect = this.getBoundingClientRect(); const scrollTop = $(window).scrollTop(); elementData.push({ element: $element, topY: rect.top + scrollTop, bottomY: rect.bottom + scrollTop, hasTriggered: false, }); }); // Initialize cube data cubeElements.each(function (index) { const $cube = $(this); cubeData.push({ element: $cube, index: index, }); }); // Hover event on cubes cubeElements.on("mouseenter.floatingTrigger", function (e) { if (isThrottled) return; const cubeRect = this.getBoundingClientRect(); const scrollTop = $(window).scrollTop(); // Get cube's vertical position const cubeTopY = cubeRect.top + scrollTop; const cubeBottomY = cubeRect.bottom + scrollTop; // Throttle to prevent rapid triggers isThrottled = true; setTimeout(() => { isThrottled = false; }, 100); // Check which floating element this cube is vertically aligned with elementData.forEach((data, index) => { const { element, topY, bottomY } = data; const tolerance = 20; // Adjust this value to make the trigger area more or less sensitive // Check if cube's Y position is within the Y bounds of the floating element const isWithinVerticalBounds = cubeTopY <= bottomY + tolerance && cubeBottomY >= topY - tolerance; // Trigger if cube is within vertical bounds and hasn't been triggered recently if ( isWithinVerticalBounds && !data.hasTriggered && lastTriggeredElement !== index ) { triggerFloatingAnimation(element, index + 1, data); lastTriggeredElement = index; // Reset trigger state after animation setTimeout(() => { data.hasTriggered = false; if (lastTriggeredElement === index) { lastTriggeredElement = null; } }, 1200); } }); }); // Update positions on scroll and resize function updateElementPositions() { floatingElements.each(function (index) { const rect = this.getBoundingClientRect(); const scrollTop = $(window).scrollTop(); if (elementData[index]) { elementData[index].topY = rect.top + scrollTop; elementData[index].bottomY = rect.bottom + scrollTop; } }); } // Initial position update setTimeout(() => { updateElementPositions(); }, 1); $(window).on( "mousemove scroll.floatingTrigger resize.floatingTrigger", updateElementPositions ); return elementData; } function triggerFloatingAnimation(element, elementNumber, data) { data.hasTriggered = true; const moveDirection = elementNumber === 1 || elementNumber === 3 ? "-60%" : "60%"; // Create timeline for the animation sequence const floatingIconTl = gsap.timeline(); floatingIconTl .to(element[0], { x: moveDirection, duration: 0.7, ease: "power2.out", }) .to(element[0], { x: "0%", duration: 0.7, ease: "sine.inOut", }); } // Initialize the mouse trigger system const elementData = initializeMouseTriggers.call(this); // Force an immediate position update after a brief delay setTimeout(() => { $(this) .find(".about-floating_el") .each(function (index) { const rect = this.getBoundingClientRect(); const scrollTop = $(window).scrollTop(); if (elementData[index]) { elementData[index].topY = rect.top + scrollTop; elementData[index].bottomY = rect.bottom + scrollTop; } }); }, 150); // Clean up on page change/unload $(window).on("beforeunload", function () { $(document).off(".floatingTrigger"); $(window).off(".floatingTrigger"); }); let aboutHero = aboutPage.find(".about-hero"); let aboutDivider = aboutPage.find(".about-divider"); let aboutPanels = aboutPage.find(".about-divider_panel"); let aboutPanelsMobile = aboutPage.find(".about-divider_panel.is-mobile"); let aboutPanelsTl = gsap.timeline({ scrollTrigger: { trigger: aboutDivider, start: "top bottom", end: "bottom top", scrub: 0.4, }, defaults: { ease: "linear" }, }); onDesktop(() => { aboutPanelsTl.fromTo( aboutPanels, { height: "0%", }, { height: "100%", ease: "power1.inOut", stagger: { amount: 1, from: "center", // ease: "power3.out", }, } ); }); onMobile(() => { aboutPanelsTl.fromTo( aboutPanelsMobile, { height: "0%", }, { height: "100%", ease: "power1.out", stagger: { each: 0.06, from: "center", // ease: "power3.out", }, } ); }); (function () { const container = document.getElementById("container"); const scene = document.getElementById("scene"); const toggleBtn = document.querySelector(".shape-toggle"); const aboutHero = document.querySelector(".team-section"); // Config const baseRadiusUnits = 100; // logical units const particleCountSphere = 50; const particleCountRings = 30; const minBrightness = 0.1; const maxBrightness = 1.0; const baseRotationSpeed = 0.001; const dragSensitivity = 0.0025; const momentumDecay = 0.95; const minMomentum = 0.001; // State let dotElements = []; let spherePositions = []; let ringPositions = []; let isSpherical = true; let morphProgress = 0; let targetMorphProgress = 0; const morphSpeed = 0.05; let rotX = 0; let rotY = 0; let momentum = { x: 0, y: 0.002 }; let isDragging = false; let prevMouse = { x: 0, y: 0 }; let dragVelocity = { x: 0, y: 0 }; let outerRingRotation = 0; let innerRingRotation = 0; const outerRingSpeed = 0.001; const innerRingSpeed = -0.001; let innerRingUserRotX = 0; let innerRingUserRotY = 0; let outerRingUserYaw = 0; // user-driven spin around Y let ringMomentum = { outerY: 0, innerX: 0, innerY: 0 }; const innerRingTiltX = Math.PI * 0.9; const innerRingYawOffset = Math.PI * 0.1; const ringDragSensitivityX = 0.0022; // horizontal -> inner yaw const ringDragSensitivityY = 0.002; // vertical -> pitch let pxPerUnit = 2; // responsive scale, recalculated on resize // Utilities function clamp01(v) { return Math.max(0, Math.min(1, v)); } function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); } function lerp(a, b, t) { return a + (b - a) * t; } function normalizeAngle(a) { const twoPi = Math.PI * 2; return ((((a + Math.PI) % twoPi) + twoPi) % twoPi) - Math.PI; } function rotatePointXYZ(p, rx, ry, rz = 0) { const cosY = Math.cos(ry), sinY = Math.sin(ry); const cosX = Math.cos(rx), sinX = Math.sin(rx); const cosZ = Math.cos(rz), sinZ = Math.sin(rz); let x = p.x, y = p.y, z = p.z; // Y let x1 = x * cosY + z * sinY; let z1 = -x * sinY + z * cosY; // X let y2 = y * cosX - z1 * sinX; let z2 = y * sinX + z1 * cosX; // Z let x3 = x1 * cosZ - y2 * sinZ; let y3 = x1 * sinZ + y2 * cosZ; return { x: x3, y: y3, z: z2 }; } function fibonacciSphere(count, radius) { const pts = []; const goldenRatio = (1 + Math.sqrt(5)) / 2; for (let i = 0; i < count; i++) { const theta = (2 * Math.PI * i) / goldenRatio; const phi = Math.acos(1 - (2 * i) / count); pts.push({ x: radius * Math.sin(phi) * Math.cos(theta), y: radius * Math.sin(phi) * Math.sin(theta), z: radius * Math.cos(phi), }); } return pts; } function generateRings(count, size) { const pts = []; const outerRingCount = Math.ceil(count * 0.6); const innerRingCount = count - outerRingCount; const outerRadius = size * 0.9; const innerRadius = size * 0.6; // Outer ring (slightly tilted around X) const outerTilt = Math.PI * 0.04; for (let i = 0; i < outerRingCount; i++) { const angle = (i / outerRingCount) * Math.PI * 2; let x = Math.cos(angle) * outerRadius; let y = 0; let z = Math.sin(angle) * outerRadius; // tilt around X const cy = Math.cos(outerTilt), sy = Math.sin(outerTilt); let y2 = y * cy - z * sy; let z2 = y * sy + z * cy; pts.push({ x, y: y2, z: z2 }); } // Inner ring (steeper tilt + slight Y rotation) const innerTilt = innerRingTiltX; const yRot = innerRingYawOffset; for (let i = 0; i < innerRingCount; i++) { const angle = (i / innerRingCount) * Math.PI * 2 + Math.PI * 0.3; let x = Math.cos(angle) * innerRadius; let y = 0; let z = Math.sin(angle) * innerRadius; // tilt around X const cix = Math.cos(innerTilt), six = Math.sin(innerTilt); let y2 = y * cix - z * six; let z2 = y * six + z * cix; // small Y rotation const cy = Math.cos(yRot), sy = Math.sin(yRot); let x3 = x * cy + z2 * sy; let z3 = -x * sy + z2 * cy; pts.push({ x: x3, y: y2, z: z3 }); } return pts; } function setResponsiveScale() { const maxSize = Math.min(container.clientWidth, container.clientHeight); pxPerUnit = (maxSize * 0.4) / baseRadiusUnits; } async function preloadIcons(urls) { await Promise.all( urls.map( (src) => new Promise((resolve) => { const img = new Image(); img.onload = img.onerror = resolve; img.decoding = "async"; img.referrerPolicy = "no-referrer"; img.src = src; }) ) ); } function rebuildDots() { const count = isSpherical ? particleCountSphere : particleCountRings; spherePositions = fibonacciSphere(count, baseRadiusUnits); ringPositions = generateRings(count, baseRadiusUnits * 1.2); scene.innerHTML = ""; dotElements = []; for (let i = 0; i < count; i++) { const el = document.createElement("div"); el.className = "dot"; const src = globalIcons[i % globalIcons.length]; el.style.backgroundImage = `url("${src}")`; scene.appendChild(el); dotElements.push(el); } } function toggleShape() { isSpherical = !isSpherical; targetMorphProgress = isSpherical ? 0 : 1; aboutHero.classList.toggle("is-rings", !isSpherical); aboutHero.classList.toggle("is-spherical", isSpherical); toggleBtn.setAttribute("aria-pressed", String(!isSpherical)); // Dampen momentum so morph doesn’t spin many turns momentum.x *= 0.3; momentum.y *= 0.3; ringMomentum.innerX *= 0.3; ringMomentum.innerY *= 0.3; ringMomentum.outerY *= 0.3; // Normalize all angles to nearest equivalent orientation rotX = normalizeAngle(rotX); rotY = normalizeAngle(rotY); innerRingUserRotX = normalizeAngle(innerRingUserRotX); innerRingUserRotY = normalizeAngle(innerRingUserRotY); outerRingUserYaw = normalizeAngle(outerRingUserYaw); outerRingRotation = normalizeAngle(outerRingRotation); innerRingRotation = normalizeAngle(innerRingRotation); const maxInnerPitch = Math.PI * 0.5; innerRingUserRotX = Math.max( -maxInnerPitch, Math.min(maxInnerPitch, innerRingUserRotX) ); rebuildDots(); } function handlePointerDown(x, y) { isDragging = true; prevMouse.x = x; prevMouse.y = y; dragVelocity.x = 0; dragVelocity.y = 0; container.style.cursor = "grabbing"; } function handlePointerMove(x, y) { if (!isDragging) return; const dx = x - prevMouse.x; const dy = y - prevMouse.y; dragVelocity.x = dx * dragSensitivity; dragVelocity.y = dy * dragSensitivity; if (morphProgress > 0.1) { // Ring mode: // - Inner ring reacts to both axes innerRingUserRotY += dx * ringDragSensitivityX; // inner yaw (Y) innerRingUserRotX += dy * ringDragSensitivityY; // inner pitch (X) outerRingUserYaw -= dx * ringDragSensitivityX; // outer spin (Y only) } else { rotY += dragVelocity.x; rotX -= dragVelocity.y; } prevMouse.x = x; prevMouse.y = y; } function handlePointerUp() { if (!isDragging) return; isDragging = false; if (morphProgress > 0.1) { // Ring mode momentum ringMomentum.innerY = dragVelocity.x * 1.5; ringMomentum.innerX = dragVelocity.y * 1.5; ringMomentum.outerY = -dragVelocity.x * 1.2; } else { momentum.y = dragVelocity.x * 1.5; momentum.x = -dragVelocity.y * 1.5; } container.style.cursor = "grab"; } function attachPointer() { container.addEventListener("mousedown", (e) => handlePointerDown(e.clientX, e.clientY) ); container.addEventListener("mousemove", (e) => handlePointerMove(e.clientX, e.clientY) ); container.addEventListener("mouseup", handlePointerUp); container.addEventListener("mouseleave", handlePointerUp); const passive = { passive: true }; container.addEventListener( "touchstart", (e) => { const t = e.touches[0]; handlePointerDown(t.clientX, t.clientY); }, passive ); container.addEventListener( "touchmove", (e) => { if (e.touches.length) { const t = e.touches[0]; handlePointerMove(t.clientX, t.clientY); } }, passive ); container.addEventListener("touchend", handlePointerUp); container.style.cursor = "grab"; } function render() { requestAnimationFrame(render); rotX = normalizeAngle(rotX); rotY = normalizeAngle(rotY); innerRingUserRotX = normalizeAngle(innerRingUserRotX); innerRingUserRotY = normalizeAngle(innerRingUserRotY); outerRingUserYaw = normalizeAngle(outerRingUserYaw); outerRingRotation = normalizeAngle(outerRingRotation); innerRingRotation = normalizeAngle(innerRingRotation); // Morph progress easing if (Math.abs(morphProgress - targetMorphProgress) > 0.001) { morphProgress += (targetMorphProgress - morphProgress) * morphSpeed; } // Auto spin if (morphProgress > 0.1) { outerRingRotation += outerRingSpeed * (morphProgress * morphProgress); innerRingRotation += innerRingSpeed * (morphProgress * morphProgress); } // Momentum if (!isDragging) { if (morphProgress > 0.1) { // Inner ring momenta if (Math.abs(ringMomentum.innerY) > minMomentum) { innerRingUserRotY += ringMomentum.innerY; ringMomentum.innerY *= momentumDecay; } else { ringMomentum.innerY = 0; } if (Math.abs(ringMomentum.innerX) > minMomentum) { innerRingUserRotX += ringMomentum.innerX; ringMomentum.innerX *= momentumDecay; } else { ringMomentum.innerX = 0; } // Outer ring X momentum only if (Math.abs(ringMomentum.outerY) > minMomentum) { outerRingUserYaw += ringMomentum.outerY; ringMomentum.outerY *= momentumDecay; } else { ringMomentum.outerY = 0; } } else { // Sphere mode if (Math.abs(momentum.y) > minMomentum) { momentum.y *= momentumDecay; if (Math.abs(momentum.y) < baseRotationSpeed * 2) { momentum.y += (baseRotationSpeed - momentum.y) * 0.02; } } else { momentum.y = baseRotationSpeed; } if (Math.abs(momentum.x) > minMomentum) { momentum.x *= momentumDecay; } else { momentum.x = 0; } rotY += momentum.y; rotX += momentum.x; } } // Project and draw const count = dotElements.length; const outerCount = Math.ceil(count * 0.6); for (let i = 0; i < count; i++) { const el = dotElements[i]; const spherePos = spherePositions[i]; const baseRingPos = ringPositions[i]; let ringPos; if (i < outerCount) { // OUTER RING: auto Y revolution + user X pitch only const baseAng = Math.atan2(baseRingPos.z, baseRingPos.x); const radius = Math.hypot(baseRingPos.x, baseRingPos.z); const angle = baseAng + outerRingRotation + outerRingUserYaw; // add user yaw let x = Math.cos(angle) * radius, y = 0, z = Math.sin(angle) * radius; // Generation tilt (around X) const t = Math.PI * 0.04; (ct = Math.cos(t)), (st = Math.sin(t)); let y2 = y * ct - z * st, z2 = y * st + z * ct; ringPos = { x, y: y2, z: z2 }; // Apply user pitch (X) only ringPos = { x, y: y2, z: z2 }; } else { // INNER RING: auto Y revolution + user yaw (Y) + user pitch (X) const innerIdx = i - outerCount; const innerCount = count - outerCount; const innerRadius = baseRadiusUnits * 1.2 * 0.6; const originalAngle = (innerIdx / innerCount) * Math.PI * 2 + Math.PI * 0.3; let x = Math.cos(originalAngle + innerRingRotation + innerRingUserRotY) * innerRadius; let y = 0; let z = Math.sin(originalAngle + innerRingRotation + innerRingUserRotY) * innerRadius; // Generation tilt (X) const t = innerRingTiltX, ct = Math.cos(t), st = Math.sin(t); let y2 = y * ct - z * st, z2 = y * st + z * ct; // Small fixed Y rotation from generation const yR = innerRingYawOffset, cy = Math.cos(yR), sy = Math.sin(yR); let x3 = x * cy + z2 * sy, z3 = -x * sy + z2 * cy; ringPos = { x: x3, y: y2, z: z3 }; // User pitch (X) ringPos = rotatePointXYZ(ringPos, innerRingUserRotX, 0, 0); } // Interpolate sphere->ring const tMorph = clamp01(morphProgress); const baseX = lerp(spherePos.x, ringPos.x, tMorph); const baseY = lerp(spherePos.y, ringPos.y, tMorph); const baseZ = lerp(spherePos.z, ringPos.z, tMorph); // Blend out sphere global rotation as rings take over const smooth = tMorph * tMorph * (3 - 2 * tMorph); // smoothstep const sphereBlend = 1 - smooth; const final = rotatePointXYZ( { x: baseX, y: baseY, z: baseZ }, rotX * sphereBlend, rotY * sphereBlend, 0 ); const xpx = final.x * pxPerUnit; const ypx = final.y * pxPerUnit; const zpx = final.z * pxPerUnit; // “Depth†brightness and scaling const maxDepthUnits = baseRadiusUnits * 1.4; const normDepth = clamp01( (final.z + maxDepthUnits) / (maxDepthUnits * 2) ); const brightness = minBrightness + (maxBrightness - minBrightness) * normDepth; const sizeScale = 2 + 0.75 * normDepth; const baseDotPx = window.innerWidth > 991 ? 20 : 12; const dotPx = baseDotPx * sizeScale; el.style.transform = `translate3d(${xpx}px, ${ypx}px, ${zpx}px)`; // no scale el.style.width = `${dotPx}px`; el.style.height = `${dotPx}px`; el.style.backgroundSize = `100% 100%`; el.style.filter = `brightness(${brightness})`; // el.style.opacity = 0.85 * (0.5 + 0.5 * normDepth); } } function onResize() { setResponsiveScale(); } async function init() { await preloadIcons(globalIcons); setResponsiveScale(); aboutHero.classList.add("is-spherical"); rebuildDots(); attachPointer(); toggleBtn.addEventListener("click", toggleShape); window.addEventListener("resize", onResize); render(); } init(); })(); let teamSection = aboutPage.find(".team-section_outer"); let teamSphere = aboutPage.find(".team-section"); let teamMembersWrap = aboutPage.find(".team-members_wrap"); let teamMembersList = aboutPage.find(".team-members_list"); let teamMembersItem = aboutPage.find(".team-members_item"); let threeStickyTl = gsap.timeline({ defaults: { ease: "linear", }, scrollTrigger: { trigger: teamSection, start: "clamp(top top)", end: "clamp(bottom bottom)", pin: teamSphere, scrub: true, pinSpacing: false, }, }); ScrollTrigger.create({ trigger: teamMembersWrap, start: "clamp(top top)", end: "clamp(bottom bottom)", endTrigger: teamSection, pin: teamMembersList, pinSpacing: false, scrub: true, }); const tl = gsap.timeline({ scrollTrigger: { trigger: teamMembersWrap, start: "clamp(top bottom)", end: "clamp(bottom bottom)", endTrigger: teamSection, scrub: true, }, }); tl.fromTo( teamMembersItem, { y: 1 * window.innerHeight }, { y: -1 * window.innerHeight, duration: 1, stagger: 0.01, ease: CustomEase.create( "custom", "M0,0 C0,0 0,0.5 0.5,0.5 1,0.5 1,1 1,1" ), }, "step" ); // threeStickyTl.to(teamSphere, { // y: "50vh", // scrollTrigger: { // trigger: teamSphere, // start: "clamp(top center)", // end: "clamp(bottom top)", // scrub: true, // }, // }); // onDesktop(() => { // threeStickyTl.to(teamSphere, { // y: "50vh", // scrollTrigger: { // trigger: teamSphere, // start: "clamp(top center)", // end: "clamp(bottom top)", // scrub: true, // }, // }); // }); // onMobile(() => { // threeStickyTl.to(teamSphere, { // scrollTrigger: { // trigger: aboutDivider, // start: "clamp(bottom top)", // end: "clamp(bottom top)", // endTrigger: teamSphere, // pin: teamSphere, // // markers: true, // }, // }); // }); // threeStickyTl.fromTo( // teamMembersWrap, // { // yPercent: 50, // }, // { // yPercent: -50, // }, // "<" // ); //Our Mission let missionSection = aboutPage.find(".mission-section"); let missionSticky = aboutPage.find(".mission-sticky_wrap"); let missionImgWrap = aboutPage.find(".mission-imgs_outer"); let missionImgs = missionImgWrap.find(".mission-img"); let missionImgsEl = missionImgWrap.find(".mission-imgs_wrap"); let missionTitle = aboutPage.find(".mission-info_wrap"); let missionGreyText = aboutPage.find(".mission-title"); let missionClipImgs = missionImgWrap.find(".mission-img_clip"); let missionGrowTl = gsap.timeline({ defaults: { ease: "linear", }, scrollTrigger: { trigger: missionSection, start: "clamp(top 80%)", end: "clamp(bottom bottom)", scrub: true, }, }); missionGrowTl.from(missionImgWrap, { height: "110svh", }); ScrollTrigger.create({ trigger: missionSticky, start: "top top", end: "bottom top", pin: true, endTrigger: missionTitle, scrub: true, // markers: true, pinSpacing: false, }); let missionClipTl = gsap.timeline({ defaults: { ease: "linear", }, scrollTrigger: { trigger: missionSticky, start: "top top", end: "bottom bottom", endTrigger: missionSection, scrub: true, // pinSpacing: false, }, }); missionClipTl.fromTo( missionClipImgs, { clipPath: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)" }, { clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", stagger: 0.5, } ); missionClipTl.fromTo( missionImgsEl, { clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", }, { clipPath: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)", } ); missionClipTl.fromTo( missionGreyText, { yPercent: 0, }, { yPercent: -50, } ); }); } function globalScripts() { $("img").attr("loading", "auto"); $("img, video").each(function () { $(this).on("contextmenu", function (e) { e.preventDefault(); return false; }); }); lenis.resize(); lenis.start(); ScrollTrigger.refresh(); onMobile(() => { lenis.options.syncTouch = true; lenis.options.syncTouchLerp = 0.075; lenis.options.touchInertiaExponent = 1.7; lenis.options.touchMultiplier = 1; }); // let isAnimationPlaying = false; function updateUKTime() { $("[data-uk-time]").each(function () { const ukTime = new Date().toLocaleString("en-GB", { timeZone: "Europe/London", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, }); $(this).text(ukTime + " (GMT)"); }); } function initUKTimeDisplay() { // Update immediately updateUKTime(); // Update every second setInterval(updateUKTime, 1000); } // Initialise the UK time display initUKTimeDisplay(); function popupHandlers() { $("[popup-component]").each(function () { const $component = $(this); const $trigger = $component.find("[popup-trig]"); const $closeBtn = $component.find("[popup-close]"); const $popupEl = $component.find("[popup-el]"); // Check if this is a credit toggle const isCreditToggle = $trigger.hasClass("credit-toggle"); $trigger.on("click", function (e) { // e.preventDefault(); e.stopPropagation(); $component.toggleClass("show-popup"); // Handle credit toggle body class if (isCreditToggle) { if ($component.hasClass("show-popup")) { $("body").addClass("credits-reveal"); onMobile(() => { lenis.stop(); }); } else { $("body").removeClass("credits-reveal"); onMobile(() => { lenis.start(); }); } } }); $closeBtn.on("click", function (e) { // e.preventDefault(); e.stopPropagation(); $component.removeClass("show-popup"); // Remove credits class if this was a credit toggle if (isCreditToggle) { $("body").removeClass("credits-reveal"); lenis.start(); } }); $popupEl.on("click", function (e) { e.stopPropagation(); }); $component.on("click", function (e) { if (e.target === this) { $component.removeClass("show-popup"); // Remove credits class if this was a credit toggle if (isCreditToggle) { $("body").removeClass("credits-reveal"); lenis.start(); } } }); }); $(document).on("click", function (e) { if (!$(e.target).closest("[popup-component]").length) { const $openPopups = $("[popup-component].show-popup"); // Check if any closing popups are credit toggles $openPopups.each(function () { const $trigger = $(this).find("[popup-trig]"); if ($trigger.hasClass("credit-toggle")) { $("body").removeClass("credits-reveal"); lenis.start(); } }); $openPopups.removeClass("show-popup"); } }); $(document).on("keydown", function (e) { if (e.key === "Escape") { const $openPopups = $("[popup-component].show-popup"); // Check if any closing popups are credit toggles $openPopups.each(function () { const $trigger = $(this).find("[popup-trig]"); if ($trigger.hasClass("credit-toggle")) { $("body").removeClass("credits-reveal"); } }); $openPopups.removeClass("show-popup"); } }); } popupHandlers(); function themeToggle() { $(".ld-toggle").on("click", function () { let pageMain = $("body"); if (!pageMain.attr("theme-mode")) { pageMain.attr("theme-mode", "dark"); } else { pageMain.removeAttr("theme-mode"); } }); } themeToggle(); $(".page-main").each(function () { let pageEl = $(this); pageEl.removeAttr("nav-scrolled"); let targets = pageEl.find("[data-cursor]"); let xOffset = 20; let yOffset = 10; let cursorIsOnRight = false; let currentTarget = null; let lastText = ""; let cursorItem = pageEl.find(".cursor"); let cursorImg = cursorItem.find(".cursor-img"); let ctaCursorWrap = pageEl.find("[cta-cursor_wrap]"); let cursorParagraph = cursorItem.find(".cursor-text"); let isHoveringSwiper = false; let currentSwiperEl = null; let cursorInner = cursorItem.find("[cursor-item]"); onDesktop(() => { if (cursorItem.length) { gsap.set(cursorItem, { xPercent: xOffset, yPercent: yOffset }); let xTo = gsap.quickTo(cursorItem[0], "x", { duration: 0.1, ease: "power3", }); let yTo = gsap.quickTo(cursorItem[0], "y", { ease: "power3", duration: 0.1, }); $(window).on("mouseover mousemove", function (e) { let windowWidth = $(window).width(); let scrollY = $(window).scrollTop(); let cursorX = e.clientX; let cursorY = e.clientY + scrollY; let xPercent = xOffset; let yPercent = yOffset; if (cursorX > windowWidth * 0.9) { cursorIsOnRight = true; } else { cursorIsOnRight = false; } $("body").toggleClass("on-right", cursorIsOnRight); if (isHoveringSwiper && currentSwiperEl) { const rect = currentSwiperEl[0].getBoundingClientRect(); const relativeX = e.clientX - rect.left; const halfWidth = rect.width / 2; const newText = relativeX < halfWidth ? "Prev" : "Next"; if (newText !== lastText) { cursorParagraph.html(newText); lastText = newText; } } else if (currentTarget) { let newText = currentTarget.attr("data-cursor"); if (newText !== lastText) { cursorParagraph.html(newText); lastText = newText; } } gsap.to(cursorItem, { xPercent: xPercent, yPercent: yPercent, duration: 0.1, ease: "power3", }); xTo(cursorX); yTo(cursorY - scrollY); }); targets.each(function () { let target = $(this); target.on("mouseleave mouseout", function () { $("body").removeClass("c-vis"); }); target.on("mouseover mousemove", function () { $("body").addClass("c-vis"); currentTarget = target; let newText = target.is("[data-easteregg]") ? target.attr("data-easteregg") : target.attr("data-cursor"); if (newText !== lastText) { cursorParagraph.html(newText); lastText = newText; } }); }); } }); $("[slider-section]").each(function (index) { let swiperEl = $(this).find(".swiper"); const projSlider = new Swiper(swiperEl[0], { autoplay: false, centeredSlides: true, simulateTouch: false, loop: true, runCallbacksOnInit: true, touchMoveStopPropagation: true, mousewheel: { forceToAxis: true, }, speed: 800, spaceBetween: 20, breakpoints: { 320: { slidesPerView: 1.2, spaceBetween: 10, }, 992: { slidesPerView: 1.4, }, }, }); swiperEl.on("mousemove mouseover", function () { isHoveringSwiper = true; currentSwiperEl = $(this); cursorItem.addClass("active"); }); swiperEl.on("mouseleave mouseout", function () { isHoveringSwiper = false; currentSwiperEl = null; cursorItem.removeClass("active"); lastText = ""; }); swiperEl.on("click", function (e) { const rect = this.getBoundingClientRect(); const clickX = e.clientX - rect.left; const elementWidth = rect.width; const halfWidth = elementWidth / 2; if (clickX < halfWidth) { projSlider.slidePrev(); } else { projSlider.slideNext(); } }); }); ScrollTrigger.create({ trigger: $(this), start: "top top", end: "+1000px", onLeave: ({ self }) => { pageEl.attr("nav-detail", ""); }, onEnterBack: ({ self }) => { pageEl.removeAttr("nav-detail"); }, }); let previousScroll = 0; let upwardScrollDistance = 0; lenis.on("scroll", ({ scroll, direction }) => { if (direction === 1) { pageEl.attr("nav-scrolled", ""); upwardScrollDistance = 0; // Reset upward distance when scrolling down } else if (direction === -1) { upwardScrollDistance += previousScroll - scroll; if (upwardScrollDistance > 50) { pageEl.removeAttr("nav-scrolled"); } } previousScroll = scroll; }); $("[video-player]").each(function (index) { let playVidBtn = $(this).find("[video-trigger]"); let videoPopupEl = $(this).find(".video-popup"); let videoItem = $(this).find(".plyr_component"); let videoBg = $(this).find(".video-popup_bg"); let videoClose = $(this).find("[video-close]"); let vidPreviewTrigger = $(this).find("[vid-preview_trigger]"); let vidPreview = $(this).find(".nav-video_preview"); let vidPreviewClose = $(this).find(".vid-preview_close"); let vidPlayInner = $(this).find(".video-inner_play"); let thisComponent = $(this); const controls = `