/* Motion engine — reveals (scale/blur), parallax, count-up. Baseado em scroll + rect checks (NÃO depende de IntersectionObserver, que pode não disparar em alguns ambientes). Respeita reduced-motion. */ const REDUCED = window.matchMedia("(prefers-reduced-motion: reduce)").matches; function inView(el, frac) { const r = el.getBoundingClientRect(); const h = window.innerHeight || document.documentElement.clientHeight; return r.top < h * (frac || 0.9) && r.bottom > 0; } /* ---- Reveal ---- */ function revealCheck() { const els = document.querySelectorAll(".reveal:not(.is-in)"); els.forEach((el) => { if (REDUCED || inView(el)) el.classList.add("is-in"); }); } function revealAll() { document.querySelectorAll(".reveal:not(.is-in)").forEach((el) => el.classList.add("is-in")); } /* ---- Count-up ---- */ function animateCount(el) { const raw = el.dataset.count; const target = parseFloat(raw); const dec = raw.indexOf(".") >= 0 ? 1 : 0; if (REDUCED) { el.textContent = target.toFixed(dec); return; } const dur = 1400, start = performance.now(); function tick(now) { const p = Math.min(1, (now - start) / dur); const eased = 1 - Math.pow(1 - p, 3); el.textContent = (target * eased).toFixed(dec); if (p < 1) requestAnimationFrame(tick); else el.textContent = target.toFixed(dec); } requestAnimationFrame(tick); } function counterCheck() { const els = document.querySelectorAll("[data-count]:not([data-counted])"); els.forEach((el) => { if (inView(el, 0.85)) { el.setAttribute("data-counted", ""); animateCount(el); } }); } /* ---- Parallax ---- */ let parallaxEls = []; function collectParallax() { parallaxEls = Array.from(document.querySelectorAll("[data-parallax]")); } function parallaxFrame() { if (REDUCED) return; const vh = window.innerHeight; for (const el of parallaxEls) { const r = el.getBoundingClientRect(); const off = (r.top + r.height / 2 - vh / 2) / vh; const f = parseFloat(el.dataset.parallax) || 0.1; el.style.transform = `translate3d(0, ${(-off * f * 100).toFixed(2)}px, 0)`; } } /* ---- Unified scroll loop ---- */ let ticking = false; function frame() { ticking = false; revealCheck(); counterCheck(); parallaxFrame(); } function onScroll() { if (!ticking) { ticking = true; requestAnimationFrame(frame); } } let wired = false; function initParallax() { collectParallax(); if (!wired) { wired = true; window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", () => { collectParallax(); onScroll(); }, { passive: true }); } onScroll(); } /* Re-run after variant changes inject new nodes */ function refreshMotion() { collectParallax(); revealCheck(); counterCheck(); parallaxFrame(); setTimeout(revealCheck, 120); // failsafe: se NADA revelou (ambiente sem scroll/rAF), libera tudo setTimeout(() => { if (document.querySelectorAll(".reveal.is-in").length === 0) revealAll(); counterCheck(); }, 1800); } window.Motion = { REDUCED, initParallax, refreshMotion, revealCheck, revealAll };