/** * Testimonial Slider СЃ эффектом Cube Рё синхронизированными CMS-табами */ /** * Testimonial Slider with Cube effect and synced CMS tabs */ document.addEventListener("DOMContentLoaded", () => { const tabs = document.querySelectorAll(".testimonials-tab"); const sliderContainer = document.querySelector(".testimonials-slider"); const tabsContainer = document.querySelector(".testimonial-slider-tabs"); const section = document.querySelector(".section-testimonials"); if (!tabs.length || !sliderContainer) return; if (tabsContainer) { tabsContainer.classList.add("is-initialized"); } const AUTOPLAY_DELAY = 8000; if (tabsContainer) { tabsContainer.style.setProperty("--autoplay-delay", AUTOPLAY_DELAY + "ms"); } const mqMobile = window.matchMedia("(max-width: 479px)"); const mqDesktop = window.matchMedia("(min-width: 480px)"); const mqTabsMobile = mqMobile; const tabsWrapper = document.querySelector(".testimonials-tabs-wrapper"); // ---- Desktop collapse polish: clip the avatar + measure exact text width ---- // 1) Wrap the avatar in an overflow:hidden clip so it never squishes. function wrapProfileImages() { tabs.forEach((tab) => { const img = tab.querySelector(".profile-image"); if (!img) return; if (img.parentElement && img.parentElement.classList.contains("pf-clip")) return; const clip = document.createElement("span"); clip.className = "pf-clip"; img.parentNode.insertBefore(clip, img); clip.appendChild(img); }); } // 2) Measure real content width so the collapse has no "dead zone". function measureTabs() { if (!mqDesktop.matches) return; tabs.forEach((tab) => { // avatar natural width const img = tab.querySelector(".profile-image"); if (img) { const iw = img.offsetWidth || 38; tab.style.setProperty("--img-max", iw + "px"); } // text-container natural width (ignore current max-width clamp) const text = tab.querySelector(".text-container"); if (text) { const prevMax = text.style.maxWidth; const prevTrans = text.style.transition; text.style.transition = "none"; text.style.maxWidth = "none"; const w = Math.ceil(text.scrollWidth) + 2; text.style.maxWidth = prevMax; text.getBoundingClientRect(); // reflow so restore lands before transition returns text.style.transition = prevTrans; tab.style.setProperty("--text-max", w + "px"); } }); } wrapProfileImages(); measureTabs(); // Re-measure once fonts are ready (fallback font gives a wrong width otherwise) if (document.fonts && document.fonts.ready) { document.fonts.ready.then(measureTabs); } let _measureT; window.addEventListener("resize", () => { clearTimeout(_measureT); _measureT = setTimeout(measureTabs, 150); }); // --------------------------------------------------------------------------- function centerTab(tab) { if (!tabsWrapper) return; const wrapperRect = tabsWrapper.getBoundingClientRect(); const tabRect = tab.getBoundingClientRect(); const tabOffsetInWrapper = tabsWrapper.scrollLeft + (tabRect.left - wrapperRect.left); const target = tabOffsetInWrapper - (tabsWrapper.clientWidth - tab.offsetWidth) / 2; tabsWrapper.scrollTo({ left: target, behavior: "smooth" }); } function resetTabFiller(tab) { const filler = tab.querySelector(".tab-filler"); if (!filler) return; // restart the keyframes animation from 0 filler.style.animation = "none"; filler.getBoundingClientRect(); // force reflow filler.style.animation = ""; } function updateActiveTab(activeIndex) { tabs.forEach((tab, index) => { if (index === activeIndex) { resetTabFiller(tab); tab.classList.add("is-active"); if (mqTabsMobile.matches) { centerTab(tab); } } else { tab.classList.remove("is-active"); } }); } const testimonialSwiper = new Swiper(".testimonials-slider", { effect: "cube", grabCursor: true, speed: 600, loop: false, cubeEffect: { shadow: false, slideShadows: true, shadowOffset: 20, shadowScale: 0.94, }, autoplay: { delay: AUTOPLAY_DELAY, disableOnInteraction: false, waitForTransition: false, }, on: { init: function () { updateActiveTab(this.realIndex); // Do not autoplay until the section is in view; IntersectionObserver starts it this.autoplay.stop(); }, slideChange: function () { updateActiveTab(this.realIndex); }, }, }); // Desktop: hover over the testimonial pauses autoplay; leaving resumes it // from where it stopped (not from the beginning). if (sliderContainer) { sliderContainer.addEventListener("mouseenter", () => { if (mqMobile.matches) return; // desktop only if (testimonialSwiper.autoplay && testimonialSwiper.autoplay.pause) { testimonialSwiper.autoplay.pause(); } if (tabsContainer) tabsContainer.classList.add("is-paused"); }); sliderContainer.addEventListener("mouseleave", () => { if (mqMobile.matches) return; if (testimonialSwiper.autoplay && testimonialSwiper.autoplay.resume) { testimonialSwiper.autoplay.resume(); } if (tabsContainer) tabsContainer.classList.remove("is-paused"); }); } // Robust autoplay start for Swiper 8: after autoplay.stop() the internal // `running` flag can stay out of sync, so a plain start() silently no-ops. // Reset the flag and re-apply params before starting. function startAutoplay() { const ap = testimonialSwiper.autoplay; if (!ap) return; ap.running = false; if (testimonialSwiper.params && testimonialSwiper.params.autoplay) { testimonialSwiper.params.autoplay.disableOnInteraction = false; } ap.start(); } function stopAutoplay() { if (testimonialSwiper.autoplay) testimonialSwiper.autoplay.stop(); } if (section) { const autoplayObserver = new IntersectionObserver( (entries) => { const entry = entries[0]; if (entry.isIntersecting) { updateActiveTab(testimonialSwiper.realIndex); startAutoplay(); } else { stopAutoplay(); } }, { threshold: 0, rootMargin: "-5% 0px -5% 0px" } ); autoplayObserver.observe(section); } tabs.forEach((tab, index) => { tab.addEventListener("click", () => { testimonialSwiper.slideTo(index); }); }); // Swipe on the tabs bar (mobile) -> controls the slider if (tabsWrapper) { let touchStartX = 0; let touchStartY = 0; let isSwiping = false; tabsWrapper.addEventListener( "touchstart", (e) => { touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; isSwiping = false; }, { passive: true } ); tabsWrapper.addEventListener( "touchmove", (e) => { const dx = e.touches[0].clientX - touchStartX; const dy = e.touches[0].clientY - touchStartY; if (!isSwiping && Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 8) { isSwiping = true; } if (isSwiping) e.preventDefault(); }, { passive: false } ); tabsWrapper.addEventListener( "touchend", (e) => { if (!isSwiping) return; const dx = e.changedTouches[0].clientX - touchStartX; if (Math.abs(dx) < 40) return; if (dx < 0) { testimonialSwiper.slideNext(); } else { testimonialSwiper.slidePrev(); } }, { passive: true } ); } }); // (function () { // var audio = new Audio('https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/click-sound-1.wav'); // audio.preload = 'auto'; // audio.volume = 0.05; // function playHoverSound() { // var clone = audio.cloneNode(); // clone.volume = audio.volume; // clone.play().catch(function () {}); // } // function isHoverable(el) { // if (!el || el === document.body || el === document.documentElement) return false; // if (el.matches('a, button, input, select, textarea, [role="button"], [onclick], [data-w-id]')) return true; // return getComputedStyle(el).cursor === 'pointer'; // } // function findOutermostHoverable(el) { // var found = null; // while (el && el !== document.body && el !== document.documentElement) { // if (isHoverable(el)) found = el; // el = el.parentElement; // } // return found; // } // document.addEventListener('mouseover', function (e) { // var el = findOutermostHoverable(e.target); // if (!el) return; // if (el.contains(e.relatedTarget)) return; // playHoverSound(); // }); // })(); (function () { function stabilizeTeamImages() { var teamCms = document.querySelector(".team-cms"); if (!teamCms) return; function doStabilize() { teamCms.querySelectorAll(".team-image").forEach(function (img) { img.loading = "eager"; img.decoding = "async"; img.setAttribute("fetchpriority", "high"); if (img.decode && !img.complete) { img.decode().catch(function () {}); } img.addEventListener( "load", function () { var swiperRoot = img.closest(".team-cms"); if (swiperRoot && swiperRoot.swiper && swiperRoot.swiper.update) { swiperRoot.swiper.update(); } }, { once: true } ); }); } // Откладываем eager-загрузку РґРѕ момента РєРѕРіРґР° секция близко Рє viewport new IntersectionObserver( function (entries, obs) { if (!entries[0].isIntersecting) return; obs.disconnect(); doStabilize(); }, { rootMargin: "400px" } ).observe(teamCms); } function initFooterVideoLazyLoad() { var isMobile = window.matchMedia("(max-width: 767px)").matches; var activeMobileVideos = []; function pauseExtraMobileVideos(currentVideo) { if (!isMobile) return; activeMobileVideos = activeMobileVideos.filter(function (video) { return video !== currentVideo && !video.paused; }); activeMobileVideos.push(currentVideo); while (activeMobileVideos.length > 2) { activeMobileVideos.shift().pause(); } } function loadVideo(video) { if (video.dataset.videoLoaded === "true") return; video .querySelectorAll("source[data-lazy-src]") .forEach(function (source) { source.src = source.dataset.lazySrc; }); video.dataset.videoLoaded = "true"; video.load(); } document.querySelectorAll("video.footer-video").forEach(function (video) { var sources = video.querySelectorAll("source[src]"); if (!sources.length || video.dataset.videoPrepared === "true") return; video.dataset.videoPrepared = "true"; video.preload = "none"; video.removeAttribute("autoplay"); sources.forEach(function (source) { source.dataset.lazySrc = source.getAttribute("src"); source.removeAttribute("src"); }); video.load(); new IntersectionObserver( function (entries) { var entry = entries[0]; if (!entry.isIntersecting) { if (isMobile) video.pause(); return; } loadVideo(video); var playPromise = video.play(); if (playPromise && playPromise.catch) playPromise.catch(function () {}); pauseExtraMobileVideos(video); }, { rootMargin: isMobile ? "120px" : "400px" } ).observe(video); }); } function initRuntimeStabilityFixes() { stabilizeTeamImages(); initFooterVideoLazyLoad(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initRuntimeStabilityFixes); } else { initRuntimeStabilityFixes(); } })(); (function () { const list = document.querySelector(".team-wrapper") || document.querySelector(".swiper-wrapper"); if (!list) return; const teamBlock = list.closest(".team-block") || document.querySelector(".team-block"); const viewport = document.querySelector(".team-cms"); if (!teamBlock || !viewport) return; teamBlock.style.position = "relative"; list.style.position = "relative"; const mqMobile = window.matchMedia("(max-width: 767px)"); const items = Array.from(list.querySelectorAll(".team-item, .swiper-slide")); const floater = teamBlock.querySelector(".team-floating-text") || document.createElement("div"); floater.className = "team-floating-text"; floater.setAttribute("aria-hidden", "true"); let fName = floater.querySelector(".team-name-text:not(.role-text)"); let fRole = floater.querySelector(".team-name-text.role-text"); if (!fName) { fName = document.createElement("div"); fName.className = "team-name-text"; floater.appendChild(fName); } if (!fRole) { fRole = document.createElement("div"); fRole.className = "team-name-text role-text"; floater.appendChild(fRole); } function setText(item) { const wrap = item.querySelector(".team-name-wrapper"); if (!wrap) return; const name = wrap.querySelector(".team-name-text:not(.role-text)"); const role = wrap.querySelector(".role-text"); fName.textContent = name ? name.textContent : ""; fRole.textContent = role ? role.textContent : ""; } /* ---------- ДЕСКТОП: ховер + слайд ---------- */ let hovered = null, curX = 0, tgtX = 0, visible = false, raf = null; const EASE = 0.22; function targetFor(item) { const image = item.querySelector(".team-image-wrapper") || item.querySelector(".team-image"); if (!image) return { x: 0, y: 0 }; const br = teamBlock.getBoundingClientRect(); const ir = image.getBoundingClientRect(); const centerX = ir.left + ir.width / 2 - br.left - teamBlock.clientLeft; return { x: centerX - floater.offsetWidth / 2, y: ir.top - br.top - teamBlock.clientTop, }; } function tick() { if (hovered) tgtX = targetFor(hovered).x; curX += (tgtX - curX) * EASE; floater.style.transform = `translate(${curX}px)`; const settled = Math.abs(tgtX - curX) < 0.2; raf = hovered || !settled ? requestAnimationFrame(tick) : null; } function startLoop() { if (!raf) raf = requestAnimationFrame(tick); } items.forEach((item) => { item.addEventListener("mouseenter", () => { if (mqMobile.matches) return; setText(item); const t = targetFor(item); if (!visible) { curX = t.x; tgtX = t.x; floater.style.transform = `translate(${curX}px)`; floater.classList.add("is-visible"); visible = true; } hovered = item; startLoop(); }); }); viewport.addEventListener("mouseleave", () => { if (mqMobile.matches) return; hovered = null; visible = false; floater.classList.remove("is-visible"); }); window.addEventListener("resize", () => { if (!mqMobile.matches && hovered) tgtX = targetFor(hovered).x; }); /* ---------- МОБИЛКА: слайдер ---------- */ let swiperTeam = null; let teamVisibilityObserver = null; function initTeamSwiper() { if (swiperTeam) return; hovered = null; if (raf) { cancelAnimationFrame(raf); raf = null; } floater.style.transform = ""; floater.classList.add("is-visible"); const updateActiveSlide = (swiperInstance) => { const allSlides = swiperInstance.el.querySelectorAll( ".team-item, .swiper-slide" ); const activeSlide = swiperInstance.slides[swiperInstance.activeIndex]; allSlides.forEach((el) => { el.classList.toggle("is-active", el === activeSlide); }); if (activeSlide) setText(activeSlide); }; swiperTeam = new Swiper(".team-cms", { slidesPerView: "auto", centeredSlides: true, speed: 450, loop: true, loopAdditionalSlides: 2, roundLengths: true, watchSlidesProgress: true, observer: true, observeParents: true, spaceBetween: 0, slidesOffsetBefore: 2, autoplay: { delay: 1400, disableOnInteraction: false, }, on: { init() { updateActiveSlide(this); }, slideChange() { updateActiveSlide(this); }, }, }); } function destroyTeamSwiper() { if (!swiperTeam) return; swiperTeam.destroy(true, true); swiperTeam = null; list.style.transform = ""; floater.classList.remove("is-visible"); } mqMobile.addEventListener("change", () => { if (mqMobile.matches) { // переключились РЅР° мобилку — запускаем наблюдатель заново observeTeamSection(); } else { if (teamVisibilityObserver) { teamVisibilityObserver.disconnect(); teamVisibilityObserver = null; } destroyTeamSwiper(); } }); function observeTeamSection() { if (!mqMobile.matches) return; if (teamVisibilityObserver) teamVisibilityObserver.disconnect(); const obs = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { initTeamSwiper(); if (swiperTeam && swiperTeam.autoplay) swiperTeam.autoplay.start(); } else if (swiperTeam && swiperTeam.autoplay) { swiperTeam.autoplay.stop(); } }, { rootMargin: "0px" } ); obs.observe(viewport); teamVisibilityObserver = obs; } if (mqMobile.matches) { observeTeamSection(); } })(); //////////// (function () { var IMAGES = [ { shape: "ver", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f13e8abdd281dd625b2_Ver_Jupiter.avif", }, { shape: "hor", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/WEBM/Hor_Rayo.webm", type: "video/webm", }, { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/HEVC/Hor_Rayo.mov", type: "video/mp4", }, ], }, { shape: "ver", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f134943e3f91ac7b6a1_Ver_Silo.avif", }, { shape: "ver", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f1372d8cb2563e74ee4_Ver_Sui.avif", }, { shape: "hor", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/WEBM/Hor_Subquery.webm", type: "video/webm", }, { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/HEVC/Hor_Subquery.mov", type: "video/mp4", }, ], }, { shape: "ver", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f13d29f330c1094ab70_Ver_Palliora.avif", }, { shape: "sqr", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/WEBM/Sqr_Babydoge.webm", type: "video/webm", }, { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/HEVC/Sqr_Babydoge.mov", type: "video/mp4", }, ], }, { shape: "sqr", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/WEBM/Sqr_Chaingpt.webm", type: "video/webm", }, { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/HEVC/Sqr_Chaingpt.mov", type: "video/mp4", }, ], }, { shape: "ver", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f13cc2ae2ca02ca5568_Ver_Shio_3.avif", }, { shape: "sqr", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f129d66fc71dc52388f_Sqr_Jupiter_2.avif", }, { shape: "sqr", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/WEBM/Sqr_Jup.webm", type: "video/webm", }, { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/HEVC/Sqr_Jup.mov", type: "video/mp4", }, ], }, { shape: "sqr", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f1232acdbaac20aded5_Sqr_PlayAI.avif", }, { shape: "sqr", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/WEBM/Sqr_Neuron_1.webm", type: "video/webm", }, { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/HEVC/Sqr_Neuron_1.mov", type: "video/mp4", }, ], }, { shape: "sqr", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f1290f0aa68cdb4d81e_Sqr_UNCX.avif", }, { shape: "sqr", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f12870521c621d2945b_Sqr_Neuron_2.avif", }, { shape: "sqr", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f129c954bcdf83a5edf_Sqr_Echo_2.avif", }, { shape: "sqr", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/WEBM/Sqr_SQDGN.webm", type: "video/webm", }, { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/HEVC/Sqr_SQDGN.mov", type: "video/mp4", }, ], }, { shape: "sqr", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/WEBM/Sqr_Shift.webm", type: "video/webm", }, { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/HEVC/Sqr_Shift.mov", type: "video/mp4", }, ], }, { shape: "sqr", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f12d1be1e4e353b0bbc_Sqr_Cetus_2.avif", }, { shape: "sqr", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/WEBM/Sqr_Shio.webm", type: "video/webm", }, { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20Alpha/HEVC/Sqr_Shio.mov", type: "video/mp4", }, ], }, { shape: "hor", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f10c518c15a0970fc77_Hor_Jupiter.avif", }, { shape: "hor", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20MP4_HEVC/Hor_Exeos.mp4", type: "video/mp4", }, ], }, { shape: "hor", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20MP4_HEVC/Hor_Ledger.mp4", type: "video/mp4", }, ], }, { shape: "hor", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20MP4_HEVC/Hor_MegaETH.mp4", type: "video/mp4", }, ], }, { shape: "hor", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f10d189ee780fbe68e7_Hor_Bucket.avif", }, { shape: "hor", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20MP4_HEVC/Hor_Mysten.mp4", type: "video/mp4", }, ], }, { shape: "hor", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f1029da39d7648c5d3e_Hor_Prim3.avif", }, { shape: "hor", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20MP4_HEVC/Hor_concent.mp4", type: "video/mp4", }, ], }, { shape: "hor", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f0f985611af07cd3d5c_Hor_Mean.avif", }, { shape: "ver", type: "video", sources: [ { src: "https://pub-58acd61005124a0abff7ca5eb1adbe37.r2.dev/Videos%20MP4_HEVC/Ver_Up10.mp4", type: "video/mp4", }, ], }, { shape: "sqr", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a28068904be48cde74a73d1_Sqr.avif", }, { shape: "hor", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a28068953d87493635d484d_Hor-2.avif", }, { shape: "hor", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a280689073426755ba0fa42_Hor.avif", }, { shape: "ver", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a280689bdb2fd150759f99d_Ver-1.avif", }, { shape: "hor", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a280689120ae281ed9121c7_Hor-3.avif", }, { shape: "hor", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a28068917e3d75881daa536_Hor-1.avif", }, { shape: "hor", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a28068966adc7259a5752c7_Hor-5.avif", }, { shape: "ver", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a2806888b0ce59761c38b9f_Ver.avif", }, { shape: "hor", src: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a2806885c2b229c15ab93bb_Hor-4.avif", }, ]; var isMobile = window.innerWidth <= 479; var isSmallMobile = window.innerWidth <= 479; var isSafari = /^((?!chrome|android|crios|fxios|edgios).)*safari/i.test( navigator.userAgent ); // МОБИЛКА: видео-тайлы заменяем РЅР° статичные картинки (заранее подготовленные // кадры-постеры). Safari теперь обрабатывается через blob URL + canvas. if (isMobile) { var MOBILE_IMAGE_OVERRIDES = { 1: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a2fe644c674429ec1644d8c_rayo-mob.avif", 4: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a2fe87818dc38cb81fb358b_subquery-mob.avif", 6: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a29342f8c3835393c10fc80_babydodge-preloader.avif", 7: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a29342f35f7347a564f99b0_chain-preloader.avif", 10: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a29342e66483cbc4dd9f484_jupiter-preloader.avif", 12: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a2fe9676005bc6d440174b9_neuron-mob.avif", 16: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a2feb3397ae8984c79d265d_SQDGN-mob.avif", 17: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a2feab2613697fd2db00c93_shift-mob.avif", 19: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a2feb0e8ab0f651e619a169_shio-mob.avif", 21: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a2fe5cfc657f4d8810a9263_exeos-mob.avif", 22: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a2fe6123e5daf114675f1a9_ledger-mob.avif", 23: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a2fe60a38eae8639ccee40b_megaeth-mob.avif", 25: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a29342fdb67812c3f9b7e36_mysten-preloader.avif", 27: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a2fe5ab04806db476e0bee4_frame_08.avif", 29: "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a2feb618ab0f651e619c7bf_ver-mob.avif", }; Object.keys(MOBILE_IMAGE_OVERRIDES).forEach(function (idx) { idx = +idx; IMAGES[idx] = { shape: IMAGES[idx].shape, src: MOBILE_IMAGE_OVERRIDES[idx], }; }); } var SPHERE_RADIUS = isMobile ? 160 : 300; var TILE_RADIUS = 1.5; var TILE_SIZES = { hor: { w: isMobile ? 78 : 107, h: isMobile ? 59 : 80 }, sqr: { w: isMobile ? 68 : 92, h: isMobile ? 68 : 92 }, ver: { w: isMobile ? 59 : 80, h: isMobile ? 78 : 107 }, }; var SPIN_SPEED = 0.0025; var HOVER_SCALE = 1.08; var EXPLODE_DUR = 1500; var BEND_AMOUNT = 0.18; var TEXT_FONT_SIZE = "2.3rem"; var TEXT_COLOR = "#000"; var FLIP_SOURCES = [4, 8, 27, 11, 7]; var FLIP_IMAGE_OVERRIDES = [ "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a1d2f1372d8cb2563e74ee4_Ver_Sui.avif", "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a29342f35f7347a564f99b0_chain-preloader.avif", "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a29342fdb67812c3f9b7e36_mysten-preloader.avif", "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a29342e66483cbc4dd9f484_jupiter-preloader.avif", "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a29342f8c3835393c10fc80_babydodge-preloader.avif", ]; var FLIP_FALLBACKS = [4, 10, 26, 10, 10]; var FLIP_CARD_SIZE = 200; var FLIP_CARD_RADIUS = 1.5; // px — скругление углов карточки прелоадера var FLIP_CARD_GAP = 5; var TEXT_INIT_NUDGE = 0; var TEXT_PER_FLIP = 10; var CARD_START_DELAY = 1000; var MOBILE_TEXT_HOLD = 100; // РјСЃ — сколько текст РІРёСЃРёС‚ РЅР° мобилке перед растворением var MOBILE_DISSOLVE_DUR = 400; // РјСЃ — длительность растворения текста РЅР° мобилке var CARD_APPEAR_DUR = "0.5s"; var CARD_APPEAR_EASE = "cubic-bezier(0.6, 0, 0, 1)"; var CARD_FLIP_DUR = "1s"; var CARD_FLIP_EASE = "cubic-bezier(0.8, 0, 0.2, 1)"; var NEXT_FLIP_DELAY = 120; var PRELOADER_MEDIA_TIMEOUT = 7000; var LOADED_EXIT_CARD_COUNT = 3; var END_DUR = "1.5s"; var END_EASE = "cubic-bezier(1, 0, 0.2, 1)"; var MOBILE_PERCENT_OUT_DUR = "0.3s"; var NAVBAR_SLIDE_FROM = "-120%"; var NAVBAR_SLIDE_DUR = "0.7s"; var NAVBAR_SLIDE_EASE = "cubic-bezier(0.3,0,0,1)"; // МОБИЛКА: облегчаем сферу — убираем часть тайлов if (isMobile) { var MOBILE_REMOVE = [4, 8, 14, 15, 16, 17, 18, 20, 21, 23, 24, 31, 34]; // flip-источники, которые удаляются РёР· сферы в†’ замены РёР· оставшихся var FLIP_SUBSTITUTES = { 4: 3, 8: 9 }; // 4(Subquery)в†’3(Ver_Sui), 8(Shio)в†’9(Sqr_Jupiter_2) var removeSet = {}; MOBILE_REMOVE.forEach(function (i) { removeSet[i] = true; }); var indexMap = {}; var filtered = []; IMAGES.forEach(function (item, i) { if (removeSet[i]) return; indexMap[i] = filtered.length; filtered.push(item); }); IMAGES = filtered; var remapIdx = function (oldIdx) { if (removeSet[oldIdx]) oldIdx = FLIP_SUBSTITUTES[oldIdx]; return indexMap[oldIdx]; }; FLIP_SOURCES = FLIP_SOURCES.map(remapIdx); FLIP_FALLBACKS = FLIP_FALLBACKS.map(remapIdx); } function easeOutExpo(t) { return t >= 1 ? 1 : 1 - Math.pow(2, -10 * t); } function easeOutQuint(t) { return 1 - Math.pow(1 - t, 5); } var root = document.getElementById("sphere-embed-root"); function resetPageScrollTop() { try { if ("scrollRestoration" in history) history.scrollRestoration = "manual"; } catch (e) {} try { var prevBehavior = document.documentElement.style.scrollBehavior; document.documentElement.style.scrollBehavior = "auto"; window.scrollTo(0, 0); document.documentElement.scrollTop = 0; document.body.scrollTop = 0; document.documentElement.style.scrollBehavior = prevBehavior; } catch (e) {} } resetPageScrollTop(); setTimeout(resetPageScrollTop, 0); setTimeout(resetPageScrollTop, 50); setTimeout(resetPageScrollTop, 250); window.addEventListener("pageshow", resetPageScrollTop); window.addEventListener("load", resetPageScrollTop); requestAnimationFrame(resetPageScrollTop); function getRootSize() { var r = root.getBoundingClientRect(); return { w: r.width > 4 ? r.width : root.offsetWidth || window.innerWidth, h: r.height > 4 ? r.height : root.offsetHeight || window.innerHeight, }; } var sz = getRootSize(); var W = sz.w, H = sz.h; var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(60, W / H, 1, 2000); camera.position.z = 520; // МОБИЛКА: легче рендерер — без antialias Рё preserveDrawingBuffer, pixelRatio ≤ 1.5. // Это сильно снижает потребление памяти GPU (РѕРґРЅР° РёР· причин крашей вкладки). var renderer = new THREE.WebGLRenderer( isMobile ? { antialias: false, alpha: true, powerPreference: "low-power" } : { antialias: true, alpha: true, preserveDrawingBuffer: true } ); renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.5 : 2)); renderer.setSize(W, H); renderer.domElement.style.display = "block"; root.appendChild(renderer.domElement); var group = new THREE.Group(); scene.add(group); var loader = new THREE.TextureLoader(); loader.setCrossOrigin("anonymous"); function makeTileMaterial(dirVec, tW, tH) { return new THREE.ShaderMaterial({ transparent: true, side: THREE.DoubleSide, depthWrite: false, uniforms: { map: { value: null }, opacity: { value: 0.0 }, hasMap: { value: 0.0 }, radius: { value: TILE_RADIUS / Math.min(tW, tH) }, tileSize: { value: new THREE.Vector2(tW, tH) }, tileDir: { value: dirVec.clone() }, bendAmt: { value: BEND_AMOUNT }, }, vertexShader: [ "uniform vec3 tileDir; uniform float bendAmt; varying vec2 vUv;", "void main() { vUv = uv; vec3 pos = position; vec2 uvC = uv - 0.5;", " float bend = bendAmt * (uvC.y*uvC.y); pos += tileDir * bend * 18.0;", " gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); }", ].join("\n"), fragmentShader: [ "uniform sampler2D map; uniform float opacity; uniform float hasMap;", "uniform float radius; uniform vec2 tileSize; varying vec2 vUv;", "float roundedBox(vec2 uv, float r) {", " vec2 q = abs(uv-0.5)-(vec2(0.5)-vec2(r));", " float d = length(max(q,0.0))+min(max(q.x,q.y),0.0)-r;", " return 1.0-smoothstep(-0.002,0.002,d); }", "void main() {", " float r = radius*min(tileSize.x,tileSize.y)/max(tileSize.x,tileSize.y);", " float mask = roundedBox(vUv,r); if(mask<0.01) discard;", " vec4 color = hasMap>0.5 ? texture2D(map,vUv) : vec4(0.0,0.0,0.0,0.0);", " gl_FragColor = vec4(color.rgb, color.a*opacity*mask); }", ].join("\n"), }); } var N = IMAGES.length, tiles = []; for (var i = 0; i < N; i++) { var y2 = 1 - (i / Math.max(1, N - 1)) * 2, r2 = Math.sqrt(Math.max(0, 1 - y2 * y2)); var theta = Math.PI * (3 - Math.sqrt(5)) * i; var dir = new THREE.Vector3( Math.cos(theta) * r2, y2, Math.sin(theta) * r2 ).normalize(); var tSz = TILE_SIZES[IMAGES[i].shape] || TILE_SIZES.hor; var mat = makeTileMaterial(dir, tSz.w, tSz.h); var geo = new THREE.PlaneGeometry(tSz.w, tSz.h, 8, 6); var mesh = new THREE.Mesh(geo, mat); mesh.userData.dir = dir; mesh.userData.tileW = tSz.w; mesh.userData.tileH = tSz.h; mesh.userData.appearT = -1; mesh.userData.hoverScale = 1.0; mesh.userData.bendCurrent = 0.0; mesh.scale.setScalar(0); group.add(mesh); tiles.push(mesh); } var loadedCount = 0, videoElements = [], videoTextures = []; var percentTarget = 0, percentDisplay = 0; var previewStarted = false; var allLoadedFlag = false; function playAllVideos() { videoElements.forEach(function (v) { v.play().catch(function () {}); }); } document.addEventListener("click", playAllVideos, { once: true }); document.addEventListener("touchstart", playAllVideos, { once: true }); var FLIP_COUNT_OFFSET = FLIP_SOURCES.length; function markLoaded(i) { tiles[i].userData.appearT = performance.now(); loadedCount++; var shown = Math.max(0, loadedCount - FLIP_COUNT_OFFSET); var totalEx = N - FLIP_COUNT_OFFSET; percentTarget = Math.min(100, Math.round((shown / totalEx) * 100)); if (loadedCount >= N) { allLoadedFlag = true; loadingDone = true; // ассеты СѓР¶Рµ готовы; РЅРµ ждем easing процента, чтобы РЅРµ запускать лишний РєСЂСѓРі flip-РѕРІ percentTarget = 100; percentDisplay = 100; if (percentEl) percentEl.textContent = "100%"; startPreviewSequence(); } } function loadOneTile(i) { var item = IMAGES[i]; if (item.type === "video") { var vid = document.createElement("video"); vid.crossOrigin = "anonymous"; vid.muted = true; vid.loop = true; vid.playsInline = true; vid.setAttribute("playsinline", ""); var mp4src = null, webmsrc = null; item.sources.forEach(function (s) { if (s.type.indexOf("webm") !== -1) webmsrc = s.src; else mp4src = s.src; }); var tv = document.createElement("video"); if (isSafari) { // Safari: prefer .mov (HEVC with alpha), canvas drawImage preserves HEVC alpha vid.src = mp4src || webmsrc || item.sources[0].src; } else { vid.src = webmsrc && tv.canPlayType("video/webm") !== "" ? webmsrc : mp4src || item.sources[0].src; } var vidTex; if (isSafari) { var safariCanvas = document.createElement("canvas"); safariCanvas.width = 2; safariCanvas.height = 2; vid._safariCanvas = safariCanvas; vid._safariCtx = safariCanvas.getContext("2d"); vid.addEventListener("loadedmetadata", function () { safariCanvas.width = vid.videoWidth || 2; safariCanvas.height = vid.videoHeight || 2; }); vidTex = new THREE.CanvasTexture(safariCanvas); vidTex.minFilter = THREE.LinearFilter; vidTex.magFilter = THREE.LinearFilter; } else { vidTex = new THREE.VideoTexture(vid); vidTex.minFilter = THREE.LinearFilter; vidTex.magFilter = THREE.LinearFilter; vidTex.format = THREE.RGBAFormat; } tiles[i].material.uniforms.map.value = vidTex; tiles[i].material.uniforms.hasMap.value = 1.0; tiles[i].material.needsUpdate = true; videoElements.push(vid); videoTextures.push(vidTex); var fired = false; function onReady() { if (fired) return; fired = true; vid.play().catch(function () {}); markLoaded(i); } vid.addEventListener("canplay", onReady, { once: true }); vid.addEventListener("error", function () { if (!fired) { fired = true; markLoaded(i); } }); setTimeout(function () { if (!fired) onReady(); }, 4000); vid.load(); } else { loader.load( item.src, function (tex) { tex.minFilter = THREE.LinearFilter; tex.magFilter = THREE.LinearFilter; tiles[i].material.uniforms.map.value = tex; tiles[i].material.uniforms.hasMap.value = 1.0; tiles[i].material.needsUpdate = true; markLoaded(i); }, undefined, function () { markLoaded(i); } ); } } var FLIP_TILE_SET = {}; FLIP_SOURCES.forEach(function (idx) { FLIP_TILE_SET[idx] = true; }); function loadRemainingTiles() { for (var idx = 0; idx < N; idx++) { if (!FLIP_TILE_SET[idx]) loadOneTile(idx); } } FLIP_SOURCES.forEach(function (idx) { loadOneTile(idx); }); // ========== DOM РћР’Р•РЛЕЙ ========== var textOverlay, textLine1, textLine2, cardWrap, cardInner, faceFront, faceBack; var mobileLogo, mobileTextWrap, mobileCardWrap; if (isMobile) { // --- МОБИЛЬНЫЙ ФЛОУ --- textOverlay = document.createElement("div"); textOverlay.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;pointer-events:none;z-index:10;overflow:hidden;"; // Лого сверху РЅРµ добавляем РЅР° маленькой мобилке: там остается обычный navbar. if (!isSmallMobile) { mobileLogo = document.createElement("img"); mobileLogo.src = "https://cdn.prod.website-files.com/64f5d4cc10cb604f0982d896/6a28fd80afc9db849bd09d56_relate.svg"; mobileLogo.style.cssText = "position:absolute;top:3.125rem;left:50%;transform:translateX(-50%);height:1.35rem;width:auto;display:block;z-index:12;"; } // Текст РІ РґРІРµ строки РїРѕ центру mobileTextWrap = document.createElement("div"); mobileTextWrap.style.cssText = "position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);text-align:center;font-size:2rem;line-height:100%;letter-spacing:-0.08rem;color:" + TEXT_COLOR + ";opacity:1;transition:none;white-space:nowrap;"; mobileTextWrap.innerHTML = "design studio
for the new internet"; // Контейнер карточки Р С—Р С• центру (изначально РїСѓСЃС‚/скрыт) mobileCardWrap = document.createElement("div"); mobileCardWrap.style.cssText = "position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:" + FLIP_CARD_SIZE + "px;height:" + FLIP_CARD_SIZE + "px;perspective:1200px;opacity:0;"; cardInner = document.createElement("div"); cardInner.style.cssText = "position:absolute;left:50%;top:50%;width:" + FLIP_CARD_SIZE + "px;height:" + FLIP_CARD_SIZE + "px;transform:translate(-50%,-50%) rotateY(90deg);transform-style:preserve-3d;transition:none;"; faceFront = document.createElement("div"); faceFront.style.cssText = "position:absolute;width:100%;height:100%;backface-visibility:hidden;-webkit-backface-visibility:hidden;display:flex;align-items:center;justify-content:center;"; faceBack = document.createElement("div"); faceBack.style.cssText = "position:absolute;width:100%;height:100%;backface-visibility:hidden;-webkit-backface-visibility:hidden;display:flex;align-items:center;justify-content:center;transform:rotateY(180deg);"; cardInner.appendChild(faceFront); cardInner.appendChild(faceBack); mobileCardWrap.appendChild(cardInner); if (mobileLogo) textOverlay.appendChild(mobileLogo); textOverlay.appendChild(mobileTextWrap); textOverlay.appendChild(mobileCardWrap); // Заглушки чтобы остальной РєРѕРґ Р Р…Р Вµ падал (десктопные textLine1/2 Р Р…Р Вµ используются Р Р…Р В° РјРѕР±) textLine1 = mobileTextWrap; textLine2 = mobileTextWrap; cardWrap = mobileCardWrap; } else { // --- ДЕСКТОПНЫЙ ФЛОУ --- textOverlay = document.createElement("div"); textOverlay.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;gap:" + FLIP_CARD_GAP + "px;pointer-events:none;z-index:10;overflow:hidden;transform:translateX(-1rem);"; textLine1 = document.createElement("div"); textLine1.textContent = "relate\u2014design studio"; textLine1.style.cssText = "font-size:" + TEXT_FONT_SIZE + ";line-height:1.1;white-space:nowrap;letter-spacing:-0.04em;opacity:1;transform:translateX(0px);transition:none;color:" + TEXT_COLOR + ";flex-shrink:0;margin-right:0px;"; cardWrap = document.createElement("div"); cardWrap.style.cssText = "position:relative;display:inline-block;width:0px;height:0px;overflow:visible;flex-shrink:0;vertical-align:middle;perspective:1200px;transition:width " + CARD_APPEAR_DUR + " " + CARD_APPEAR_EASE + ",height " + CARD_APPEAR_DUR + " " + CARD_APPEAR_EASE + ";"; cardInner = document.createElement("div"); cardInner.style.cssText = "position:absolute;left:50%;top:50%;width:" + FLIP_CARD_SIZE + "px;height:" + FLIP_CARD_SIZE + "px;transform:translate(-50%,-50%) rotateY(90deg);transform-style:preserve-3d;transition:none;"; faceFront = document.createElement("div"); faceFront.style.cssText = "position:absolute;width:100%;height:100%;backface-visibility:hidden;-webkit-backface-visibility:hidden;display:flex;align-items:center;justify-content:center;"; faceBack = document.createElement("div"); faceBack.style.cssText = "position:absolute;width:100%;height:100%;backface-visibility:hidden;-webkit-backface-visibility:hidden;display:flex;align-items:center;justify-content:center;transform:rotateY(180deg);"; cardInner.appendChild(faceFront); cardInner.appendChild(faceBack); cardWrap.appendChild(cardInner); textLine2 = document.createElement("div"); textLine2.textContent = "for the new internet"; textLine2.style.cssText = "font-size:" + TEXT_FONT_SIZE + ";line-height:1.1;white-space:nowrap;letter-spacing:-0.04em;opacity:1;transform:translateX(0px);transition:none;color:" + TEXT_COLOR + ";flex-shrink:0;margin-left:0px;"; textOverlay.appendChild(textLine1); textOverlay.appendChild(cardWrap); textOverlay.appendChild(textLine2); } var percentEl = document.createElement("div"); percentEl.style.cssText = "position:absolute;left:50%;bottom:3rem;transform:translateX(-50%);font-size:1rem;line-height:100%;letter-spacing:-0.02rem;opacity:0.4;color:#000;z-index:20;pointer-events:none;"; percentEl.textContent = "0%"; root.appendChild(percentEl); root.appendChild(textOverlay); var keepNavbarVisible = isMobile; var headerEls = Array.prototype.slice.call( document.querySelectorAll("#header, .new-header-wrapper") ); var headerShown = keepNavbarVisible; if (isMobile) { headerEls.forEach(function (el) { el.style.transform = "translateY(28px)"; el.style.transition = "none"; }); } else if (!keepNavbarVisible) { headerEls.forEach(function (el) { el.style.transform = "translateY(" + NAVBAR_SLIDE_FROM + ")"; el.style.transition = "none"; }); } var smallMenuEl = null, smallMenuShown = false; function getSmallMenuEl() { if (!smallMenuEl) smallMenuEl = document.querySelector(".small-menu"); return smallMenuEl; } if (isMobile && getSmallMenuEl()) { smallMenuEl.style.transform = "translateY(130%)"; smallMenuEl.style.opacity = "1"; smallMenuEl.style.visibility = "visible"; smallMenuEl.style.transition = "none"; } try { document.documentElement.style.overflow = "hidden"; document.body.style.overflow = "hidden"; } catch (e) {} function revealSmallMenu() { var menuEl = getSmallMenuEl(); if (!isMobile || !menuEl || smallMenuShown) return; smallMenuShown = true; menuEl.style.display = ""; menuEl.style.visibility = "visible"; menuEl.style.opacity = "1"; menuEl.style.pointerEvents = ""; menuEl.style.transform = "translateY(130%)"; menuEl.style.transition = "none"; void menuEl.offsetWidth; menuEl.style.transition = "transform 0.6s ease"; menuEl.style.transform = "translateY(0%)"; headerEls.forEach(function (el) { el.style.transition = "transform 0.6s ease"; el.style.transform = "translateY(0)"; }); } function revealNavbar() { if (headerShown || !headerEls.length) { try { document.documentElement.style.overflow = ""; document.body.style.overflow = ""; } catch (e) {} return; } headerShown = true; void headerEls[0].offsetWidth; headerEls.forEach(function (el) { el.style.transition = "transform " + NAVBAR_SLIDE_DUR + " " + NAVBAR_SLIDE_EASE; el.style.transform = "translateY(0)"; }); try { document.documentElement.style.overflow = ""; document.body.style.overflow = ""; } catch (e) {} } // ========== МЕДИА ДЛЯ РџР•РЕВОРРћРўРћР’ ========== function buildMediaEl(imgIdx, flipIdx) { var item = IMAGES[imgIdx], el; if (FLIP_IMAGE_OVERRIDES[flipIdx]) { el = document.createElement("img"); el.src = FLIP_IMAGE_OVERRIDES[flipIdx]; el.crossOrigin = "anonymous"; el.style.cssText = "max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;display:block;border-radius:" + FLIP_CARD_RADIUS + "px;"; } else if (item.type === "video") { el = document.createElement("video"); el.muted = true; el.loop = true; el.playsInline = true; el.setAttribute("playsinline", ""); el.style.cssText = "max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;display:block;border-radius:" + FLIP_CARD_RADIUS + "px;"; item.sources.forEach(function (s) { var src = document.createElement("source"); src.src = s.src; src.type = s.type; el.appendChild(src); }); el.load(); } else { el = document.createElement("img"); el.src = item.src; el.crossOrigin = "anonymous"; el.style.cssText = "max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;display:block;border-radius:" + FLIP_CARD_RADIUS + "px;"; } return el; } function buildFallbackMediaEl(flipIdx) { var fallbackIdx = FLIP_FALLBACKS[flipIdx]; var fallbackItem = IMAGES[fallbackIdx] || IMAGES[FLIP_SOURCES[flipIdx]]; var el = document.createElement("img"); el.src = FLIP_IMAGE_OVERRIDES[flipIdx] || fallbackItem.src || ""; el.crossOrigin = "anonymous"; el.style.cssText = "max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;display:block;border-radius:" + FLIP_CARD_RADIUS + "px;"; return el; } function buildEmptyFallbackEl() { var el = document.createElement("div"); el.setAttribute("data-preloader-ready", "true"); el.style.cssText = "width:100%;height:100%;display:block;border-radius:" + FLIP_CARD_RADIUS + "px;background:#f2f2f2;"; return el; } var flipMediaEls = []; var flipMediaReady = []; var flipMediaFallbackUsed = []; var preloaderMediaReady = false; var preloaderBootReady = false; var preloaderStartDelayDone = false; var remainingTilesStarted = false; for (var fsi = 0; fsi < FLIP_SOURCES.length; fsi++) { (function (idx) { var el = buildMediaEl(FLIP_SOURCES[idx], idx); flipMediaEls.push(el); flipMediaReady.push(false); flipMediaFallbackUsed.push(false); var item = IMAGES[FLIP_SOURCES[idx]]; if (el.tagName && el.tagName.toLowerCase() === "video") { el.addEventListener("canplay", checkPreloaderMediaReady); el.addEventListener("canplaythrough", checkPreloaderMediaReady); el.addEventListener("error", function () { usePreloaderFallback(idx); }); setTimeout(function () { if (!isFlipMediaDisplayReady(idx)) usePreloaderFallback(idx); }, PRELOADER_MEDIA_TIMEOUT); } else { if (el.complete && el.naturalWidth > 0) { flipMediaReady[idx] = true; } else if (el.complete) { usePreloaderFallback(idx); } else { el.addEventListener("load", checkPreloaderMediaReady); el.addEventListener("error", function () { usePreloaderFallback(idx); }); } } })(fsi); } function usePreloaderFallback(idx) { if (flipMediaReady[idx] || flipMediaFallbackUsed[idx]) return; flipMediaFallbackUsed[idx] = true; var currentEl = flipMediaEls[idx]; if (currentEl && currentEl.pause) { try { currentEl.pause(); } catch (e) {} currentEl.removeAttribute("src"); try { currentEl.load(); } catch (e) {} } var fallbackEl = buildFallbackMediaEl(idx); if (currentEl && currentEl.parentNode) currentEl.parentNode.replaceChild(fallbackEl, currentEl); flipMediaEls[idx] = fallbackEl; if (fallbackEl.complete && fallbackEl.naturalWidth > 0) { flipMediaReady[idx] = true; checkPreloaderMediaReady(); } else { fallbackEl.addEventListener( "load", function () { flipMediaReady[idx] = true; checkPreloaderMediaReady(); }, { once: true } ); fallbackEl.addEventListener( "error", function () { var emptyEl = buildEmptyFallbackEl(); if (fallbackEl.parentNode) fallbackEl.parentNode.replaceChild(emptyEl, fallbackEl); flipMediaEls[idx] = emptyEl; flipMediaReady[idx] = true; checkPreloaderMediaReady(); }, { once: true } ); } } function checkPreloaderMediaReady() { if (preloaderMediaReady) return; for (var k = 0; k < FLIP_SOURCES.length; k++) { flipMediaReady[k] = isFlipMediaDisplayReady(k); if (!flipMediaReady[k]) return; } preloaderMediaReady = true; if (!remainingTilesStarted) { remainingTilesStarted = true; loadRemainingTiles(); } maybeStartPreloaderCards(); } // Карточки прелоадера стартуют только после паузы Р С‘ полной готовности 5 медиа. setTimeout(function () { preloaderStartDelayDone = true; maybeStartPreloaderCards(); }, CARD_START_DELAY); checkPreloaderMediaReady(); // ========== ЛОГИКА РџР•РЕВОРРћРўРћР’ ========== var currentAngle = 90; var flipCounter = 0; var totalFlips = 0; var loadingDone = false; var cardAppeared = false; var textNudgeX = 0; var introPhase = "text"; var previewPhase = "text"; var explodeStart = -1; var flipInProgress = false; var introFinishing = false; preloaderBootReady = true; maybeStartPreloaderCards(); function setNudge(px) { textNudgeX = px; if (isMobile) return; // Р Р…Р В° мобилке текст растворяется, Р Р…Р Вµ двигается textLine1.style.transition = "transform " + CARD_FLIP_DUR + " " + CARD_FLIP_EASE; textLine1.style.transform = "translateX(-" + px + "px)"; textLine2.style.transition = "transform " + CARD_FLIP_DUR + " " + CARD_FLIP_EASE; textLine2.style.transform = "translateX(" + px + "px)"; } function isFlipMediaDisplayReady(idx) { var el = flipMediaEls[idx]; if (!el) return false; if (el.getAttribute && el.getAttribute("data-preloader-ready") === "true") return true; if (el.tagName && el.tagName.toLowerCase() === "video") return el.readyState >= 2; return el.complete && el.naturalWidth > 0; } function waitForFlipMediaDisplayReady(idx, callback) { if (isFlipMediaDisplayReady(idx)) { callback(); return; } function check() { if (isFlipMediaDisplayReady(idx)) callback(); else requestAnimationFrame(check); } requestAnimationFrame(check); } function getHiddenFace() { return totalFlips % 2 === 0 ? faceBack : faceFront; } function placeOnHidden(srcIdx) { var hf = getHiddenFace(); while (hf.firstChild) hf.removeChild(hf.firstChild); var el = flipMediaEls[srcIdx]; hf.appendChild(el); if (el.play) el.play().catch(function () {}); } function isIntroDoneForExit() { return ( introFinishing || loadingDone || allLoadedFlag || percentTarget >= 100 ); } function getLoadedExitIdx() { return Math.min( FLIP_SOURCES.length - 1, Math.max(0, LOADED_EXIT_CARD_COUNT - 1) ); } function lockFinalCardFrame(finalSrcIdx) { if (typeof finalSrcIdx !== "number") finalSrcIdx = flipCounter; finalSrcIdx = Math.max(0, Math.min(FLIP_SOURCES.length - 1, finalSrcIdx)); var finalEl = flipMediaEls[finalSrcIdx]; if (!finalEl) return; cardInner.style.transition = "none"; while (faceFront.firstChild && faceFront.firstChild !== finalEl) { faceFront.removeChild(faceFront.firstChild); } if (finalEl.parentNode !== faceFront) { if (finalEl.parentNode) finalEl.parentNode.removeChild(finalEl); faceFront.appendChild(finalEl); } while (faceBack.firstChild) faceBack.removeChild(faceBack.firstChild); cardInner.style.transform = "translate(-50%,-50%) rotateY(0deg)"; currentAngle = 0; flipCounter = finalSrcIdx; if (finalEl.play) finalEl.play().catch(function () {}); } function finishIntroAfterLastCard(finalSrcIdx) { if (introFinishing || introPhase === "exploding" || introPhase === "done") return; introFinishing = true; flipInProgress = false; percentTarget = 100; percentDisplay = 100; if (percentEl) percentEl.textContent = "100%"; lockFinalCardFrame(finalSrcIdx); requestAnimationFrame(function () { requestAnimationFrame(function () { startExplode(performance.now()); }); }); } function doFlip() { if ( introFinishing || flipInProgress || previewPhase === "flying" || introPhase === "exploding" || introPhase === "done" ) return; // Если загрузка СѓР¶Рµ завершена Р С‘ РјС‹ СѓР¶Рµ стоим Р Р…Р В° последнем intro-слайде, // Р Р…Р Вµ запускаем wrap 4 -> 0. Это Р С‘ был визуальный loop/скачок назад. if (isIntroDoneForExit() && flipCounter >= getLoadedExitIdx()) { finishIntroAfterLastCard(flipCounter); return; } var nextSrcIdx = (flipCounter + 1) % FLIP_SOURCES.length; waitForFlipMediaDisplayReady(nextSrcIdx, function () { if ( introFinishing || flipInProgress || previewPhase === "flying" || introPhase === "exploding" || introPhase === "done" ) return; // Повторная проверка РЅСѓР¶РЅР°, если загрузка стала 100% РїРѕРєР° РјС‹ ждали media-ready. if (isIntroDoneForExit() && flipCounter >= getLoadedExitIdx()) { finishIntroAfterLastCard(flipCounter); return; } flipInProgress = true; var nextAngle = currentAngle - 180; placeOnHidden(nextSrcIdx); cardInner.style.transition = "transform " + CARD_FLIP_DUR + " " + CARD_FLIP_EASE; cardInner.style.transform = "translate(-50%,-50%) rotateY(" + nextAngle + "deg)"; setNudge(textNudgeX + TEXT_PER_FLIP); var finished = false; var fallbackTimer = null; var flipStartedAt = performance.now(); function finishFlip() { if (finished) return; finished = true; flipInProgress = false; cardInner.removeEventListener("transitionend", onFlipEnd); if (fallbackTimer) clearTimeout(fallbackTimer); currentAngle = nextAngle; flipCounter = nextSrcIdx; totalFlips++; // Важно: первая карточка СѓР¶Рµ показала FLIP_SOURCES[0]. // Поэтому финал должен быть Р Р…Р В° последнем source, без wrap обратно Р Р…Р В° 0. if (isIntroDoneForExit() && nextSrcIdx >= getLoadedExitIdx()) { finishIntroAfterLastCard(nextSrcIdx); } else { setTimeout( doFlip, nextSrcIdx >= FLIP_SOURCES.length - 1 ? 250 : NEXT_FLIP_DELAY ); } } var onFlipEnd = function (e) { if (e.target !== cardInner || e.propertyName !== "transform") return; if (performance.now() - flipStartedAt < 100) return; finishFlip(); }; cardInner.addEventListener("transitionend", onFlipEnd); fallbackTimer = setTimeout( finishFlip, parseFloat(CARD_FLIP_DUR) * 1000 + 120 ); }); } function maybeStartPreloaderCards() { if ( !preloaderBootReady || !preloaderStartDelayDone || !preloaderMediaReady || cardAppeared ) return; showFirstCard(); } function showFirstCard() { if (cardAppeared) return; if (!preloaderMediaReady) { checkPreloaderMediaReady(); return; } flipCounter = 0; totalFlips = 0; flipInProgress = false; waitForFlipMediaDisplayReady(0, function () { if (cardAppeared || !preloaderMediaReady) return; cardAppeared = true; while (faceFront.firstChild) faceFront.removeChild(faceFront.firstChild); faceFront.appendChild(flipMediaEls[0]); if (flipMediaEls[0].play) flipMediaEls[0].play().catch(function () {}); while (faceBack.firstChild) faceBack.removeChild(faceBack.firstChild); if (isMobile) { // МОБИЛКА: текст РІРёСЃРёС‚ MOBILE_TEXT_HOLD, затем растворяется, потом карточка setTimeout(function () { mobileTextWrap.style.transition = "opacity " + MOBILE_DISSOLVE_DUR + "ms ease"; mobileTextWrap.style.opacity = "0"; setTimeout(function () { mobileTextWrap.style.display = "none"; // Карточка появляется (90в†’0) Р Р…Р В° месте текста mobileCardWrap.style.transition = "opacity " + CARD_APPEAR_DUR + " " + CARD_APPEAR_EASE; mobileCardWrap.style.opacity = "1"; cardInner.style.transition = "transform " + CARD_APPEAR_DUR + " " + CARD_APPEAR_EASE; void cardInner.offsetWidth; cardInner.style.transform = "translate(-50%,-50%) rotateY(0deg)"; currentAngle = 0; setTimeout(function () { doFlip(); }, parseFloat(CARD_APPEAR_DUR) * 1000 + 80); }, MOBILE_DISSOLVE_DUR); }, MOBILE_TEXT_HOLD); } else { // ДЕСКТОП: карточка раздвигает текст cardWrap.style.width = FLIP_CARD_SIZE + "px"; cardWrap.style.height = FLIP_CARD_SIZE + "px"; cardInner.style.transition = "transform " + CARD_APPEAR_DUR + " " + CARD_APPEAR_EASE; void cardInner.offsetWidth; cardInner.style.transform = "translate(-50%,-50%) rotateY(0deg)"; currentAngle = 0; textLine1.style.transition = "transform " + CARD_APPEAR_DUR + " " + CARD_APPEAR_EASE; textLine1.style.transform = "translateX(-" + TEXT_INIT_NUDGE + "px)"; textLine2.style.transition = "transform " + CARD_APPEAR_DUR + " " + CARD_APPEAR_EASE; textLine2.style.transform = "translateX(" + TEXT_INIT_NUDGE + "px)"; textNudgeX = TEXT_INIT_NUDGE; setTimeout(function () { doFlip(); }, parseFloat(CARD_APPEAR_DUR) * 1000 + 80); } }); } function startPreviewSequence() { if (previewStarted) return; previewStarted = true; loadingDone = true; maybeStartPreloaderCards(); } if (isMobile) { setTimeout(function () { if (introPhase === "done" || introPhase === "exploding") return; percentTarget = 100; percentDisplay = 100; if (percentEl) percentEl.textContent = "100%"; preloaderMediaReady = true; preloaderStartDelayDone = true; maybeStartPreloaderCards(); setTimeout(function () { if ( introPhase !== "done" && introPhase !== "exploding" && !introFinishing ) { finishIntroAfterLastCard(Math.min(flipCounter, getLoadedExitIdx())); } }, 2500); }, 7000); } var HARD_FAILSAFE_MS = 9000; function forceUnlockScroll() { try { document.documentElement.style.overflow = ""; document.body.style.overflow = ""; } catch (e) {} } setTimeout(function () { if ( introPhase !== "done" && introPhase !== "exploding" && !introFinishing ) { if (!cardAppeared) { try { finishIntroAfterLastCard(0); } catch (e) {} } else { try { finishIntroAfterLastCard(Math.min(flipCounter, getLoadedExitIdx())); } catch (e) {} } } }, HARD_FAILSAFE_MS); setTimeout(function () { forceUnlockScroll(); try { revealNavbar(); } catch (e) {} if (window.ScrollTrigger && ScrollTrigger.refresh) { try { ScrollTrigger.refresh(); } catch (e) {} } }, HARD_FAILSAFE_MS + 800); function startExplode(nowTs) { if (previewPhase === "flying") return; previewPhase = "flying"; if (!isMobile) { revealSmallMenu(); // ДЕСКТОП: текст улетает Р Р† стороны var flyDist = W * 0.6; textLine1.style.transition = "transform " + END_DUR + " " + END_EASE + ", opacity " + END_DUR + " " + END_EASE; textLine1.style.transform = "translateX(calc(-" + flyDist + "px - " + textNudgeX + "px))"; textLine2.style.transition = "transform " + END_DUR + " " + END_EASE + ", opacity " + END_DUR + " " + END_EASE; textLine2.style.transform = "translateX(calc(" + flyDist + "px + " + textNudgeX + "px))"; textLine1.style.opacity = "0"; textLine2.style.opacity = "0"; } else if (mobileLogo) { // МОБИЛКА: лого плавно исчезает mobileLogo.style.transition = "opacity " + END_DUR + " " + END_EASE; mobileLogo.style.opacity = "0"; } var dx = 0; var dy = -H * 0.2; cardInner.style.transition = "transform " + END_DUR + " " + END_EASE + ", opacity " + END_DUR + " " + END_EASE; cardInner.style.transform = "translate(calc(-50% + " + dx + "px), calc(-50% + " + dy + "px)) rotateY(" + currentAngle + "deg) scale(0.3)"; cardInner.style.opacity = "0"; if (percentEl) { percentEl.style.transition = "opacity " + (isMobile ? MOBILE_PERCENT_OUT_DUR : END_DUR) + " " + END_EASE; percentEl.style.opacity = "0"; } if (isMobile) { setTimeout(revealSmallMenu, parseFloat(MOBILE_PERCENT_OUT_DUR) * 1000); } introPhase = "exploding"; explodeStart = performance.now() + 700; setTimeout(function () { textOverlay.style.display = "none"; }, parseFloat(END_DUR) * 1000 + 100); } // ========== РџРђРЈР—Рђ ========== // ФИКС РљРРђРЁРђ Р СњР С’ МОБИЛКЕ: раньше РїСЂРё _paused РІСЃС‘ равно вызывался renderer.render() // каждый кадр через requestAnimationFrame — то есть сфера продолжала рендериться // (жрать GPU/память) даже РєРѕРіРґР° была далеко Р В·Р В° пределами экрана РїСЂРё скролле. // Теперь РїСЂРё паузе rAF-цикл полностью останавливается Р С‘ возобновляется только // РєРѕРіРґР° сфера СЃРЅРѕРІР° попадает Р Р† Р·РѕРЅСѓ видимости. var _paused = false, _autoPaused = false, _manualPaused = false, _rafId = null; function updatePausedState() { var was = _paused; _paused = _autoPaused || _manualPaused; if (was && !_paused) resumeAnimate(); } function setAutoPaused(v) { _autoPaused = v; updatePausedState(); } function setManualPaused(v) { _manualPaused = v; updatePausedState(); } if ("IntersectionObserver" in window) { new IntersectionObserver( function (entries) { if (introPhase !== "done") { setAutoPaused(false); return; } setAutoPaused( !entries.some(function (e) { return e.isIntersecting && e.intersectionRatio > 0; }) ); }, { threshold: 0.01 } ).observe(root); } document.addEventListener("visibilitychange", function () { setAutoPaused(document.visibilityState !== "visible"); }); setAutoPaused(document.visibilityState !== "visible"); // ========== Р’РАЩЕНИЕ ========== var spinSpeed = SPIN_SPEED, t0 = performance.now(); var velX = 0, velY = 0, dragging = false, lastX = 0, lastY = 0; var slow = 1, hovering = false, _hoveredMesh = null; var groupQuat = new THREE.Quaternion(); var axisX = new THREE.Vector3(1, 0, 0), axisY = new THREE.Vector3(0, 1, 0), tmpQ = new THREE.Quaternion(); var raycaster = new THREE.Raycaster(), mouse = new THREE.Vector2(); var groupOffsetY = 0, groupOffsetYTarget = 0, lastScrollY = 0, scrollVel = 0; function getMaxLift() { var fovRad = (camera.fov * Math.PI) / 180; return 2 * camera.position.z * Math.tan(fovRad / 2) * 0.3; } window.addEventListener( "scroll", function () { if (introPhase !== "done") return; var dy = window.scrollY - lastScrollY; lastScrollY = window.scrollY; scrollVel += dy * 0.00035; if (!isMobile) { groupOffsetYTarget = Math.min( getMaxLift(), Math.max(0, window.scrollY * 0.5) ); } }, { passive: true } ); function setPointer(e) { var rect = renderer.domElement.getBoundingClientRect(), p = e.touches ? e.touches[0] : e; mouse.x = ((p.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((p.clientY - rect.top) / rect.height) * 2 + 1; return p; } function pickMesh() { raycaster.setFromCamera(mouse, camera); var hits = raycaster.intersectObjects(tiles, false); return hits.length ? hits[0].object : null; } function down(e) { dragging = true; var p = setPointer(e); lastX = p.clientX; lastY = p.clientY; root.style.cursor = "grabbing"; } function move(e) { var p = setPointer(e); if (dragging) { var dx = p.clientX - lastX, dy = p.clientY - lastY; if (introPhase === "done") { tmpQ.setFromAxisAngle(axisY, dx * 0.005); groupQuat.premultiply(tmpQ); tmpQ.setFromAxisAngle(axisX, dy * 0.005); groupQuat.premultiply(tmpQ); velY = dx * 0.0006; velX = dy * 0.0006; } lastX = p.clientX; lastY = p.clientY; if (e.cancelable) e.preventDefault(); } else { _hoveredMesh = introPhase === "done" ? pickMesh() : null; hovering = !!_hoveredMesh; root.style.cursor = "grab"; } } function up() { dragging = false; root.style.cursor = "grab"; } renderer.domElement.addEventListener("mousedown", down); window.addEventListener("mousemove", move); window.addEventListener("mouseup", up); if (!isMobile) { renderer.domElement.addEventListener("touchstart", down, { passive: true }); renderer.domElement.addEventListener("touchmove", move, { passive: false }); renderer.domElement.addEventListener("touchend", up); } var camWorld = new THREE.Vector3(); // ========== PERF GUARD: Р Р…Р Вµ крутим сферу бесконечно, если устройству тяжело ========== // Меряем средний frame-time через rAF. Если устойчиво держится лаг (РЅРёР·РєРёР№ FPS), // замораживаем холостое вращение Р С‘ пропускаем рендер, РїРѕРєР° юзер Р Р…Р Вµ начнёт // взаимодействовать (hover/drag) — тогда работаем как обычно. var _perfPaused = false; var _frameTimes = []; var _perfBadSince = 0, _perfGoodSince = 0; var PERF_FRAME_WINDOW = 60; var PERF_BAD_MS = 1000 / 24; // средний фрейм дольше ~41РјСЃ (<24fps) — тяжело var PERF_GOOD_MS = 1000 / 50; // средний фрейм короче ~20РјСЃ (>50fps) — РјРѕР¶РЅРѕ вернуть СЃРїРёРЅ var PERF_BAD_HOLD = 1500; // держим лаг столько РјСЃ РїРѕРґСЂСЏРґ, прежде чем поставить Р Р…Р В° паузу var PERF_GOOD_HOLD = 3000; // держим хороший fps столько РјСЃ РїРѕРґСЂСЏРґ, прежде чем снять паузу var _lastFrameTime = performance.now(); function updatePerfPause(now) { var dt = now - _lastFrameTime; _lastFrameTime = now; _frameTimes.push(dt); if (_frameTimes.length > PERF_FRAME_WINDOW) _frameTimes.shift(); if (_frameTimes.length < 10) return; var sum = 0; for (var i = 0; i < _frameTimes.length; i++) sum += _frameTimes[i]; var avg = sum / _frameTimes.length; if (avg > PERF_BAD_MS) { if (!_perfBadSince) _perfBadSince = now; _perfGoodSince = 0; if (!_perfPaused && now - _perfBadSince > PERF_BAD_HOLD) _perfPaused = true; } else { _perfBadSince = 0; if (avg < PERF_GOOD_MS) { if (!_perfGoodSince) _perfGoodSince = now; if (_perfPaused && now - _perfGoodSince > PERF_GOOD_HOLD) _perfPaused = false; } else { _perfGoodSince = 0; } } } function animate() { if (_paused) { _rafId = null; return; } _rafId = requestAnimationFrame(animate); var now = performance.now(); updatePerfPause(now); // Устройству тяжело Р С‘ сфера сейчас Р Р…Р Вµ РЅСѓР¶РЅР° юзеру — замораживаем кадр целиком. if (_perfPaused && introPhase === "done" && !dragging && !hovering) return; for (var vi = 0; vi < videoTextures.length; vi++) { var vel = videoElements[vi]; if (vel && vel._safariCanvas) { if (vel.readyState >= 2 && vel.currentTime !== vel._lastDrawTime) { vel._lastDrawTime = vel.currentTime; if (vel.videoWidth > 0) { if (vel._safariCanvas.width !== vel.videoWidth) { vel._safariCanvas.width = vel.videoWidth; vel._safariCanvas.height = vel.videoHeight; } try { vel._safariCtx.clearRect( 0, 0, vel._safariCanvas.width, vel._safariCanvas.height ); vel._safariCtx.drawImage( vel, 0, 0, vel._safariCanvas.width, vel._safariCanvas.height ); } catch (e) {} videoTextures[vi].needsUpdate = true; } } } else if ( videoTextures[vi].image && videoTextures[vi].image.readyState >= 2 ) { videoTextures[vi].needsUpdate = true; } } if (introPhase === "done") { slow = 1; if (!dragging) { var elapsed = (now - t0) * 0.001; var axisAngle = Math.sin(elapsed * (Math.PI / 9)) * (Math.PI / 3); var spinAxis = new THREE.Vector3( -Math.sin(axisAngle), Math.cos(axisAngle), 0 ).normalize(); scrollVel *= 0.88; tmpQ.setFromAxisAngle(spinAxis, spinSpeed * slow + scrollVel); groupQuat.premultiply(tmpQ); velY *= 0.95; velX *= 0.95; if (Math.abs(velY) > 0.00001) { tmpQ.setFromAxisAngle(axisY, velY * slow); groupQuat.premultiply(tmpQ); } if (Math.abs(velX) > 0.00001) { tmpQ.setFromAxisAngle(axisX, velX * slow); groupQuat.premultiply(tmpQ); } } if (!isMobile) groupOffsetY += (groupOffsetYTarget - groupOffsetY) * 0.5; } group.quaternion.copy(groupQuat); group.position.y = isMobile ? 0 : groupOffsetY; camera.getWorldPosition(camWorld); var allDone = true, rotatedDir = new THREE.Vector3(); for (var i = 0; i < N; i++) { var mesh = tiles[i], dir = mesh.userData.dir, fp = dir.clone().multiplyScalar(SPHERE_RADIUS); if (introPhase !== "exploding" && introPhase !== "done") { mesh.scale.setScalar(0); mesh.material.uniforms.opacity.value = 0; mesh.material.uniforms.bendAmt.value = 0; } else if (introPhase === "exploding") { var et = (now - explodeStart) / EXPLODE_DUR; var staggerOffset = (i / N) * 0.35; var localT = Math.max( 0, Math.min(1, (et - staggerOffset) / (1 - staggerOffset * 0.5 + 0.01)) ); var easedT = easeOutQuint(localT); var expandedRadius = SPHERE_RADIUS * easeOutExpo(Math.min(1, et * 1.2)); var fp2 = dir.clone().multiplyScalar(expandedRadius); mesh.position.set(fp2.x * easedT, fp2.y * easedT, fp2.z * easedT); mesh.lookAt(camWorld); mesh.scale.setScalar(1.0); mesh.material.uniforms.opacity.value = easeOutExpo( Math.min(1, et * 2.5) ); mesh.userData.bendCurrent += (BEND_AMOUNT * easedT - mesh.userData.bendCurrent) * 0.08; mesh.material.uniforms.bendAmt.value = mesh.userData.bendCurrent; if (et < 1) allDone = false; } else { mesh.position.set(fp.x, fp.y, fp.z); mesh.lookAt(camWorld); var ts = mesh === _hoveredMesh ? HOVER_SCALE : 1.0; mesh.userData.hoverScale += (ts - mesh.userData.hoverScale) * 0.12; mesh.scale.setScalar(mesh.userData.hoverScale); mesh.material.uniforms.opacity.value = 1; mesh.userData.bendCurrent += (BEND_AMOUNT - mesh.userData.bendCurrent) * 0.05; mesh.material.uniforms.bendAmt.value = mesh.userData.bendCurrent; } rotatedDir.copy(dir).applyQuaternion(groupQuat); mesh.material.uniforms.tileDir.value.copy(rotatedDir); } if (introPhase === "exploding" && allDone) { introPhase = "done"; revealNavbar(); } if (percentEl && percentEl.style.opacity !== "0" && introPhase !== "done") { percentDisplay += (percentTarget - percentDisplay) * 0.06; var displayVal = Math.min(100, Math.round(percentDisplay)); if (percentEl.textContent !== displayVal + "%") percentEl.textContent = displayVal + "%"; } renderer.render(scene, camera); } function resumeAnimate() { if (!_rafId) { _rafId = requestAnimationFrame(animate); } } resumeAnimate(); window.addEventListener("resize", function () { var s = getRootSize(); W = s.w; H = s.h; camera.aspect = W / H; camera.updateProjectionMatrix(); renderer.setSize(W, H); }); window.sphere = { pause: function () { setManualPaused(true); }, resume: function () { setManualPaused(false); }, goToStep: function (step) { if (step === "card") showFirstCard(); if (step === "flip") doFlip(); if (step === "explode") startExplode(performance.now()); if (step === "done") { textOverlay.style.display = "none"; introPhase = "done"; previewPhase = "done"; revealNavbar(); } }, }; })(); // ============================================ // РљРћРќРЎРўРђРќРўР« И КОНФИГУРАЦИЯ // ============================================ const BREAKPOINTS = { MOBILE: 479, DESKTOP: 480, SWIPER: 991, TICKER: 992, }; const menu = document.querySelector(".small-menu"); // ============================================ // МЕНЮ // ============================================ function initMenu() { if (!menu) return; const isMobile = window.innerWidth < 480; if (isMobile) { return; } gsap.set(menu, { y: "130%", autoAlpha: 1 }); setTimeout(() => { ScrollTrigger.create({ trigger: ".section-hero", start: "bottom 80%", endTrigger: ".section-footer", end: "top bottom", invalidateOnRefresh: true, onEnter: () => gsap.to(menu, { y: "0%", duration: 0.6 }), onLeaveBack: () => gsap.to(menu, { y: "130%", duration: 0.6 }), onLeave: () => gsap.to(menu, { y: "130%", duration: 0.6 }), onEnterBack: () => gsap.to(menu, { y: "0%", duration: 0.6 }), }); }, 1000); } function initMenuMobile() { const isMobile = window.innerWidth < 480; if (!isMobile) return; const logo = menu.querySelector(".small-menu-logo"); const tgButton = menu.querySelector(".icon-button.is-mob"); const menuInner = menu.querySelector(".small-menu-inner"); menuInner.classList.add("is-transparent"); ScrollTrigger.create({ trigger: ".section-hero", start: "bottom 40%", endTrigger: ".section-footer", end: "30% bottom", invalidateOnRefresh: true, onEnter: () => { logo.classList.remove("is-hidden-mob"); tgButton.classList.add("is-compressed"); menuInner.classList.remove("is-transparent"); }, onLeaveBack: () => { logo.classList.add("is-hidden-mob"); tgButton.classList.remove("is-compressed"); menuInner.classList.add("is-transparent"); }, onLeave: () => { gsap.to(menu, { y: "130%", duration: 0.6 }); }, onEnterBack: () => { gsap.to(menu, { y: "0%", duration: 0.6 }); }, }); } // ============================================ // ОБЩИЕ ФУНКЦИИ // ============================================ let mm = gsap.matchMedia(); mm.add("(min-width: 480px)", () => { const mediaWraps = document.querySelectorAll(".cases_media-wrap"); const remInPixels = parseFloat( getComputedStyle(document.documentElement).fontSize ); // Слушатели РЅРёР¶Рµ Р Р…Р Вµ GSAP-объекты, поэтому gsap.matchMedia РёС… Р Р…Р Вµ ревертит // сам — без СЏРІРЅРѕРіРѕ снятия РїСЂРё выходе РёР· брейкпоинта РѕРЅРё копятся РїСЂРё // каждом пересечении 480px (ресайз, РїРѕРІРѕСЂРѕС‚, DevTools). const cleanups = []; mediaWraps.forEach((wrap) => { const cursor = wrap.querySelector(".case-cursor-follow"); if (!cursor) return; let isHovered = false; let lastMouseEvent = null; let scrollTicking = false; const cursorWidth = cursor.offsetWidth; const halfWidth = cursorWidth / 2; const offsetX = halfWidth + remInPixels; const offsetY = remInPixels; const gapX = 0.25 * remInPixels; const gapY = 1 * remInPixels; const updateCursorPosition = (e) => { const rect = wrap.getBoundingClientRect(); const x = e.clientX - rect.left + offsetX + gapX; const y = e.clientY - rect.top + offsetY + gapY; gsap.to(cursor, { x: x, y: y, duration: 0.3, ease: "power2.out", overwrite: "auto", }); }; const onMouseEnter = (e) => { isHovered = true; lastMouseEvent = e; const rect = wrap.getBoundingClientRect(); const x = e.clientX - rect.left + offsetX + gapX; const y = e.clientY - rect.top + offsetY + gapY; gsap.set(cursor, { x: x, y: y }); gsap.to(cursor, { opacity: 1, scale: 1, duration: 0.3, ease: "power2.out", }); }; const onMouseMove = (e) => { lastMouseEvent = e; updateCursorPosition(e); }; const onMouseLeave = () => { isHovered = false; lastMouseEvent = null; gsap.to(cursor, { opacity: 0, scale: 0.8, duration: 0.3, ease: "power2.out", }); }; const handleScroll = () => { if (!isHovered || !lastMouseEvent || scrollTicking) return; scrollTicking = true; requestAnimationFrame(() => { updateCursorPosition(lastMouseEvent); scrollTicking = false; }); }; wrap.addEventListener("mouseenter", onMouseEnter); wrap.addEventListener("mousemove", onMouseMove); wrap.addEventListener("mouseleave", onMouseLeave); window.addEventListener("scroll", handleScroll, { passive: true }); cleanups.push(() => { wrap.removeEventListener("mouseenter", onMouseEnter); wrap.removeEventListener("mousemove", onMouseMove); wrap.removeEventListener("mouseleave", onMouseLeave); window.removeEventListener("scroll", handleScroll); }); }); return () => cleanups.forEach((fn) => fn()); }); function initTextAnimations() { let typeSplit = new SplitType("[text-split]", { types: "words, chars", tagName: "span", }); $("[scrub-each-word]").each(function () { let tl = gsap.timeline({ scrollTrigger: { trigger: $(this), start: "top 70%", end: "top 40%", scrub: true, }, }); tl.from($(this).find(".word"), { opacity: 0.2, duration: 0.2, ease: "power1.out", stagger: { each: 0.4 }, }); }); gsap.set("[text-split]", { opacity: 1 }); } function initTimeDisplay() { function updateTime() { const now = new Date(); const barcelonaTime = now.toLocaleTimeString("en-GB", { timeZone: "Europe/Madrid", hour: "2-digit", minute: "2-digit", }); const belgradeTime = now.toLocaleTimeString("en-GB", { timeZone: "Europe/Belgrade", hour: "2-digit", minute: "2-digit", }); const barcelonaEl = document.getElementById("time-barcelona"); const belgradeEl = document.getElementById("time-belgrade"); if (barcelonaEl) barcelonaEl.textContent = barcelonaTime; if (belgradeEl) belgradeEl.textContent = belgradeTime; } updateTime(); setInterval(updateTime, 60 * 1000); } // ============================================ // TICKER И SWIPER // ============================================ let tickerScriptLoaded = false; function initTickerAndSwiper() { const carousel = document.querySelector(".carousel-horizontal[fb-carousel]"); const mqMobile = window.matchMedia("(max-width: 479px)"); // Remember original slides so we can rebuild cleanly when switching modes. let originalHTML = null; if (carousel) { originalHTML = carousel.querySelector(".carousel-horizontal-content")?.innerHTML || null; } let partnerSwiper = null; // Mark the slide whose center is closest to the container center as active. // Works for both autoplay marquee and manual free-drag. function updateCenterActive(swiper) { const containerRect = swiper.el.getBoundingClientRect(); const containerCenter = containerRect.left + containerRect.width / 2; let closest = null; let closestDist = Infinity; swiper.slides.forEach((slide) => { const r = slide.getBoundingClientRect(); const slideCenter = r.left + r.width / 2; const dist = Math.abs(slideCenter - containerCenter); if (dist < closestDist) { closestDist = dist; closest = slide; } }); swiper.slides.forEach((slide) => { slide.classList.toggle("is-active", slide === closest); }); } // ---- MOBILE (<=479): Swiper marquee with free drag + auto-resume ---- function initMobileMarquee() { if (!carousel || partnerSwiper) return; // Stop Flowbase from owning this element on mobile. carousel.removeAttribute("fb-carousel"); carousel.removeAttribute("fb-booster-initialized"); const content = carousel.querySelector(".carousel-horizontal-content"); if (!content) return; // Remove Flowbase's cloned duplicates (aria-hidden) so Swiper's loop is clean. content .querySelectorAll('.swiper-slide[aria-hidden="true"]') .forEach((el) => el.remove()); // Clear inline styles Flowbase injected on the track. content.style.transform = ""; content.style.willChange = ""; content.style.display = ""; content.style.width = ""; // Swiper needs the standard wrapper/slide structure. Slides already have // .swiper-slide; make the container a .swiper and the track a .swiper-wrapper. carousel.classList.add("swiper", "is-partner-mobile"); content.classList.add("swiper-wrapper"); const remPx = parseFloat( getComputedStyle(document.documentElement).fontSize ); let isDragging = false; let momentumActive = false; partnerSwiper = new Swiper(".carousel-horizontal.is-partner-mobile", { slidesPerView: "auto", spaceBetween: 0.13 * remPx, loop: true, grabCursor: true, speed: 6000, allowTouchMove: true, freeMode: { enabled: true, momentum: true, momentumRatio: 2, // дальше катится momentumVelocityRatio: 1.2, // сильнее реагирует РЅР° скорость пальца momentumBounce: false, sticky: false, }, resistanceRatio: 0.85, autoplay: { delay: 0, disableOnInteraction: false, pauseOnMouseEnter: false, }, on: { init() { this.wrapperEl.style.transitionTimingFunction = "linear"; }, touchStart() { isDragging = true; momentumActive = false; }, touchEnd() { isDragging = false; momentumActive = true; // резюмируем marquee почти сразу, чтобы РѕРЅ подхватил ленту РЅР° С…РѕРґСѓ setTimeout(() => { if (partnerSwiper && partnerSwiper.autoplay) { partnerSwiper.autoplay.start(); } }, 50); }, setTransition(swiper) { if (momentumActive) { // очень пологий С…РІРѕСЃС‚ — почти РЅРµ «садится» РІ конце swiper.wrapperEl.style.transitionTimingFunction = "cubic-bezier(0.12, 0.8, 0.32, 1)"; momentumActive = false; } else { swiper.wrapperEl.style.transitionTimingFunction = "linear"; } }, }, }); } function destroyMobileMarquee() { if (!partnerSwiper) return; partnerSwiper.destroy(true, true); // true,true = clean inline styles partnerSwiper = null; if (carousel) { carousel.classList.remove("swiper", "is-partner-mobile"); const content = carousel.querySelector(".carousel-horizontal-content"); if (content) content.classList.remove("swiper-wrapper"); // Restore original slides so Flowbase can re-init cleanly on desktop. if (content && originalHTML != null) content.innerHTML = originalHTML; } } // ---- DESKTOP: load Flowbase as before ---- function loadFlowbase() { if (!carousel) return; // Make sure the element is Flowbase-owned again. carousel.setAttribute("fb-carousel", "true"); if (!tickerScriptLoaded) { const script = document.createElement("script"); script.src = "https://cdn.jsdelivr.net/npm/@flowbase-co/boosters-carousel-ticker@1/dist/carousel-ticker.min.js"; script.type = "text/javascript"; document.head.appendChild(script); tickerScriptLoaded = true; } } function apply() { if (mqMobile.matches) { // mobile: kill Flowbase ownership, run Swiper marquee initMobileMarquee(); } else { // desktop: kill Swiper, hand back to Flowbase destroyMobileMarquee(); loadFlowbase(); } } // On mobile-first load, strip fb-carousel BEFORE Flowbase script would run, // so Flowbase never initializes this element on mobile. if (mqMobile.matches && carousel) { carousel.removeAttribute("fb-carousel"); } apply(); mqMobile.addEventListener("change", apply); // keep your existing is--active class logic const mqlMobile = window.matchMedia(`(max-width: ${BREAKPOINTS.MOBILE}px)`); function handleActiveClass() { document .querySelectorAll(".swiper-hero .swiper-partner-card") .forEach((card) => { if (mqlMobile.matches) card.classList.add("is--active"); else card.classList.remove("is--active"); }); } mqlMobile.addEventListener("change", handleActiveClass); handleActiveClass(); } // ============================================ // Р—РђРџРЈРЎРљ // ============================================ function initSectionAnimations() { var ease = "power4.out"; [".section-partners", ".section-about", ".cases-heading-wrapper"].forEach( function (sel) { var el = document.querySelector(sel); if (!el) return; gsap.fromTo( el, { opacity: 0, y: 56 }, { scrollTrigger: { trigger: el, start: "top 70%" }, opacity: 1, y: 0, duration: 1.15, ease: ease, } ); } ); var footer = document.querySelector(".section-footer"); if (footer) { gsap.fromTo( footer, { opacity: 0, y: 40 }, { scrollTrigger: { trigger: footer, start: "top 75%" }, opacity: 1, y: 0, duration: 1, ease: ease, } ); } gsap.utils.toArray(".cases_grid-item").forEach(function (item) { gsap.fromTo( item, { opacity: 0, y: 72, scale: 0.95 }, { scrollTrigger: { trigger: item, start: "top 82%" }, opacity: 1, y: 0, scale: 1, duration: 1.1, ease: ease, } ); }); } function initFooterAnimation() { const footerRoot = document.querySelector(".mwg_effect020"); if (!footerRoot) return; const elements = []; footerRoot.querySelectorAll(".medias > div").forEach((element) => { elements.push(element.cloneNode(true)); }); if (!elements.length) return; let indexElement = 0; const activeTimelines = new Set(); document.addEventListener("visibilitychange", () => { activeTimelines.forEach((tl) => { if (document.hidden) tl.pause(); else tl.resume(); }); }); // общая функция — РЅРµ зависит РѕС‚ источника координат function createMedia(x, y, deltaX, deltaY) { if (document.hidden) return; const element = elements[indexElement].cloneNode(true); footerRoot.appendChild(element); const tl = gsap.timeline({ onComplete: () => { activeTimelines.delete(tl); footerRoot.removeChild(element); tl.kill(); }, }); activeTimelines.add(tl); tl.fromTo( element, { xPercent: -50 + (Math.random() - 0.5) * 80, yPercent: -50 + (Math.random() - 0.5) * 10, scaleX: 1.3, scaleY: 1.3, }, { scaleX: 1, scaleY: 1, ease: "elastic.out(2, 0.6)", duration: 0.6 } ); tl.fromTo( element, { x, y, rotation: (Math.random() - 0.5) * 20 }, { x: "+=" + deltaX * 4, y: "+=" + deltaY * 4, rotation: (Math.random() - 0.5) * 20, ease: "power4.out", duration: 1.5, }, "<" ); tl.to(element, { duration: 0.3, scale: 0.5, delay: 0.1, ease: "back.in(1.5)", }); indexElement = (indexElement + 1) % elements.length; } // автоспавн в случайных точках, пока футер виден (мобилка + десктоп в простое) function createAutoSpawner(minDelay, randDelay) { let timeoutId = null; let visible = true; let enabled = false; // не жжём батарею, пока футер за экраном const io = new IntersectionObserver( ([entry]) => { visible = entry.isIntersecting; syncPlayback(); }, { threshold: 0.1 } ); io.observe(footerRoot); function spawnLoop() { timeoutId = null; if (!enabled || !visible || document.hidden) return; const rect = footerRoot.getBoundingClientRect(); const x = Math.random() * rect.width; const y = Math.random() * rect.height; const angle = Math.random() * Math.PI * 2; const speed = 6 + Math.random() * 10; const deltaX = Math.cos(angle) * speed; const deltaY = Math.sin(angle) * speed; createMedia(x, y, deltaX, deltaY); timeoutId = setTimeout(spawnLoop, minDelay + Math.random() * randDelay); } const stop = () => { enabled = false; clearTimeout(timeoutId); timeoutId = null; }; function syncPlayback() { if (!enabled || !visible || document.hidden) { clearTimeout(timeoutId); timeoutId = null; } else if (timeoutId === null) { spawnLoop(); } } document.addEventListener("visibilitychange", syncPlayback); return { start: () => { enabled = true; syncPlayback(); }, stop, destroy: () => { stop(); io.disconnect(); document.removeEventListener("visibilitychange", syncPlayback); }, }; } const mm = gsap.matchMedia(); // в”Ђв”Ђ ДЕСКТОП: РїРѕ движению мыши (как было) в”Ђв”Ђ mm.add("(min-width: 480px) and (pointer: fine)", () => { let incr = 0, oldIncrX = 0, oldIncrY = 0; const resetDist = window.innerWidth / 4; // нет движения курсора IDLE_DELAY мс → картинки появляются сами; // любое движение мыши сразу отдаёт управление курсору const IDLE_DELAY = 1500; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const auto = reduceMotion ? null : createAutoSpawner(500, 400); let idleId = null; const scheduleIdle = () => { if (!auto) return; clearTimeout(idleId); idleId = setTimeout(auto.start, IDLE_DELAY); }; const onActivity = () => { if (auto) auto.stop(); scheduleIdle(); }; scheduleIdle(); const onFirstMove = (e) => { oldIncrX = e.clientX; oldIncrY = e.clientY; }; const onMove = (e) => { const valX = e.clientX, valY = e.clientY; incr += Math.abs(valX - oldIncrX) + Math.abs(valY - oldIncrY); if (incr > resetDist) { incr = 0; createMedia( valX, valY - footerRoot.getBoundingClientRect().top, valX - oldIncrX, valY - oldIncrY ); } oldIncrX = valX; oldIncrY = valY; }; footerRoot.addEventListener("mousemove", onFirstMove, { once: true }); footerRoot.addEventListener("mousemove", onMove); footerRoot.addEventListener("mousemove", onActivity); return () => { footerRoot.removeEventListener("mousemove", onFirstMove); footerRoot.removeEventListener("mousemove", onMove); footerRoot.removeEventListener("mousemove", onActivity); clearTimeout(idleId); if (auto) auto.destroy(); }; }); mm.add("(max-width: 479px), (pointer: coarse)", () => { const auto = createAutoSpawner(300, 400); auto.start(); return auto.destroy; }); } window.addEventListener("DOMContentLoaded", () => { initTextAnimations(); initMenu(); initMenuMobile(); initTimeDisplay(); initTickerAndSwiper(); initSectionAnimations(); initFooterAnimation(); });