git / moonbite-network / static/home-cinematic.js · raw
/* ============================================================================ MoonBite cinematic homepage — starfield, parallax, kinetic headline. Self-contained IIFE. Loaded only on / via home.html scripts_extra (defer). Respects prefers-reduced-motion and degrades gracefully. ========================================================================== */(function () { "use strict"; window.__cineHomeVersion = 3; var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; /* ---------------------------------------------------- STARFIELD */ var canvas = document.getElementById("cineStars"); if (canvas && canvas.getContext) { var ctx = canvas.getContext("2d"); var stars = []; var shooters = []; var w = 0, h = 0, dpr = Math.min(window.devicePixelRatio || 1, 2); var raf = null; function resize() { w = canvas.clientWidth; h = canvas.clientHeight; canvas.width = Math.floor(w * dpr); canvas.height = Math.floor(h * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); buildStars(); } function buildStars() { var count = Math.min(220, Math.floor((w * h) / 6500)); stars = []; for (var i = 0; i < count; i++) { var layer = Math.random(); stars.push({ x: Math.random() * w, y: Math.random() * h, r: layer * 1.6 + 0.3, z: 0.25 + layer * 0.75, // parallax depth tw: Math.random() * Math.PI * 2, // twinkle phase tws: 0.008 + Math.random() * 0.03, hue: Math.random() < 0.18 ? "52,211,255" : (Math.random() < 0.2 ? "124,92,255" : "230,235,255") }); } } function spawnShooter() { if (reduce) return; var fromLeft = Math.random() < 0.5; shooters.push({ x: fromLeft ? -40 : w + 40, y: Math.random() * h * 0.5, vx: (fromLeft ? 1 : -1) * (6 + Math.random() * 5), vy: 2 + Math.random() * 2.5, life: 0, max: 60 + Math.random() * 30 }); } var mx = 0, my = 0, tmx = 0, tmy = 0; function frame() { ctx.clearRect(0, 0, w, h); mx += (tmx - mx) * 0.05; my += (tmy - my) * 0.05; for (var i = 0; i < stars.length; i++) { var s = stars[i]; s.tw += s.tws; var a = 0.35 + Math.abs(Math.sin(s.tw)) * 0.65; var px = s.x + mx * s.z * 22; var py = s.y + my * s.z * 22; ctx.beginPath(); ctx.arc(px, py, s.r, 0, Math.PI * 2); ctx.fillStyle = "rgba(" + s.hue + "," + a.toFixed(3) + ")"; ctx.fill(); if (s.r > 1.2) { ctx.beginPath(); ctx.arc(px, py, s.r * 2.6, 0, Math.PI * 2); ctx.fillStyle = "rgba(" + s.hue + "," + (a * 0.12).toFixed(3) + ")"; ctx.fill(); } } for (var j = shooters.length - 1; j >= 0; j--) { var sh = shooters[j]; sh.life++; sh.x += sh.vx; sh.y += sh.vy; var t = 1 - sh.life / sh.max; if (t <= 0) { shooters.splice(j, 1); continue; } var tailX = sh.x - sh.vx * 6; var tailY = sh.y - sh.vy * 6; var grad = ctx.createLinearGradient(sh.x, sh.y, tailX, tailY); grad.addColorStop(0, "rgba(255,255,255," + (0.9 * t).toFixed(3) + ")"); grad.addColorStop(1, "rgba(124,92,255,0)"); ctx.strokeStyle = grad; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(sh.x, sh.y); ctx.lineTo(tailX, tailY); ctx.stroke(); } raf = requestAnimationFrame(frame); } window.addEventListener("resize", resize, { passive: true }); resize(); if (reduce) { // Paint one static frame, no animation loop. for (var k = 0; k < stars.length; k++) { var st = stars[k]; ctx.beginPath(); ctx.arc(st.x, st.y, st.r, 0, Math.PI * 2); ctx.fillStyle = "rgba(" + st.hue + ",0.8)"; ctx.fill(); } } else { raf = requestAnimationFrame(frame); setInterval(spawnShooter, 4200); // Pause when tab hidden (save cycles) document.addEventListener("visibilitychange", function () { if (document.hidden) { if (raf) cancelAnimationFrame(raf); raf = null; } else if (!raf) { raf = requestAnimationFrame(frame); } }); /* ------------------------------------------ MOUSE PARALLAX */ var scene = document.querySelector(".cine-scene"); var orbit = document.querySelector(".cine-orbit-wrap"); window.addEventListener("mousemove", function (e) { var nx = (e.clientX / window.innerWidth) - 0.5; var ny = (e.clientY / window.innerHeight) - 0.5; tmx = nx; tmy = ny; if (orbit) orbit.style.transform = "translate3d(" + (nx * 26) + "px," + (ny * 20) + "px,0)"; if (scene) scene.style.transform = "translate3d(" + (nx * -10) + "px," + (ny * -8) + "px,0)"; }, { passive: true }); } } /* -------------------------------------------- KINETIC HEADLINE */ var title = document.querySelector(".cine-title"); if (title && !title.dataset.split) { title.dataset.split = "1"; var walk = function (node) { var kids = Array.prototype.slice.call(node.childNodes); kids.forEach(function (child) { if (child.nodeType === 3) { var frag = document.createDocumentFragment(); child.textContent.split(/(\s+)/).forEach(function (part) { if (part.trim() === "") { frag.appendChild(document.createTextNode(part)); return; } var span = document.createElement("span"); span.className = "cine-word"; span.textContent = part; frag.appendChild(span); }); node.replaceChild(frag, child); } else if (child.nodeType === 1) { // keep gradient wrappers, split their text too walk(child); } }); }; walk(title); var words = title.querySelectorAll(".cine-word"); words.forEach(function (word, i) { word.style.animationDelay = (0.15 + i * 0.08) + "s"; }); } /* ------------------------------------------- STAT COUNT-UP PODS */ // Enhance the ticker values with an animated count-up whenever they change. function countUp(el, target) { if (reduce) { el.textContent = target.toLocaleString(); return; } var start = 0, dur = 900, t0 = performance.now(); function step(now) { var p = Math.min((now - t0) / dur, 1); var eased = 1 - Math.pow(1 - p, 3); el.textContent = Math.floor(start + (target - start) * eased).toLocaleString(); if (p < 1) requestAnimationFrame(step); else el.textContent = target.toLocaleString(); } requestAnimationFrame(step); } var podMap = [ ["cinePodHeight", "mbStatHeight"], ["cinePodSupply", "mbStatSupply"], ["cinePodTx", "mbStatTx"], ["cinePodMempool", "mbStatMempool"] ]; // Poll the hidden ticker values (site.js fills them) and mirror into pods. var lastVals = {}; setInterval(function () { podMap.forEach(function (pair) { var pod = document.getElementById(pair[0]); var src = document.getElementById(pair[1]); if (!pod || !src) return; var raw = (src.textContent || "").replace(/[^0-9]/g, ""); if (raw === "" ) return; var n = parseInt(raw, 10); if (lastVals[pair[0]] !== n) { lastVals[pair[0]] = n; countUp(pod, n); } }); }, 1000); /* -------------------------------------- LUNAR SCROLL PROGRESS */ var progFill = document.querySelector(".cine-progress > span"); var progMoon = document.querySelector(".cine-progress-moon"); if (progFill) { function updateProgress() { var doc = document.documentElement; var max = (doc.scrollHeight - doc.clientHeight) || 1; var y = window.scrollY || window.pageYOffset || doc.scrollTop || 0; var p = Math.min(Math.max(y / max, 0), 1); var pct = (p * 100).toFixed(2) + "%"; progFill.style.width = pct; if (progMoon) progMoon.style.left = pct; } window.addEventListener("scroll", updateProgress, { passive: true }); window.addEventListener("resize", updateProgress, { passive: true }); updateProgress(); } /* --------------------------------- EMISSION CURVE: DRAW ON SCROLL */ var chart = document.querySelector(".cine-chart"); var curve = document.querySelector(".cine-curve-line"); if (chart && curve && curve.getTotalLength) { var len = curve.getTotalLength(); curve.style.strokeDasharray = len; if (reduce) { curve.style.strokeDashoffset = 0; chart.classList.add("is-drawn"); } else { curve.style.strokeDashoffset = len; var drawn = false; var drawTick = false; function drawCurve() { var rect = chart.getBoundingClientRect(); var vh = window.innerHeight || 800; // progress: 0 when chart top hits 85% of viewport, 1 when it reaches 25% var start = vh * 0.85, end = vh * 0.25; var p = (start - rect.top) / (start - end); p = Math.min(Math.max(p, 0), 1); curve.style.strokeDashoffset = (len * (1 - p)).toFixed(1); if (p > 0.06) chart.classList.add("is-drawn"); drawTick = false; } window.addEventListener("scroll", function () { if (!drawTick) { drawTick = true; requestAnimationFrame(drawCurve); } }, { passive: true }); window.addEventListener("resize", drawCurve, { passive: true }); drawCurve(); } } /* ------------------------------------------------ SCROLL REVEAL */ var revealEls = document.querySelectorAll(".cine-reveal"); if ("IntersectionObserver" in window && revealEls.length) { var io = new IntersectionObserver(function (entries) { entries.forEach(function (en) { if (en.isIntersecting) { en.target.classList.add("is-visible"); io.unobserve(en.target); } }); }, { threshold: 0.14 }); revealEls.forEach(function (el) { io.observe(el); }); } else { revealEls.forEach(function (el) { el.classList.add("is-visible"); }); } /* ------------------------ SCENE CHOREOGRAPHY (whole-page cinema) */ // Feed each section a live --enter (0 → 1) tied to its viewport position, // so the whole page reads as one continuous flight through space rather // than a stack of static slabs. CSS turns --enter into drift + scale. var sceneEls = document.querySelectorAll(".cine-scene-block"); if (sceneEls.length) { if (reduce) { for (var s = 0; s < sceneEls.length; s++) { sceneEls[s].style.setProperty("--enter", "1"); } } else { var sceneTick = false; var choreograph = function () { var vh = window.innerHeight || 800; var start = vh * 0.96, end = vh * 0.60; for (var i = 0; i < sceneEls.length; i++) { var rect = sceneEls[i].getBoundingClientRect(); var p = (start - rect.top) / (start - end); p = p < 0 ? 0 : (p > 1 ? 1 : p); p = 1 - Math.pow(1 - p, 3); // ease-out for a soft landing sceneEls[i].style.setProperty("--enter", p.toFixed(3)); } sceneTick = false; }; window.addEventListener("scroll", function () { if (!sceneTick) { sceneTick = true; requestAnimationFrame(choreograph); } }, { passive: true }); window.addEventListener("resize", choreograph, { passive: true }); choreograph(); } }})();