document.addEventListener("DOMContentLoaded", function () { let lenis; let isNavOpen = false; let isModalOpen = false; const ST_Config = { start: "top 88%", markers: false, }; console.log( "%cWebsite by Web Disco\nhttps://webdisco.digital", "color: #1A1918; font-size: 14px; font-weight: 600;" ); /* Lenis */ if (Webflow.env("editor") === undefined) { lenis = new Lenis(); lenis?.on("scroll", ScrollTrigger.update); gsap.ticker.add((time) => { lenis?.raf(time * 1000); }); gsap.ticker.lagSmoothing(0); } /* Lenis */ /* Cursor */ const initCursor = () => { const cursor = document.querySelector(".cursor"); const homeIntro = document.querySelector(".section_intro"); const aboutIntro = document.querySelector(".about_intro"); const hero = document.querySelector(".section_hero"); const hideElements = document.querySelectorAll(".header, .section_hero-content"); if (!cursor) return; let cursorLocked = false; // locked by intro ScrollTriggers let forceVisible = false; // soft override (featured active slide hover) let hardHidden = false; // hard override (project player click) let heroLocked = false; // hide cursor once hero is left // Base setup gsap.set(cursor, { xPercent: -50, yPercent: -50 }); const xTo = gsap.quickTo(cursor, "x", { duration: 0.6, ease: "power3" }); const yTo = gsap.quickTo(cursor, "y", { duration: 0.6, ease: "power3" }); const showCursor = () => { if (hardHidden) return; gsap.to(cursor, { autoAlpha: 1, scale: 1, duration: 0.25, overwrite: "auto" }); }; const hideCursor = () => { gsap.to(cursor, { autoAlpha: 0, scale: 0, duration: 0.25, overwrite: "auto" }); }; const updateCursorVisibility = () => { if (hardHidden) { hideCursor(); return; } if (forceVisible) { showCursor(); return; } // hide if ANY lock is active if (cursorLocked || heroLocked) hideCursor(); else showCursor(); }; // Follow mouse (only bind once) if (!window.__cursorMoveBound) { window.__cursorMoveBound = true; window.addEventListener( "mousemove", (e) => { xTo(e.clientX); yTo(e.clientY - 10); }, { passive: true } ); } // Hide over header / hero elements hideElements.forEach((el) => { el.addEventListener("mouseenter", () => { if (hardHidden) return; if (forceVisible) return; hideCursor(); }); el.addEventListener("mouseleave", () => { updateCursorVisibility(); }); }); // Soft force events (used by Featured Swiper hover) window.addEventListener("cursor:force:on", () => { forceVisible = true; updateCursorVisibility(); }); window.addEventListener("cursor:force:off", () => { forceVisible = false; updateCursorVisibility(); }); // External hard hide/show events (reserve ONLY for player clicks / true hard state) window.addEventListener("cursor:hide:hard", () => { hardHidden = true; updateCursorVisibility(); }); window.addEventListener("cursor:show", () => { hardHidden = false; updateCursorVisibility(); }); // ScrollTrigger lock zones (Home + About behave the same) const intros = [homeIntro, aboutIntro].filter(Boolean); if (intros.length) { intros.forEach((introEl) => { ScrollTrigger.create({ trigger: introEl, start: "top 90%", markers: false, onEnter: () => { cursorLocked = true; updateCursorVisibility(); }, onLeaveBack: () => { cursorLocked = false; updateCursorVisibility(); }, }); }); // Initial state on load (if past ANY intro start, lock) const scrollY = window.scrollY || window.pageYOffset; cursorLocked = intros.some((introEl) => { const startPoint = introEl.offsetTop + introEl.offsetHeight * 0.5; return scrollY > startPoint; }); } if (hero) { ScrollTrigger.create({ trigger: hero, start: "top top", end: 'bottom 90%', markers: false, onEnter: () => { heroLocked = false; updateCursorVisibility(); }, onLeave: () => { heroLocked = true; updateCursorVisibility(); }, onEnterBack: () => { heroLocked = false; updateCursorVisibility(); }, }); } ScrollTrigger.addEventListener("refreshInit", () => updateCursorVisibility()); // Set initial visibility updateCursorVisibility(); }; /* Cursor */ /* Featured Slider */ const initFeaturedSwiper = () => { const swiperEl = document.querySelector(".swiper.is-featured"); if (!swiperEl) return; const prevBtn = document.querySelector('[featured-swiper-button="prev"]'); const nextBtn = document.querySelector('[featured-swiper-button="next"]'); const slidePrevBtn = document.querySelector(".featured_slide-arrow.is-left"); const slideNextBtn = document.querySelector(".featured_slide-arrow.is-right"); const mobileTitle = document.querySelector('[data-active-slide-title="mobile"]'); const desktopTitle = document.querySelector('[data-active-slide-title="desktop"]'); const titleWrap = document.querySelector(".swiper_title"); const titleTargets = [mobileTitle, desktopTitle].filter(Boolean); const fadeTargets = [titleWrap, ...titleTargets].filter(Boolean); if (!fadeTargets.length) return; let lastRealIndex = null; const getRealSlideEl = (swiper) => { const idx = swiper.realIndex; return Array.from(swiper.slides).find((s) => { const slideIndexAttr = s.getAttribute("data-swiper-slide-index"); return slideIndexAttr !== null && Number(slideIndexAttr) === idx; }); }; const animateTitleChange = (swiper, { force = false } = {}) => { const realIndex = swiper.realIndex; if (!force && lastRealIndex === realIndex) return; lastRealIndex = realIndex; const realSlide = getRealSlideEl(swiper); const title = realSlide?.dataset?.slideTitle || ""; gsap.killTweensOf(fadeTargets); gsap .timeline({ defaults: { overwrite: "auto" } }) .to(fadeTargets, { opacity: 0, duration: 0.25, ease: "power2.out", }) .add(() => { titleTargets.forEach((el) => (el.textContent = title)); }) .to(fadeTargets, { opacity: 1, duration: 0.3, ease: "power2.out", }); }; const cursorForceOn = () => window.dispatchEvent(new Event("cursor:force:on")); const cursorForceOff = () => window.dispatchEvent(new Event("cursor:force:off")); new Swiper(swiperEl, { slidesPerView: "auto", centeredSlides: true, loop: true, navigation: { nextEl: [nextBtn, slideNextBtn].filter(Boolean), prevEl: [prevBtn, slidePrevBtn].filter(Boolean), }, on: { init(swiper) { animateTitleChange(swiper, { force: true }); // Cursor only when hovering the ACTIVE slide (loop-safe) const isActiveSlide = (target) => { const slide = target?.closest?.(".swiper-slide"); return !!slide && slide.classList.contains("swiper-slide-active"); }; const onMove = (e) => (isActiveSlide(e.target) ? cursorForceOn() : cursorForceOff()); const onLeave = () => cursorForceOff(); swiper.el.addEventListener("pointermove", onMove, true); swiper.el.addEventListener("pointerleave", onLeave, true); swiper.on("slideChangeTransitionEnd", () => cursorForceOff()); }, realIndexChange(swiper) { animateTitleChange(swiper); }, }, }); }; /* Featured Slider */ /* Stills Slider */ const initStillsSlider = () => { const swiperEl = document.querySelector(".swiper.is-stills"); const stills = document.querySelectorAll(".project_still-image"); const stillsModal = document.querySelector(".stills_modal"); const closeBtn = document.querySelector('[data-stills-modal="close"]'); let isStillsOpen = false; if (!swiperEl || !stills.length) return; const prevBtn = document.querySelector('[stills-swiper-button="prev"]'); const nextBtn = document.querySelector('[stills-swiper-button="next"]'); const currentSlideText = document.querySelector('[data-stills-modal="current"]'); const totalSlideText = document.querySelector('[data-stills-modal="total"]'); const openStillsModal = () => { isStillsOpen = true; gsap.to(stillsModal, { autoAlpha: 1, duration: 0.25, }); lenis?.stop(); }; const closeStillsModal = () => { isStillsOpen = false; gsap.to(stillsModal, { autoAlpha: 0, duration: 0.25, }); lenis?.start(); }; const swiper = new Swiper(swiperEl, { slidesPerView: "auto", navigation: { nextEl: nextBtn, prevEl: prevBtn, }, on: { init(swiper) { if (totalSlideText) totalSlideText.textContent = swiper.slides.length; if (currentSlideText) currentSlideText.textContent = swiper.activeIndex + 1; }, slideChange(swiper) { if (currentSlideText) currentSlideText.textContent = swiper.activeIndex + 1; }, }, breakpoints: { 1024: { slidesPerView: 1.1 }, 1440: { slidesPerView: 1.5 }, 1920: { slidesPerView: "auto" }, }, }); stills.forEach((still, index) => { still.addEventListener("click", () => { openStillsModal(); swiper.slideTo(index); }); }); closeBtn?.addEventListener("click", () => { closeStillsModal(); }); stillsModal?.addEventListener("click", (e) => { const isImage = e.target.closest(".swiper-slide img"); const isNavBtn = e.target.closest("[stills-swiper-button]"); const isCloseBtn = e.target.closest('[data-stills-modal="close"]'); const isInsideInner = e.target.closest(".stills_modal-inner"); if (isImage || isNavBtn || isCloseBtn) return; if (!isInsideInner) closeStillsModal(); }); document.addEventListener("keydown", (e) => { if (!isStillsOpen) return; switch (e.key) { case "Escape": closeStillsModal(); break; case "ArrowRight": swiper.slideNext(); break; case "ArrowLeft": swiper.slidePrev(); break; } }); }; /* Stills Slider */ /* Calendly Button */ const initCalendlyButton = () => { const btns = document.querySelectorAll("[data-calendly]"); if (!btns.length) return; btns.forEach((btn) => { btn.addEventListener("click", (e) => { e.preventDefault(); const url = btn.getAttribute("href"); if (!url) return; Calendly.initPopupWidget({ url }); }); }); }; /* Calendly Button */ /* Showreel Link */ const initShowreelLink = () => { const HOME_PATHS = ["/"]; const isHomePage = () => HOME_PATHS.includes(window.location.pathname); const goHomeWithFlag = () => { const url = new URL(window.location.href); url.pathname = "/"; url.searchParams.set("showreel", "1"); url.hash = ""; // optional window.location.href = url.toString(); }; const openHeroModalLikeHeroClick = () => { const hero = document.querySelector(".hero_overlay"); if (!hero) return; hero.click(); }; document.addEventListener("click", (e) => { const link = e.target.closest('a[data-showreel]'); if (!link) return; e.preventDefault(); e.stopPropagation(); const dd = link.closest(".w-dropdown"); if (dd && dd.classList.contains("w--open")) { dd.classList.remove("w--open"); const toggle = dd.querySelector(".w-dropdown-toggle"); const list = dd.querySelector(".w-dropdown-list"); toggle?.classList.remove("w--open"); toggle?.setAttribute("aria-expanded", "false"); list?.classList.remove("w--open"); list && (list.style.display = "none"); } if (!isHomePage()) { goHomeWithFlag(); return; } openHeroModalLikeHeroClick(); }, true); const params = new URLSearchParams(window.location.search); if (isHomePage() && params.get("showreel") === "1") { requestAnimationFrame(() => { openHeroModalLikeHeroClick(); params.delete("showreel"); const clean = window.location.pathname + (params.toString() ? `?${params.toString()}` : "") + window.location.hash; history.replaceState({}, "", clean); }); } }; /* Showreel Link */ /* Custom Bunny Player */ function initBunnyPlayer() { // ---- Page elements const hero = document.querySelector(".hero_overlay"); const heroVideoToggle = document.querySelector(".hero_video-toggle"); const modal = document.querySelector(".hero_modal"); const modalBtn = modal?.querySelector('[data-hero-modal="close"]'); const mobileReelBtn = document.querySelector(".mobile_reel-btn"); // ---- Collect players const players = Array.from(document.querySelectorAll("[data-bunny-player-init]")); if (!players.length) return; let heroPlayer = null; let modalPlayer = null; let projectPlayer = null; // ---- Global pointer tracking (ONLY ONCE) if (!window.__bunnyPointerBound) { window.__bunnyPointerBound = true; const pointer = { x: null, y: null }; window.__bunnyPointer = pointer; window.addEventListener( "pointermove", (e) => { pointer.x = e.clientX; pointer.y = e.clientY; }, { passive: true } ); } const pointer = window.__bunnyPointer; // ---- Helpers const pad2 = (n) => (n < 10 ? "0" : "") + n; const formatTime = (sec) => { if (!isFinite(sec) || sec < 0) return "00:00"; const s = Math.floor(sec); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const r = s % 60; return h > 0 ? `${h}:${pad2(m)}:${pad2(r)}` : `${pad2(m)}:${pad2(r)}`; }; const setText = (nodes, text) => nodes.forEach((n) => (n.textContent = text)); const safePlay = (video) => { const p = video.play(); if (p && typeof p.then === "function") p.catch(() => {}); }; const bestLevel = (levels) => { if (!levels || !levels.length) return null; return levels.reduce( (a, b) => ((b.width || 0) > (a.width || 0) ? b : a), levels[0] ); }; const setBeforeRatio = (player, updateSize, w, h) => { if (updateSize !== "true" || !w || !h) return; const before = player.querySelector("[data-player-before]"); if (!before) return; before.style.paddingTop = (h / w) * 100 + "%"; }; const maybeSetRatioFromVideo = (player, updateSize, video) => { if (updateSize !== "true") return; const before = player.querySelector("[data-player-before]"); if (!before) return; const hasPad = before.style.paddingTop && before.style.paddingTop !== "0%"; if (!hasPad && video.videoWidth && video.videoHeight) { setBeforeRatio(player, updateSize, video.videoWidth, video.videoHeight); } }; const resolveUrl = (base, rel) => { try { return new URL(rel, base).toString(); } catch (_) { return rel; } }; // ---- Unified meta fetch (duration + best resolution) function getSourceMeta(src, useHlsJs) { return new Promise((resolve) => { if (useHlsJs && window.Hls && Hls.isSupported()) { try { const tmp = new Hls(); const out = { width: 0, height: 0, duration: NaN }; tmp.on(Hls.Events.MANIFEST_PARSED, (_, data) => { const lvls = (data && data.levels) || tmp.levels || []; const best = bestLevel(lvls); if (best?.width && best?.height) { out.width = best.width; out.height = best.height; } }); tmp.on(Hls.Events.LEVEL_LOADED, (_, data) => { if (data?.details && isFinite(data.details.totalduration)) { out.duration = data.details.totalduration; } try { tmp.destroy(); } catch (_) {} resolve(out); }); tmp.on(Hls.Events.ERROR, () => { try { tmp.destroy(); } catch (_) {} resolve(out); }); tmp.loadSource(src); return; } catch (_) { resolve({ width: 0, height: 0, duration: NaN }); return; } } const parseMaster = (masterText) => { const lines = masterText.split(/\r?\n/); let bestW = 0, bestH = 0, firstMedia = null, lastInf = null; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.indexOf("#EXT-X-STREAM-INF:") === 0) { lastInf = line; } else if (lastInf && line && line[0] !== "#") { if (!firstMedia) firstMedia = line.trim(); const m = /RESOLUTION=(\d+)x(\d+)/.exec(lastInf); if (m) { const w = parseInt(m[1], 10); const h = parseInt(m[2], 10); if (w > bestW) { bestW = w; bestH = h; } } lastInf = null; } } return { bestW, bestH, media: firstMedia }; }; const sumDuration = (mediaText) => { let dur = 0; const re = /#EXTINF:([\d.]+)/g; let m; while ((m = re.exec(mediaText))) dur += parseFloat(m[1]); return dur; }; fetch(src, { credentials: "omit", cache: "no-store" }) .then((r) => { if (!r.ok) throw new Error("master"); return r.text(); }) .then((master) => { const info = parseMaster(master); if (!info.media) { resolve({ width: info.bestW || 0, height: info.bestH || 0, duration: NaN, }); return; } const mediaUrl = resolveUrl(src, info.media); return fetch(mediaUrl, { credentials: "omit", cache: "no-store" }) .then((r) => { if (!r.ok) throw new Error("media"); return r.text(); }) .then((mediaText) => { resolve({ width: info.bestW || 0, height: info.bestH || 0, duration: sumDuration(mediaText), }); }); }) .catch(() => resolve({ width: 0, height: 0, duration: NaN })); }); } // ---- Cursor logic helpers const cursorHardHide = () => window.dispatchEvent(new CustomEvent("cursor:hide:hard")); const cursorShow = () => window.dispatchEvent(new CustomEvent("cursor:show")); // ---- Build each player players.forEach((player) => { const id = player.dataset.bunnyPlayerId; if (id === "background") heroPlayer = player; if (id === "modal") modalPlayer = player; if (id === "project") projectPlayer = player; const src = player.getAttribute("data-player-src"); if (!src) return; const video = player.querySelector("video"); if (!video) return; // Initial safety reset try { video.pause(); } catch (_) {} try { video.removeAttribute("src"); video.load(); } catch (_) {} // Defaults if (!player.hasAttribute("data-player-activated")) player.setAttribute("data-player-activated", "false"); if (!player.hasAttribute("data-player-hover")) player.setAttribute("data-player-hover", "idle"); if (!player.hasAttribute("data-player-status")) player.setAttribute("data-player-status", "idle"); // Element refs const timeline = player.querySelector("[data-player-timeline]"); const progressBar = player.querySelector("[data-player-progress]"); const bufferedBar = player.querySelector("[data-player-buffered]"); const handle = player.querySelector("[data-player-timeline-handle]"); const timeDurationEls = player.querySelectorAll("[data-player-time-duration]"); const timeProgressEls = player.querySelectorAll("[data-player-time-progress]"); // Flags const updateSize = player.getAttribute( "data-player-update-size"); // "true" | "cover" | null const lazyMode = player.getAttribute("data-player-lazy"); // "true" | "meta" | null const isLazyTrue = lazyMode === "true"; const isLazyMeta = lazyMode === "meta"; const autoplay = player.getAttribute("data-player-autoplay") === "true"; const initialMuted = player.getAttribute("data-player-muted") === "true"; // State let pendingPlay = false; let isAttached = false; let lastPauseBy = ""; let rafId = 0; const setStatus = (s) => { if (player.getAttribute("data-player-status") !== s) player.setAttribute("data-player-status", s); }; const setActivated = (v) => player.setAttribute("data-player-activated", v ? "true" : "false"); const setFsAttr = (v) => player.setAttribute("data-player-fullscreen", v ? "true" : "false"); // ---- Project-only mobile mute CTA const mobileMuteHint = id === "project" ? player.querySelector(".bunny-player__mobile-mute") : null; const syncMobileMuteHint = () => { if (!mobileMuteHint) return; const isMutedAttr = player.getAttribute("data-player-muted") === "true"; gsap.to(mobileMuteHint, { autoAlpha: isMutedAttr ? 1 : 0, duration: 0.25, ease: "power2.out", pointerEvents: isMutedAttr ? "auto" : "none", }); }; const setMutedState = (v) => { video.muted = !!v; player.setAttribute("data-player-muted", video.muted ? "true" : "false"); if (id === "project" && !video.muted) player._wasUnmuted = true; if (id === "project") syncMobileMuteHint(); }; // Init mute/loop (autoplay => muted + loop) if (autoplay) { setMutedState(true); video.loop = true; } else { setMutedState(initialMuted); } // Ensure iOS attrs video.setAttribute("muted", ""); video.setAttribute("playsinline", ""); video.setAttribute("webkit-playsinline", ""); video.playsInline = true; if (typeof video.disableRemotePlayback !== "undefined") video.disableRemotePlayback = true; if (autoplay) video.autoplay = false; // Project player unmute tracking if (id === "project") { player._wasUnmuted = player._wasUnmuted || !video.muted; video.addEventListener("volumechange", () => { if (!video.muted) player._wasUnmuted = true; }); } // Project-only: click/tap the mobile CTA to unmute + fade out if (id === "project" && mobileMuteHint) { mobileMuteHint.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); cursorHardHide(); if (video.muted) setMutedState(false); if ((isLazyTrue || isLazyMeta) && !isAttached) attachMediaOnce(); safePlay(video); setStatus("playing"); gsap.to(mobileMuteHint, { autoAlpha: 0, duration: 0.25, ease: "power2.out", pointerEvents: "none", }); }); } // HLS capability const isSafariNative = !!video.canPlayType("application/vnd.apple.mpegurl"); const canUseHlsJs = !!(window.Hls && Hls.isSupported()) && !isSafariNative; // Ratio fetch for updateSize=true (only when not lazy-meta) if (updateSize === "true" && !isLazyMeta) { if (!isLazyTrue) { const prev = video.preload; video.preload = "metadata"; const onMeta = () => { setBeforeRatio(player, updateSize, video.videoWidth, video.videoHeight); video.preload = prev || ""; }; video.addEventListener("loadedmetadata", onMeta, { once: true }); video.src = src; } } // Lazy meta fetch (duration + ratio) const fetchMetaOnce = () => { getSourceMeta(src, canUseHlsJs).then((meta) => { if (meta.width && meta.height) setBeforeRatio(player, updateSize, meta.width, meta .height); if (timeDurationEls.length && isFinite(meta.duration) && meta.duration > 0) { setText(timeDurationEls, formatTime(meta.duration)); } if ( !pendingPlay && player.getAttribute("data-player-activated") !== "true" && player.getAttribute("data-player-status") === "idle" ) { player.setAttribute("data-player-status", "ready"); } }); }; // Attach media (once) const attachMediaOnce = () => { if (isAttached) return; isAttached = true; if (player._hls) { try { player._hls.destroy(); } catch (_) {} player._hls = null; } if (isSafariNative) { video.preload = isLazyTrue || isLazyMeta ? "auto" : video.preload; video.src = src; video.addEventListener( "loadedmetadata", () => { if (updateSize === "true") setBeforeRatio(player, updateSize, video.videoWidth, video.videoHeight); if (timeDurationEls.length) setText(timeDurationEls, formatTime(video .duration)); }, { once: true } ); } else if (canUseHlsJs) { const hls = new Hls({ maxBufferLength: 10 }); hls.attachMedia(video); hls.on(Hls.Events.MEDIA_ATTACHED, () => hls.loadSource(src)); hls.on(Hls.Events.MANIFEST_PARSED, (_, data) => { hls.startLevel = data.levels.length - 1; if (updateSize === "true") { const best = bestLevel(hls.levels || []); if (best?.width && best?.height) setBeforeRatio(player, updateSize, best.width, best.height); } }); hls.on(Hls.Events.LEVEL_LOADED, (_, data) => { if (data?.details && isFinite(data.details.totalduration)) { if (timeDurationEls.length) setText(timeDurationEls, formatTime(data.details.totalduration)); } }); player._hls = hls; } else { video.src = src; } }; // Initialize media based on lazy mode if (isLazyMeta) { fetchMetaOnce(); video.preload = "none"; } else if (isLazyTrue) { video.preload = "none"; } else { attachMediaOnce(); } // Ensure hint matches initial state (project only) if (id === "project") syncMobileMuteHint(); // API const togglePlay = () => { if (video.paused || video.ended) { if ((isLazyTrue || isLazyMeta) && !isAttached) attachMediaOnce(); pendingPlay = true; lastPauseBy = ""; setStatus("loading"); safePlay(video); } else { lastPauseBy = "manual"; video.pause(); } }; const toggleMute = () => setMutedState(!video.muted); const isFsActive = () => !!(document.fullscreenElement || document.webkitFullscreenElement); const enterFullscreen = () => { if (player.requestFullscreen) return player.requestFullscreen(); if (video.requestFullscreen) return video.requestFullscreen(); if (video.webkitSupportsFullscreen && typeof video.webkitEnterFullscreen === "function") return video.webkitEnterFullscreen(); }; const exitFullscreen = () => { if (document.exitFullscreen) return document.exitFullscreen(); if (document.webkitExitFullscreen) return document.webkitExitFullscreen(); if (video.webkitDisplayingFullscreen && typeof video.webkitExitFullscreen === "function") return video.webkitExitFullscreen(); }; const toggleFullscreen = () => { if (isFsActive() || video.webkitDisplayingFullscreen) exitFullscreen(); else enterFullscreen(); }; player._api = { play() { if (video.paused || video.ended) togglePlay(); }, pause() { if (!video.paused && !video.ended) togglePlay(); }, toggle: togglePlay, toggleMute, toggleFullscreen, video, }; // Fullscreen attribute syncing document.addEventListener("fullscreenchange", () => setFsAttr(isFsActive())); document.addEventListener("webkitfullscreenchange", () => setFsAttr(isFsActive())); video.addEventListener("webkitbeginfullscreen", () => setFsAttr(true)); video.addEventListener("webkitendfullscreen", () => setFsAttr(false)); // Controls (delegated) player.addEventListener("click", (e) => { if (id === "project" && mobileMuteHint && e.target.closest( ".bunny-player__mobile-mute")) return; const btn = e.target.closest("[data-player-control]"); if (!btn || !player.contains(btn)) return; const type = btn.getAttribute("data-player-control"); if (id === "project" && type === "playpause") { cursorHardHide(); if (video.muted) { setMutedState(false); safePlay(video); setStatus("playing"); return; } } if (type === "play" || type === "pause" || type === "playpause") togglePlay(); else if (type === "mute") toggleMute(); else if (type === "fullscreen") toggleFullscreen(); }); // Time text const updateTimeTexts = () => { if (timeDurationEls.length) setText(timeDurationEls, formatTime(video.duration)); if (timeProgressEls.length) setText(timeProgressEls, formatTime(video.currentTime)); }; video.addEventListener("timeupdate", updateTimeTexts); video.addEventListener("durationchange", updateTimeTexts); video.addEventListener("loadedmetadata", () => { updateTimeTexts(); maybeSetRatioFromVideo(player, updateSize, video); }); video.addEventListener("loadeddata", () => maybeSetRatioFromVideo(player, updateSize, video)); video.addEventListener("playing", () => maybeSetRatioFromVideo(player, updateSize, video)); // Progress visuals (rAF only when playing) const updateProgressVisuals = () => { if (!video.duration) return; const playedPct = (video.currentTime / video.duration) * 100; if (progressBar) progressBar.style.transform = `translateX(${-100 + playedPct}%)`; if (handle) handle.style.left = playedPct + "%"; }; const loopProgress = () => { updateProgressVisuals(); if (!video.paused && !video.ended) rafId = requestAnimationFrame(loopProgress); }; // Buffered bar const updateBufferedBar = () => { if (!bufferedBar || !video.duration || !video.buffered.length) return; const end = video.buffered.end(video.buffered.length - 1); const buffPct = (end / video.duration) * 100; bufferedBar.style.transform = `translateX(${-100 + buffPct}%)`; }; video.addEventListener("progress", updateBufferedBar); video.addEventListener("loadedmetadata", updateBufferedBar); video.addEventListener("durationchange", updateBufferedBar); // Media events video.addEventListener("play", () => { setActivated(true); cancelAnimationFrame(rafId); loopProgress(); setStatus("playing"); }); video.addEventListener("playing", () => { pendingPlay = false; setStatus("playing"); }); video.addEventListener("pause", () => { pendingPlay = false; cancelAnimationFrame(rafId); updateProgressVisuals(); setStatus("paused"); }); video.addEventListener("waiting", () => setStatus("loading")); video.addEventListener("canplay", () => { if ( !pendingPlay && player.getAttribute("data-player-activated") !== "true" && player.getAttribute("data-player-status") === "idle" ) { player.setAttribute("data-player-status", "ready"); } }); video.addEventListener("ended", () => { pendingPlay = false; cancelAnimationFrame(rafId); updateProgressVisuals(); setStatus("paused"); setActivated(false); }); // Scrubbing if (timeline) { let dragging = false; let wasPlaying = false; let targetTime = 0; let lastSeekTs = 0; const seekThrottle = 180; let rect = null; window.addEventListener("resize", () => { if (!dragging) rect = null; }); const getFractionFromX = (x) => { if (!rect) rect = timeline.getBoundingClientRect(); let f = (x - rect.left) / rect.width; if (f < 0) f = 0; if (f > 1) f = 1; return f; }; const previewAtFraction = (f) => { if (!video.duration) return; const pct = f * 100; if (progressBar) progressBar.style.transform = `translateX(${-100 + pct}%)`; if (handle) handle.style.left = pct + "%"; if (timeProgressEls.length) setText(timeProgressEls, formatTime(f * video.duration)); }; const maybeSeek = (now) => { if (!video.duration) return; if (now - lastSeekTs < seekThrottle) return; lastSeekTs = now; video.currentTime = targetTime; }; const onPointerMove = (e) => { if (!dragging) return; const f = getFractionFromX(e.clientX); targetTime = f * video.duration; previewAtFraction(f); maybeSeek(performance.now()); e.preventDefault(); }; const onPointerUp = () => { if (!dragging) return; dragging = false; player.setAttribute("data-timeline-drag", "false"); rect = null; video.currentTime = targetTime; if (wasPlaying) safePlay(video); else { updateProgressVisuals(); updateTimeTexts(); } window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("pointerup", onPointerUp); }; const onPointerDown = (e) => { if (!video.duration) return; dragging = true; wasPlaying = !video.paused && !video.ended; if (wasPlaying) video.pause(); player.setAttribute("data-timeline-drag", "true"); rect = timeline.getBoundingClientRect(); const f = getFractionFromX(e.clientX); targetTime = f * video.duration; previewAtFraction(f); maybeSeek(performance.now()); timeline.setPointerCapture && timeline.setPointerCapture(e.pointerId); window.addEventListener("pointermove", onPointerMove, { passive: false }); window.addEventListener("pointerup", onPointerUp, { passive: true }); e.preventDefault(); }; timeline.addEventListener("pointerdown", onPointerDown, { passive: false }); if (handle) handle.addEventListener("pointerdown", onPointerDown, { passive: false }); } // Hover/idle controls let hoverTimer = 0; const hoverHideDelay = 1500; const setHover = (state) => { if (player.getAttribute("data-player-hover") !== state) { player.setAttribute("data-player-hover", state); } }; const scheduleHide = () => { clearTimeout(hoverTimer); hoverTimer = window.setTimeout(() => setHover("idle"), hoverHideDelay); }; const wakeControls = () => { setHover("active"); scheduleHide(); }; const wakeIfPointerInside = () => { if (pointer?.x == null || pointer?.y == null) return; const r = player.getBoundingClientRect(); const inside = pointer.x >= r.left && pointer.x <= r.right && pointer.y >= r.top && pointer.y <= r.bottom; if (inside) wakeControls(); }; requestAnimationFrame(wakeIfPointerInside); window.addEventListener("scroll", wakeIfPointerInside, { passive: true }); window.addEventListener("resize", wakeIfPointerInside, { passive: true }); player.addEventListener("pointerdown", wakeControls); player.addEventListener("mousemove", wakeControls, { passive: true }); // Autoplay IO play/pause if (autoplay) { const io = new IntersectionObserver( (entries) => { entries.forEach((entry) => { const inView = entry.isIntersecting && entry.intersectionRatio > 0; if (inView) { if ((isLazyTrue || isLazyMeta) && !isAttached) attachMediaOnce(); if (video.paused && lastPauseBy !== "manual") { lastPauseBy = ""; pendingPlay = true; setStatus("loading"); safePlay(video); } else { setStatus("playing"); } } else { if (!video.paused && !video.ended) { lastPauseBy = "io"; video.pause(); setStatus("paused"); } } }); }, { threshold: 0.1 } ); io.observe(player); player._io = io; } }); // ---- ScrollTrigger: if project player is almost out of view AND never unmuted -> hide cursor if (projectPlayer) { ScrollTrigger.create({ trigger: projectPlayer, start: "top top", end: "bottom 90%", markers: false, onLeave: () => { const v = projectPlayer?._api?.video || projectPlayer.querySelector("video"); if (!v) return; if (v.muted && !projectPlayer._wasUnmuted) cursorHardHide(); }, onEnterBack: () => { const v = projectPlayer?._api?.video || projectPlayer.querySelector("video"); if (v && v.muted && !projectPlayer._wasUnmuted) cursorShow(); }, }); } // ---- Desktop modal behavior (hero -> modal) const mm = gsap.matchMedia(); mm.add("(min-width: 992px)", () => { const closeHeroModal = () => { if (modalPlayer) modalPlayer._api?.pause?.(); lenis?.start(); gsap.to(modal, { autoAlpha: 0, duration: 0.25, onComplete: () => { if (heroPlayer?._io) heroPlayer._io.observe(heroPlayer); heroPlayer?._api?.play?.(); }, }); }; hero?.addEventListener("click", (e) => { if (e.target.closest(".hero_modal") || e.target.closest("[data-player-control]")) return; heroPlayer?._api?.pause?.(); if (heroPlayer?._io) heroPlayer._io.disconnect(); lenis?.stop(); gsap.to(modal, { autoAlpha: 1, duration: 0.25, onComplete: () => modalPlayer?._api?.play?.(), }); }); modal?.addEventListener( "pointerdown", (e) => { if (e.target.closest('[data-hero-modal="close"]')) return; if (e.target.closest(".bunny-player")) return; closeHeroModal(); }, true ); modalBtn?.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); closeHeroModal(); }); const onKeyDown = (e) => { if (e.key === "Escape") closeHeroModal(); }; window.addEventListener("keydown", onKeyDown); return () => { window.removeEventListener("keydown", onKeyDown); }; }); // ---- External controls heroVideoToggle?.addEventListener("click", () => heroPlayer?._api?.toggle?.()); mobileReelBtn?.addEventListener("click", () => heroPlayer?._api?.toggleFullscreen?.()); modalBtn?.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); modalPlayer?._api?.pause?.(); lenis?.start(); gsap.to(modal, { autoAlpha: 0, duration: 0.25, onComplete: () => { if (heroPlayer?._io) heroPlayer._io.observe(heroPlayer); heroPlayer?._api?.play?.(); }, }); }); // ---- "F" key fullscreen for ACTIVE project/modal players (() => { if (window.__bunnyFsHotkeyBound) return; window.__bunnyFsHotkeyBound = true; const isEditableTarget = (el) => { if (!el) return false; const tag = el.tagName?.toLowerCase(); return tag === "input" || tag === "textarea" || el.isContentEditable; }; const isActivated = (player) => player?.getAttribute("data-player-activated") === "true"; const isModalOpenSafe = () => { const m = document.querySelector(".hero_modal"); if (!m) return false; const cs = getComputedStyle(m); return cs.visibility !== "hidden" && parseFloat(cs.opacity || "0") > 0; }; const pickActivePlayer = () => { if (modalPlayer && isModalOpenSafe() && isActivated(modalPlayer)) return modalPlayer; if (projectPlayer && isActivated(projectPlayer)) return projectPlayer; return null; }; window.addEventListener("keydown", (e) => { if (e.repeat) return; if (isEditableTarget(e.target)) return; if (e.key !== "f" && e.key !== "F") return; const active = pickActivePlayer(); if (!active?._api?.toggleFullscreen) return; e.preventDefault(); active._api.toggleFullscreen(); }); })(); } /* Custom Bunny Players */ const initHeroVideoToggle = () => { const player = document.querySelector(".bunny-player.is-background"); const toggle = document.querySelector(".hero_video-toggle"); if (!player || !toggle) return; const pauseIcon = toggle.querySelector(".pause-icon"); const playIcon = toggle.querySelector(".play-icon"); const updateIcon = () => { const status = player.dataset.playerStatus; if (status === "playing") { pauseIcon.style.display = "block"; playIcon.style.display = "none"; } else { pauseIcon.style.display = "none"; playIcon.style.display = "block"; } }; updateIcon(); const observer = new MutationObserver(updateIcon); observer.observe(player, { attributes: true, attributeFilter: ["data-player-status"], }); }; /* Loader Animation */ function initLogoRevealLoader() { const HAS_RUN_KEY = "logoLoaderHasRun"; gsap.registerPlugin(CustomEase, SplitText); CustomEase.create("loader", "0.65, 0.01, 0.05, 0.99"); const cursor = document.querySelector(".cursor"); const wrap = document.querySelector("[data-load-wrap]"); if (!wrap) return; if (sessionStorage.getItem(HAS_RUN_KEY)) { gsap.set(wrap, { display: "none" }); lenis?.start(); return; } lenis?.stop(); if (cursor) { gsap.set(cursor, { autoAlpha: 0, scale: 0, }); } const isHomePage = window.location.pathname === "/"; const header = document.querySelector("header"); const heroContent = document.querySelector(".section_hero-content"); if (isHomePage) { gsap.set([header, heroContent], { autoAlpha: 0, }); } const container = wrap.querySelector("[data-load-container]"); const bg = wrap.querySelector("[data-load-bg]"); const progressBar = wrap.querySelector("[data-load-progress]"); const logo = wrap.querySelector("[data-load-logo]"); const textElements = Array.from(wrap.querySelectorAll("[data-load-text]")); const resetTargets = Array.from( wrap.querySelectorAll('[data-load-reset]:not([data-load-text])') ); const loadTimeline = gsap .timeline({ defaults: { ease: "loader", duration: 2.5, }, }) .set(wrap, { display: "block", opacity: 1 }) .to(progressBar, { scaleX: 1 }) .to(logo, { clipPath: "inset(0% 0% 0% 0%)" }, "<") .to(container, { autoAlpha: 0, duration: 0.5 }) .to(progressBar, { scaleX: 0, transformOrigin: "right center", duration: 0.5 }, "<") .add("hideContent", "<") .to(bg, { yPercent: -101, duration: 1 }, "hideContent") .set(wrap, { display: "none" }) .add(() => { if (isHomePage) { gsap.to([header, heroContent], { autoAlpha: 1, }); } }) .add(() => { lenis?.start(); sessionStorage.setItem(HAS_RUN_KEY, "true"); }); if (resetTargets.length) { loadTimeline.set(resetTargets, { autoAlpha: 1 }, 0); } if (textElements.length >= 2) { const firstWord = textElements[0]; const secondWord = textElements[1]; loadTimeline.to( firstWord, { autoAlpha: 1, duration: 0.6, }, 0 ); loadTimeline.to(firstWord, { autoAlpha: 0, duration: 0.4, }, ">+=0.25"); loadTimeline.to(secondWord, { autoAlpha: 1, duration: 0.6, }, ">+=0.1"); loadTimeline.to(secondWord, { autoAlpha: 0, duration: 0.6, }, "hideContent-=0.2"); } } /* Loader Animation */ /* Hide/Show Credits */ const initHideShowCredits = () => { const creditsButton = document.querySelector(".credits_button"); if (!creditsButton) return; const creditsButtonLabel = creditsButton.querySelector(".label"); const credits = document.querySelector(".project_role-details"); if (!credits) return; let isCreditsOpen = false; const BREAKPOINT = 991; const resetCreditsDesktop = () => { if (window.innerWidth >= BREAKPOINT) { gsap.set(credits, { height: "auto", opacity: 1, clearProps: "overflow", }); isCreditsOpen = true; creditsButtonLabel.innerHTML = "Hide Credits"; } }; creditsButton.addEventListener("click", () => { if (!isCreditsOpen) { gsap.to(credits, { height: "auto", duration: 0.13, onComplete: () => { gsap.to(credits, { opacity: 1, duration: 0.13, }); }, }); isCreditsOpen = true; creditsButtonLabel.innerHTML = "Hide Credits"; } else { gsap.to(credits, { opacity: 0, duration: 0.13, onComplete: () => { gsap.to(credits, { height: 0, duration: 0.13, }); }, }); isCreditsOpen = false; creditsButtonLabel.innerHTML = "Show Credits"; } }); resetCreditsDesktop(); window.addEventListener("resize", resetCreditsDesktop); }; /* Hide/Show Credits */ /* Header Animation */ const initHeader = () => { const header = document.querySelector("header"); const nav = document.querySelector(".mobile_nav"); const navToggle = document.querySelector(".header_nav-toggle"); const logo = header?.querySelector(".header_logo-link svg"); const hamburgerIcon = header?.querySelector(".hamburger-icon"); const closeIcon = header?.querySelector(".close-icon"); if (!header || !nav || !navToggle || !logo || !hamburgerIcon || !closeIcon) return; let isNavOpenLocal = false; const SCROLL_OFFSET = 1; const updateLogo = () => { const isScrolled = window.scrollY > SCROLL_OFFSET; if (isNavOpenLocal || isScrolled) header.classList.add("open"); else header.classList.remove("open"); }; window.addEventListener("scroll", () => { header.classList.toggle("scrolled", window.scrollY > SCROLL_OFFSET); updateLogo(); }); const openNav = () => { gsap.to(nav, { autoAlpha: 1, duration: 0.15, pointerEvents: "auto", overwrite: "auto", }); hamburgerIcon.style.display = 'none'; closeIcon.style.display = 'block'; isNavOpenLocal = true; lenis?.stop(); updateLogo(); }; const closeNav = () => { gsap.to(nav, { autoAlpha: 0, duration: 0.15, pointerEvents: "none", overwrite: "auto", }); hamburgerIcon.style.display = 'block'; closeIcon.style.display = 'none'; isNavOpenLocal = false; lenis?.start(); updateLogo(); }; navToggle.addEventListener("click", () => { isNavOpenLocal ? closeNav() : openNav(); }); // ---- helpers for same-page detection + smooth scroll const normalizePath = (p) => { if (!p) return "/"; return p.length > 1 ? p.replace(/\/+$/, "") : p; }; const isSamePageAnchorLink = (anchor) => { if (!anchor || anchor === "#") return false; return true; }; const smoothScrollTo = (hash) => { const id = hash.replace("#", ""); if (!id) return; const target = document.getElementById(id) || document.querySelector(hash); if (!target) return; closeNav(); requestAnimationFrame(() => { if (lenis?.scrollTo) { lenis.scrollTo(target, { offset: 0, duration: 1, }); } else { target.scrollIntoView({ behavior: "smooth", block: "start" }); } }); }; // ---- click handling: close on same-page anchor, otherwise navigate nav.addEventListener("click", (e) => { const link = e.target.closest("a"); if (!link) return; if (!link) { if (isNavOpenLocal) closeNav(); return; } const href = link.getAttribute("href") || ""; if (!href) return; if (!isNavOpenLocal) return; let url; try { url = new URL(href, window.location.href); } catch (_) { return; } const currentPath = normalizePath(window.location.pathname); const linkPath = normalizePath(url.pathname); const isSamePath = currentPath === linkPath; const hasHash = !!url.hash; if (isSamePath && hasHash) { e.preventDefault(); e.stopPropagation(); smoothScrollTo(url.hash); return; } if (href.startsWith("#")) { e.preventDefault(); e.stopPropagation(); smoothScrollTo(href); return; } }); nav.addEventListener("click", (e) => { if (e.target.closest("a")) return; if (isNavOpenLocal) closeNav(); }); }; /* Header Animation */ /* About Nav Animation */ const initAboutNav = () => { const hero = document.querySelector(".section_hero"); const sections = document.querySelectorAll("#about-us, #team, #services, #contact"); const navItems = document.querySelectorAll(".about_nav-item"); const nav = document.querySelector(".about_nav"); const footer = document.querySelector(".footer"); if (!hero || !sections.length || !navItems.length || !nav || !footer) return; gsap.set(navItems, { backdropFilter: "blur(24px)" }); ScrollTrigger.create({ trigger: hero, start: "top top", end: "bottom 80px", markers: false, onLeave: () => { gsap.to(nav, { visibility: "visible", duration: 0.25 }); }, onEnterBack: () => { gsap.to(nav, { visibility: "hidden", duration: 0.25 }); }, }); const setActive = (id) => { navItems.forEach((item) => { item.classList.toggle("is-active", item.dataset.scroll === id); }); }; sections.forEach((section) => { ScrollTrigger.create({ trigger: section, start: "top 50%", markers: false, onEnter: () => setActive(section.id), onEnterBack: () => setActive(section.id), }); }); ScrollTrigger.create({ trigger: footer, start: "top 90%", markers: false, onEnter: () => { gsap.to(nav, { visibility: "hidden", duration: 0.25 }); }, onLeaveBack: () => { gsap.to(nav, { visibility: "visible", duration: 0.25 }); }, }); }; /* About Nav Animation */ /* About Slider */ const initAboutSlider = () => { const swiperEl = document.querySelector(".swiper.is-about"); if (!swiperEl) return; const slidePrevBtn = document.querySelector(".featured_slide-arrow.is-left"); const slideNextBtn = document.querySelector(".featured_slide-arrow.is-right"); const slides = swiperEl.querySelectorAll(".swiper-slide"); new Swiper(swiperEl, { slidesPerView: "auto", grabCursor: true, loop: slides.length > 2 ? true : false, navigation: { nextEl: slideNextBtn, prevEl: slidePrevBtn, }, }); }; /* About Slider */ /* Team Toggle */ const initTeamToggle = () => { const toggles = document.querySelectorAll(".team_item"); const bios = document.querySelectorAll(".bio_list-item"); const details = document.querySelectorAll(".team_item-details"); const images = document.querySelectorAll(".team_image"); const mobiles = document.querySelectorAll(".team_item-mobile"); const mobileTargets = document.querySelectorAll(".team_image-mobile"); if ( !toggles.length || !bios.length || !images.length || !mobiles.length || !mobileTargets.length || !details.length ) return; gsap.set(bios, { display: "none", opacity: 0 }); gsap.set(images, { opacity: 1 }); let mm = gsap.matchMedia(); const setIconState = (index, isOpen) => { const toggle = toggles[index]; if (!toggle) return; const plusIcon = toggle.querySelector(".plus-icon"); const minusIcon = toggle.querySelector(".minus-icon"); if (!plusIcon || !minusIcon) return; if (isOpen) { plusIcon.style.display = "none"; minusIcon.style.display = "block"; } else { plusIcon.style.display = "block"; minusIcon.style.display = "none"; } }; const resetAll = () => { gsap.set(bios, { display: "none", opacity: 0 }); gsap.set(images, { display: "block", opacity: 1 }); gsap.set(mobiles, { display: "none" }); gsap.set(details, { clearProps: "gridArea" }); toggles.forEach((_, i) => setIconState(i, false)); }; mm.add("(min-width: 992px)", () => { let activeIndex = null; resetAll(); const handlers = []; toggles.forEach((toggle, index) => { const handler = () => { if (activeIndex === index) { resetAll(); activeIndex = null; return; } resetAll(); activeIndex = index; setIconState(index, true); gsap.set(bios[index], { display: "block" }); gsap.to(bios[index], { opacity: 1, duration: 0.3 }); images.forEach((image, i) => { gsap.to(image, { opacity: i === index ? 1 : 0.5, duration: 0.3 }); }); }; toggle.addEventListener("click", handler); handlers.push({ toggle, handler }); }); return () => { handlers.forEach(({ toggle, handler }) => toggle.removeEventListener("click", handler) ); resetAll(); activeIndex = null; }; }); mm.add("(max-width: 991px)", () => { let activeIndex = null; resetAll(); const handlers = []; const closeItem = (index) => { if (index === null) return; gsap.set(images[index], { display: "block" }); gsap.set(mobiles[index], { display: "none" }); gsap.set(details[index], { gridArea: "span 1 / span 2 / span 1 / span 2", }); setIconState(index, false); }; toggles.forEach((toggle, index) => { const handler = () => { if (activeIndex === index) { closeItem(activeIndex); activeIndex = null; return; } closeItem(activeIndex); activeIndex = index; setIconState(index, true); gsap.set(images[index], { display: "none" }); gsap.set(mobiles[index], { display: "block" }); gsap.set(details[index], { gridArea: "span 1 / span 3 / span 1 / span 3", }); }; toggle.addEventListener("click", handler); handlers.push({ toggle, handler }); }); return () => { handlers.forEach(({ toggle, handler }) => toggle.removeEventListener("click", handler) ); resetAll(); activeIndex = null; }; }); }; /* Team Toggle */ /* Video Preivew */ const initPlayVideoHover = () => { const wrappers = document.querySelectorAll("[data-video-on-hover]"); wrappers.forEach((wrapper) => { const video = wrapper.querySelector("video"); const src = wrapper.getAttribute("data-video-src") || ""; if (!video || !src) return; let leaveTimer = null; wrapper.addEventListener("mouseenter", () => { if (leaveTimer) { clearTimeout(leaveTimer); leaveTimer = null; } if (!video.getAttribute("src")) { video.setAttribute("src", src); } wrapper.dataset.videoOnHover = "active"; video.play().catch((err) => { console.warn("play on hover is blocked:", err); }); }); wrapper.addEventListener("mouseleave", () => { wrapper.dataset.videoOnHover = "not-active"; leaveTimer = setTimeout(() => { if (wrapper.dataset.videoOnHover !== "active") { video.pause(); video.currentTime = 0; } leaveTimer = null; }, 200); }); }); }; /* Video Preview*/ /* Animations */ const initFadeInAnimation = () => { const elements = document.querySelectorAll("[data-fade-in]"); if (!elements.length) return; elements.forEach((element) => { const delay = parseFloat(element.dataset.delay) || 0; gsap.fromTo( element, { autoAlpha: 0, y: 10 }, { delay, autoAlpha: 1, y: 0, scrollTrigger: { trigger: element, start: ST_Config.start, markers: ST_Config.markers, refreshPriority: 1, }, } ); }); }; const initStaggerAnimation = () => { const elements = document.querySelectorAll("[data-stagger]"); if (!elements.length) return; elements.forEach((element) => { const delay = parseFloat(element.dataset.delay) || 0; const children = element.children; if (children.length === 0) return; gsap.set(children, { autoAlpha: 0, y: 10 }); ScrollTrigger.batch(children, { start: ST_Config.start, markers: ST_Config.markers, refreshPriority: 1, onEnter: (batch) => { gsap.to(batch, { delay, autoAlpha: 1, y: 0, stagger: 0.13, }); }, }); }); }; /* Animation */ /* Checkboxes */ const initProjectsCheckboxes = () => { const section = document.querySelector(".section_selected-work"); if (!section) return; const form = section.querySelector(".filter_form.is-projects"); if (!form) return; const filterList = form.querySelector(".filter_form-list.w-dyn-items"); if (!filterList) return; const fullList = section.querySelector(":scope > .container > .selected_work-wrapper") || section.querySelector(".container > .selected_work-wrapper"); if (!fullList) return; const categoryLists = Array.from( section.querySelectorAll( ".selected_work-categories-wrap .selected_wrap-category-item[data-category]" ) ); const injectAll = () => { if (filterList.querySelector('.filter_form-radio-wrap[data-category="all"]')) return; const allItem = document.createElement("div"); allItem.setAttribute("role", "listitem"); allItem.className = "filter_form-item w-dyn-item"; allItem.innerHTML = ` `; filterList.insertBefore(allItem, filterList.firstChild); }; injectAll(); const controls = Array.from(form.querySelectorAll( ".filter_form-radio-wrap[data-category]")); const setActive = (activeControl) => { controls.forEach((c) => c.classList.remove("is-list-active")); activeControl.classList.add("is-list-active"); }; const setCheckedUI = (activeControl) => { controls.forEach((c) => { c.querySelector(".w-checkbox-input")?.classList.remove("w--redirected-checked"); const input = c.querySelector('input[type="checkbox"]'); if (input) input.checked = false; }); activeControl .querySelector(".w-checkbox-input") ?.classList.add("w--redirected-checked"); const input = activeControl.querySelector('input[type="checkbox"]'); if (input) input.checked = true; }; const showAll = () => { fullList.style.display = ""; categoryLists.forEach((ci) => (ci.style.display = "none")); }; const showCategory = (category) => { fullList.style.display = "none"; categoryLists.forEach((ci) => { const cat = (ci.dataset.category || "").trim().toLowerCase(); const isMatch = cat === category.toLowerCase(); ci.style.setProperty("display", isMatch ? "block" : "none", "important"); }); }; filterList.addEventListener("click", (e) => { const control = e.target.closest(".filter_form-radio-wrap[data-category]"); if (!control) return; e.preventDefault(); e.stopPropagation(); const category = (control.dataset.category || "").trim(); if (!category) return; setActive(control); setCheckedUI(control); if (category === "all") showAll(); else showCategory(category); ScrollTrigger?.refresh(true); }); const defaultControl = controls.find((c) => (c.dataset.category || "") === "all"); if (defaultControl) { setActive(defaultControl); setCheckedUI(defaultControl); } showAll(); }; /* Checkboxes */ /* Services Marquee */ const insertAmpersands = () => { const dynLists = document.querySelectorAll(".section_home-services .w-dyn-list"); const makeAmpItem = (sourceItem) => { const ampItem = sourceItem.cloneNode(true); ampItem.setAttribute("data-amp-item", "true"); const heading = ampItem.querySelector("h1, h2, h3, h4, h5, h6"); if (heading) { heading.textContent = "&"; heading.style.color = "var(--_colors---sky-blue)"; } return ampItem; }; dynLists.forEach((list) => { const items = Array.from(list.querySelectorAll(".w-dyn-item")); if (list.querySelector('[data-amp-item="true"]')) return; if (items.length < 1) return; items.forEach((item, index) => { if (index === items.length - 1) return; item.after(makeAmpItem(item)); }); const lastOriginalItem = items[items.length - 1]; lastOriginalItem.after(makeAmpItem(lastOriginalItem)); }); }; /* Services Marquee */ /* Service Image Hover */ const initServiceImageHover = () => { const services = document.querySelectorAll(".services_item"); const imageTarget = document.querySelector(".service_image-wrap"); if (!services.length || !imageTarget) return; const firstImg = services[0].querySelector("img"); if (firstImg) { imageTarget.src = firstImg.getAttribute("src"); } services.forEach((service) => { const img = service.querySelector("img"); if (!img) return; const src = img.getAttribute("src"); service.addEventListener("mouseenter", () => { gsap.to(imageTarget, { opacity: 0, duration: 0.15, onComplete: () => { imageTarget.src = src; gsap.to(imageTarget, { opacity: 1, duration: 0.2 }); }, }); }); }); }; /* Service Image Hover */ /* Modals */ const initModals = () => { const modals = document.querySelectorAll("[data-modal]"); const triggers = document.querySelectorAll("[data-modal-trigger]"); gsap.set(modals, { autoAlpha: 0 }); const closeModal = (modal) => { if (!modal) return; gsap.to(modal, { autoAlpha: 0, duration: 0.25, ease: "power2.in", }); lenis?.start(); }; triggers.forEach((trigger) => { trigger.addEventListener("click", (e) => { e.preventDefault(); const name = trigger.getAttribute("data-modal-trigger"); const modal = document.querySelector(`[data-modal="${name}"]`); if (!modal) return; gsap.to(modal, { autoAlpha: 1, duration: 0.25, ease: "power2.out", }); lenis?.stop(); }); }); document.addEventListener("click", (e) => { const closeBtn = e.target.closest("[data-modal-close]"); if (!closeBtn) return; e.preventDefault(); closeModal(closeBtn.closest("[data-modal]")); }); document.addEventListener("pointerdown", (e) => { const modal = e.target.closest("[data-modal]"); if (!modal) return; if (e.target.closest(".modal_inner")) return; closeModal(modal); }); window.addEventListener("keydown", (e) => { if (e.key !== "Escape") return; modals.forEach((modal) => { if (gsap.getProperty(modal, "autoAlpha") > 0) { closeModal(modal); } }); }); }; /* Modals */ function watchCalendlyOverlayMobileOnly() { const isMobile = window.innerWidth <= 991; if (!isMobile) return; const obs = new MutationObserver((mutations) => { for (const m of mutations) { for (const node of m.addedNodes) { if (!(node instanceof Element)) continue; if (node.matches?.(".calendly-overlay") || node.querySelector?.( ".calendly-overlay")) { gsap.to(".page-wrapper", { autoAlpha: 0, duration: 0.25, }); } } for (const node of m.removedNodes) { if (!(node instanceof Element)) continue; if (node.matches?.(".calendly-overlay") || node.querySelector?.( ".calendly-overlay")) { gsap.to(".page-wrapper", { autoAlpha: 1, duration: 0.25, }); } } } }); obs.observe(document.documentElement, { childList: true, subtree: true }); return () => obs.disconnect(); } const initDropdown = () => { document.querySelectorAll(".dropdown").forEach((dropdown) => { dropdown.addEventListener("mouseenter", () => dropdown.classList.add("is-open")); dropdown.addEventListener("mouseleave", () => dropdown.classList.remove("is-open")); dropdown.addEventListener("click", (e) => { const link = e.target.closest("a.header_nav-dropdown-link"); if (!link) return; dropdown.classList.remove("is-open"); }); }); }; const initScripts = () => { initBunnyPlayer(); initFeaturedSwiper(); initStillsSlider(); initLogoRevealLoader(); initCursor(); initCalendlyButton(); initHideShowCredits(); initHeader(); initAboutNav(); initAboutSlider(); initTeamToggle(); initHeroVideoToggle(); initFadeInAnimation(); initStaggerAnimation(); initPlayVideoHover(); initProjectsCheckboxes(); insertAmpersands(); initServiceImageHover(); initModals(); watchCalendlyOverlayMobileOnly(); initDropdown(); initShowreelLink(); }; initScripts(); });