| 1 | // Reasonix — motion & FX layer (all effects respect data-motion + prefers-reduced-motion) |
| 2 | (function () { |
| 3 | const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches; |
| 4 | const rich = () => document.body.dataset.motion === "rich" && !reduced; |
| 5 | |
| 6 | /* spotlight border on cards */ |
| 7 | document.querySelectorAll(".feat, .os-card, .doc-card").forEach((card) => { |
| 8 | card.addEventListener("mousemove", (e) => { |
| 9 | const r = card.getBoundingClientRect(); |
| 10 | card.style.setProperty("--mx", e.clientX - r.left + "px"); |
| 11 | card.style.setProperty("--my", e.clientY - r.top + "px"); |
| 12 | }); |
| 13 | }); |
| 14 | |
| 15 | /* stat counters (run when terminal playback finishes) */ |
| 16 | const counters = Array.from(document.querySelectorAll("[data-cnt]")); |
| 17 | const fmt = (el, v) => { |
| 18 | if (el.dataset.fmt === "hm") { |
| 19 | const m = Math.round(v); |
| 20 | return Math.floor(m / 60) + "h " + (m % 60) + "m"; |
| 21 | } |
| 22 | const dec = +(el.dataset.dec || 0); |
| 23 | return (el.dataset.pre || "") + v.toFixed(dec) + (el.dataset.suf || ""); |
| 24 | }; |
| 25 | const runCounters = () => { |
| 26 | counters.forEach((el) => { |
| 27 | const target = parseFloat(el.dataset.cnt); |
| 28 | if (!rich()) { el.textContent = fmt(el, target); return; } |
| 29 | const t0 = performance.now(), dur = 1500; |
| 30 | const frame = (t) => { |
| 31 | const p = Math.min(1, (t - t0) / dur); |
| 32 | const e = 1 - Math.pow(1 - p, 3); |
| 33 | el.textContent = fmt(el, target * e); |
| 34 | if (p < 1) requestAnimationFrame(frame); |
| 35 | }; |
| 36 | requestAnimationFrame(frame); |
| 37 | }); |
| 38 | }; |
| 39 | document.addEventListener("rx:term-played", runCounters); |
| 40 | })(); |
| 41 |