/* ===================================================================== Lottie loader (Webflow-safe): supports .json and .lottie + ?ar= Hero mode: - data-hero-lottie (hero media containers) - optional data-hero-delay="150" (ms) - optional poster: inside container dotlottie-web cold-cache fix: - hero playback starts only after BOTH 'ready' and 'load' events fired ===================================================================== */ (() => { const onReady = (window.__DF_UTILS__ && window.__DF_UTILS__.onReady) || ((fn) => { if (document.readyState !== "loading") fn(); else document.addEventListener("DOMContentLoaded", fn, { once: true }); }); const prefersReducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; /* ------------------------- bodymovin (JSON) ------------------------- */ onReady(() => { if (!window.bodymovin) { console.warn("[Lottie] bodymovin not found."); return; } const orig = bodymovin.loadAnimation; bodymovin.loadAnimation = function (config) { const anim = orig(config); if (config && config.container) config.container.__lottieAnim = anim; return anim; }; }); /* ------------------------- dotlottie-web loader ------------------------- */ let dotLottiePromise = null; function loadDotLottieModule() { if (window.__DOTLOTTIE_MODULE__) return Promise.resolve(window.__DOTLOTTIE_MODULE__); if (!dotLottiePromise) { dotLottiePromise = import("https://cdn.jsdelivr.net/npm/@lottiefiles/dotlottie-web/+esm") .then((m) => { const DotLottie = m.DotLottie || m.default; const DotLottieWorker = m.DotLottieWorker; const mod = { DotLottie, DotLottieWorker }; window.__DOTLOTTIE_MODULE__ = mod; return mod; }) .catch((err) => { console.warn("[dotLottie] Failed to load dotlottie-web:", err); return null; }); } return dotLottiePromise; } function canUseWorker() { return ( typeof window.Worker !== "undefined" && typeof window.OffscreenCanvas !== "undefined" && !!HTMLCanvasElement.prototype.transferControlToOffscreen ); } /* ------------------------- helpers ------------------------- */ function isDotLottiePath(path) { return /\.lottie(\?|#|$)/i.test(path); } function getLottiePath(el) { const inlineEl = el.querySelector(".lottie-data-url"); const inlineUrl = inlineEl?.textContent?.trim() || ""; if (inlineUrl) { inlineEl.style.display = "none"; return inlineUrl; } return (el.getAttribute("data-lottie-src") || "").trim(); } function parseAspectRatioValue(raw) { if (!raw) return null; const v = String(raw).trim().replace(/"/g, ""); if (!v) return null; if (v.includes("/")) { const [a, b] = v.split("/").map((x) => parseFloat(x)); return isFinite(a) && isFinite(b) && b > 0 ? a / b : null; } if (v.includes(":")) { const [a, b] = v.split(":").map((x) => parseFloat(x)); return isFinite(a) && isFinite(b) && b > 0 ? a / b : null; } const n = parseFloat(v); return isFinite(n) && n > 0 ? n : null; } function getAspectRatioFromPath(path) { try { const url = new URL(path, window.location.href); const raw = url.searchParams.get("ar") || url.searchParams.get("aspect") || url.searchParams.get("aspect-ratio") || url.searchParams.get("ratio"); return parseAspectRatioValue(raw); } catch { return null; } } function getDesiredAspectRatio(el, path) { const attr = el.getAttribute("data-aspect-ratio"); const fromAttr = parseAspectRatioValue(attr); if (fromAttr) return fromAttr; const fromUrl = getAspectRatioFromPath(path); return fromUrl || 1; // default square } function getEffectiveDpr(el) { const raw = el.getAttribute("data-lottie-dpr"); const forced = raw ? parseFloat(raw) : NaN; if (isFinite(forced) && forced > 0) return forced; const dpr = window.devicePixelRatio || 1; return dpr > 1 ? dpr * 0.75 : 1; } function getHeroDelay(el) { const raw = el.getAttribute("data-hero-delay"); const n = raw ? parseInt(raw, 10) : 0; return Number.isFinite(n) ? Math.max(0, n) : 0; } function scheduleAfterFirstPaint(fn, delayMs) { requestAnimationFrame(() => { requestAnimationFrame(() => { window.setTimeout(fn, delayMs); }); }); } function showPoster(el) { const poster = el.querySelector(":scope > .lottie-poster, .lottie-poster"); if (!poster) return; poster.style.display = ""; poster.style.opacity = "1"; poster.style.transition = poster.style.transition || "opacity 200ms ease"; } function hidePoster(el) { const poster = el.querySelector(":scope > .lottie-poster, .lottie-poster"); if (!poster) return; poster.style.transition = poster.style.transition || "opacity 200ms ease"; poster.style.opacity = "0"; window.setTimeout(() => { poster.style.display = "none"; }, 220); } function safeCall(res) { // DotLottieWorker returns Promises; DotLottie returns void if (res && typeof res.then === "function") res.catch(() => {}); } /* ------------------------- canvas sizing ------------------------- */ function ensureCanvas(el) { let canvas = el.querySelector(":scope > canvas[data-dotlottie-canvas]") || el.querySelector("canvas[data-dotlottie-canvas]"); if (!canvas) { canvas = document.createElement("canvas"); canvas.setAttribute("data-dotlottie-canvas", "true"); canvas.style.display = "block"; canvas.style.pointerEvents = "none"; canvas.style.position = "absolute"; canvas.style.top = "50%"; canvas.style.left = "50%"; canvas.style.transform = "translate(-50%, -50%)"; el.prepend(canvas); } return canvas; } function attachAspectCanvasResize(el, canvas, getAR) { if (el.__aspectCanvasRO) return; const resize = () => { let w = el.getBoundingClientRect().width; let h = el.getBoundingClientRect().height; if (!w) w = el.offsetWidth || 0; if (!h) h = el.offsetHeight || 0; if (w < 2 || h < 2) return; const ar = Math.max(0.0001, getAR()); const containerAR = w / h; let targetW, targetH; if (containerAR > ar) { targetH = h; targetW = h * ar; } else { targetW = w; targetH = w / ar; } targetW = Math.max(1, Math.floor(targetW)); targetH = Math.max(1, Math.floor(targetH)); canvas.style.width = targetW + "px"; canvas.style.height = targetH + "px"; const dpr = getEffectiveDpr(el); const pxW = Math.max(1, Math.floor(targetW * dpr)); const pxH = Math.max(1, Math.floor(targetH * dpr)); if (canvas.width !== pxW) canvas.width = pxW; if (canvas.height !== pxH) canvas.height = pxH; }; resize(); const ro = new ResizeObserver(resize); ro.observe(el); el.__aspectCanvasRO = ro; window.addEventListener("resize", resize, { passive: true }); } /* ------------------------- JSON init ------------------------- */ function initBodymovin(el, path) { if (!window.bodymovin || !path) return; const isHero = el.hasAttribute("data-hero-lottie"); const playOnHover = el.hasAttribute("data-play-hover") && !isHero; const loopLottie = el.hasAttribute("data-lottie-loop"); const rendererType = el.getAttribute("data-lottie-renderer") || "svg"; const anim = bodymovin.loadAnimation({ container: el, renderer: rendererType, path, rendererSettings: { preserveAspectRatio: "xMidYMid slice" }, loop: !playOnHover && loopLottie, autoplay: !playOnHover && !isHero, }); if (isHero) { showPoster(el); // Cold-cache safety: wait for DOMLoaded then play after paint+delay anim.addEventListener("DOMLoaded", () => { scheduleAfterFirstPaint(() => { hidePoster(el); try { anim.play(); } catch {} }, getHeroDelay(el)); }, { once: true }); return; } if (playOnHover) { const parent = el.closest(".lottie-wrapper-hover") || el; anim.setDirection(1); parent.addEventListener("mouseenter", () => { anim.setDirection(1); anim.play(); }, { passive: true }); parent.addEventListener("mouseleave", () => { anim.setDirection(-1); anim.play(); }, { passive: true }); } } /* ------------------------- .lottie init (dotlottie-web) ------------------------- */ function whenDotLottieReadyAndLoaded(instance, cb) { // Cold-cache fix: require both events (WASM ready + animation load) let ready = false; let loaded = false; let fired = false; const tryFire = () => { if (fired) return; if (ready && loaded) { fired = true; cb(); } }; const onReady = () => { ready = true; tryFire(); }; const onLoad = () => { loaded = true; tryFire(); }; if (instance && instance.addEventListener) { instance.addEventListener("ready", onReady, { once: true }); instance.addEventListener("load", onLoad, { once: true }); } else { // Fallback: if events not available, just proceed soon setTimeout(cb, 300); } // Hard fallback in case any event is missed setTimeout(() => { if (!fired) cb(); }, 6000); } function initDotLottie(el, path) { if (!path) return; const isHero = el.hasAttribute("data-hero-lottie"); const playOnHover = el.hasAttribute("data-play-hover") && !isHero; const loopLottie = el.hasAttribute("data-lottie-loop"); loadDotLottieModule().then((mod) => { if (!mod || el.__dotLottieAnim) return; const { DotLottie, DotLottieWorker } = mod; if (getComputedStyle(el).position === "static") el.style.position = "relative"; el.style.overflow = "hidden"; const canvas = ensureCanvas(el); const desiredAR = getDesiredAspectRatio(el, path); attachAspectCanvasResize(el, canvas, () => desiredAR); const workerDisabled = (el.getAttribute("data-lottie-worker") || "").toLowerCase() === "off"; const useWorker = !!DotLottieWorker && !workerDisabled && canUseWorker(); const Ctor = useWorker ? DotLottieWorker : DotLottie; try { const instance = new Ctor({ canvas, src: path, autoplay: !playOnHover && !isHero, loop: !playOnHover && loopLottie, mode: "forward", renderConfig: { autoResize: false, freezeOnOffscreen: true, }, layout: { fit: "cover", align: [0.5, 0.5] }, }); el.__dotLottieAnim = instance; if (isHero) { showPoster(el); whenDotLottieReadyAndLoaded(instance, () => { scheduleAfterFirstPaint(() => { hidePoster(el); if (instance.setMode) safeCall(instance.setMode("forward")); safeCall(instance.play()); }, getHeroDelay(el)); }); return; } if (playOnHover) { const parent = el.closest(".lottie-wrapper-hover") || el; parent.addEventListener("mouseenter", () => { if (instance.setMode) safeCall(instance.setMode("forward")); safeCall(instance.play()); }, { passive: true }); parent.addEventListener("mouseleave", () => { if (instance.setMode) safeCall(instance.setMode("reverse")); safeCall(instance.play()); }, { passive: true }); } } catch (e) { console.warn("[dotLottie] Init failed:", e); } }); } function initAnyLottie(el) { const path = getLottiePath(el); if (!path) return; if (prefersReducedMotion) { showPoster(el); return; } if (isDotLottiePath(path)) initDotLottie(el, path); else initBodymovin(el, path); } /* ------------------------- boot ------------------------- */ onReady(() => { const candidates = new Set(); document.querySelectorAll(".lottie-element, [data-lottie-src]").forEach((el) => candidates.add(el)); const els = Array.from(candidates); // Init hero immediately (no observer) els.filter((el) => el.hasAttribute("data-hero-lottie")).forEach((el) => { if (getComputedStyle(el).position === "static") el.style.position = "relative"; el.style.overflow = "hidden"; if (!el.__lottieAnim && !el.__dotLottieAnim) initAnyLottie(el); }); // Lazy init non-hero const observer = new IntersectionObserver( (entries, obs) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; const el = entry.target; if (!el.__lottieAnim && !el.__dotLottieAnim) initAnyLottie(el); obs.unobserve(el); }); }, { rootMargin: "0px 0px 200px 0px", threshold: 0.1 } ); els.filter((el) => !el.hasAttribute("data-hero-lottie")).forEach((el) => { if (getComputedStyle(el).position === "static") el.style.position = "relative"; el.style.overflow = "hidden"; if (el.hasAttribute("data-no-wait")) { if (!el.__lottieAnim && !el.__dotLottieAnim) initAnyLottie(el); } else { observer.observe(el); } }); }); })();