const P = [ { r: 0.22, hue: 350, phase: 0.0 }, { r: 0.37, hue: 25, phase: 1.4 }, { r: 0.53, hue: 140, phase: 2.8 }, { r: 0.7, hue: 205, phase: 4.1 }, { r: 0.88, hue: 280, phase: 5.4 }, ]; const H = [320, 50, 170, 230, 95, 285]; const G = 0.15; const m = 0.0016; const mn = 0.002; const mx = 0.08; const M = 0.03; const e = 0.006; const S = 0.92; const s = 1.1; const h = 1 / 120; const X = 0.1; const b = 0.08; const k = 0.6; const z = 0.25; const d = 0.8; const fd = 0.92; const l = 0.12; const F = 0.05; const E = 0.12; const Q = 1.8; const R = 1.7; const T = 1.4; const K = 2.0; const L = 0.16; const V = 1.5; const B = 12; const W = [-2, -1, 0, 1, 2].flatMap((ox) => [-2, -1, 0, 1, 2].map((oy) => [ox, oy])); const cn = 28; const cw = 14; const bc = "aspect-square rounded-full max-w-full max-h-full size-[4px] bg-gray-200 dark:bg-gray-700"; const wq = "(min-width: 768px)"; const dq = "(prefers-color-scheme: dark)"; const rq = "(prefers-reduced-motion: reduce)"; const q = 64; const hz = 60; const c = (f) => Math.max(0, Math.min(1, f)); const r = (a, b) => Array.from({ length: b - a + 1 }, (f, i) => a + i); const C = (x, y, f) => { const m = Math.hypot(x, y); return m > f ? { x: (x / m) * f, y: (y / m) * f } : { x, y }; }; const v = (r) => Math.sqrt((G * r * r) / Math.pow(r * r + e, 1.5)); const p = (b, tx, ty, gm) => { const dx = tx - b.x; const dy = ty - b.y; const r2 = dx * dx + dy * dy + e; const inv = gm / (r2 * Math.sqrt(r2)); return [dx * inv, dy * inv]; }; const U = (b, all, k) => all.reduce( (acc, o, j) => { if (j === k) return acc; const f = p(b, o.x, o.y, o.mass); return [acc[0] + f[0], acc[1] + f[1]]; }, [0, 0], ); const a = (b, i, all, n, u) => { const s = p(b, n.x, n.y, G); const m = u.mass > 0 ? p(b, u.x, u.y, u.mass) : [0, 0]; const f = U(b, all, i); return [s[0] + m[0] + f[0], s[1] + m[1] + f[1]]; }; const A = (n, all) => { const g = U(n, all, -1); return [b * g[0] - k * n.x - z * n.vx, b * g[1] - k * n.y - z * n.vy]; }; const I = (sim, u, dt) => { const n = sim.sun; const o = sim.bodies.map((b, i) => { const f = a(b, i, sim.bodies, n, u); const vx = b.vx + f[0] * dt; const vy = b.vy + f[1] * dt; return { ...b, vx, vy, x: b.x + vx * dt, y: b.y + vy * dt }; }); const j = A(n, sim.bodies); const vx = n.vx + j[0] * dt; const vy = n.vy + j[1] * dt; return { bodies: o, sun: { x: n.x + vx * dt, y: n.y + vy * dt, vx, vy } }; }; const O = (sim, u, t) => (t >= h ? O(I(sim, u, h), u, t - h) : { sim, rem: t }); const y = (b, n) => { const f = Math.hypot(b.x - n.x, b.y - n.y); if (f >= E && Math.hypot(b.x, b.y) <= Q) return { keep: b, ate: 0 }; return { keep: null, ate: f < E ? 1 : 0 }; }; const D = () => P.map((p) => { const f = v(p.r); return { x: p.r * Math.cos(p.phase), y: p.r * Math.sin(p.phase), vx: -Math.sin(p.phase) * f, vy: Math.cos(p.phase) * f, hue: p.hue, mass: m, size: 1, }; }); const w = (arr) => (arr.length <= B ? arr : arr.slice(arr.length - B)); const o = (N, j) => { const f = Array.from(N.children, (c) => c.firstElementChild).filter(Boolean); f.forEach((el) => { el.className = bc; el.style.transition = "none"; el.style.transformOrigin = "center"; el.style.willChange = "background-color, transform"; }); return { cells: f, cols: j, rows: Math.max(1, Math.ceil(f.length / j)) }; }; const x = (el) => { el.style.backgroundColor = ""; el.style.transform = ""; el.style.border = ""; }; const g = (N) => { const ac = new AbortController(); const op = { signal: ac.signal }; const mq = matchMedia(wq); const dm = matchMedia(dq); const rd = matchMedia(rq).matches; const cl = () => (mq.matches ? cw : cn); N.style.userSelect = "none"; N.style.WebkitUserSelect = "none"; N.style.cursor = "crosshair"; let v = o(N, cl()); let dk = dm.matches; let bs = dk ? "rgb(55 65 81)" : "rgb(229 231 235)"; let dl = dk ? 60 : 45; const al = (i) => ({ energy: new Float32Array(i), hue: new Float32Array(i) }); let f = al(v.cells.length); let G = al(v.cells.length); let dr = new Float32Array(v.cells.length).fill(-1); let dh = new Float32Array(v.cells.length); let sim = { bodies: D(), sun: { x: 0, y: 0, vx: 0, vy: 0 } }; let u = { x: 0, y: 0, over: false, mass: 0 }; let pd = null; let vel = { x: 0, y: 0 }; let lm = 0; let cu = 0; let hp = 0; let fl = 0; let acc = 0; let lt = 0; let raf = 0; let vs = true; const rl = () => { v = o(N, cl()); v.cells.forEach(x); f = al(v.cells.length); G = al(v.cells.length); dr = new Float32Array(v.cells.length).fill(-1); dh = new Float32Array(v.cells.length); }; const os = () => { dk = dm.matches; bs = dk ? "rgb(55 65 81)" : "rgb(229 231 235)"; dl = dk ? 60 : 45; dr.fill(-1); }; mq.addEventListener("change", rl, op); dm.addEventListener("change", os, op); const ts = (a, e) => { const cx = (v.cols - 1) / 2; const cy = (v.rows - 1) / 2; if (cx === 0 || cy === 0) return { x: 0, y: 0 }; const rc = N.getBoundingClientRect(); const p = ((a - rc.left) / rc.width) * v.cols - 0.5; const q = ((e - rc.top) / rc.height) * v.rows - 0.5; return { x: (p - cx) / (S * cx), y: (q - cy) / (S * cy) }; }; const mv = (E) => { const tt = E.timeStamp / 1000; const s = ts(E.clientX, E.clientY); const dtm = tt - lm; if (dtm > 0 && dtm < 0.1) { vel = { x: vel.x * 0.5 + ((s.x - u.x) / dtm) * 0.5, y: vel.y * 0.5 + ((s.y - u.y) / dtm) * 0.5, }; } lm = tt; u = { ...u, x: s.x, y: s.y, over: true }; }; const dn = (E) => { if (E.pointerType === "touch") return; E.preventDefault(); N.setPointerCapture(E.pointerId); mv(E); vel = { x: 0, y: 0 }; pd = { start: E.timeStamp / 1000, hue: H[hp % H.length] }; hp += 1; }; const up = (E) => { if (!pd) return; if (N.hasPointerCapture(E.pointerId)) N.releasePointerCapture(E.pointerId); const tt = E.timeStamp / 1000; const c2 = c((tt - pd.start) / T); const fs = tt - lm < 0.09; const lc = C(fs ? vel.x * L : 0, fs ? vel.y * L : 0, V); sim = { ...sim, bodies: w([ ...sim.bodies, { x: u.x, y: u.y, vx: lc.x, vy: lc.y, hue: pd.hue, mass: mn + c2 * (mx - mn), size: 0.8 + c2 * 2.2, }, ]), }; cu = tt + K; pd = null; }; N.addEventListener("pointermove", mv, { passive: true, signal: ac.signal }); N.addEventListener("pointerdown", dn, op); N.addEventListener("pointerup", up, op); N.addEventListener( "pointerleave", () => { if (!pd) u = { ...u, over: false }; }, op, ); const add = (buf, x, y, hue, e) => { if (x < 0 || x >= v.cols || y < 0 || y >= v.rows) return; const i = y * v.cols + x; if (i >= 0 && i < buf.energy.length && e > buf.energy[i]) { buf.energy[i] = e; buf.hue[i] = hue; } }; const bl = (buf, fx, fy, hue, e, rad) => { const bx = Math.round(fx); const by = Math.round(fy); const sp = r(-Math.ceil(rad), Math.ceil(rad)); sp.forEach((ox) => sp.forEach((oy) => { const px = bx + ox; const py = by + oy; const dt = Math.hypot(px - fx, py - fy); if (dt > rad) return; add(buf, px, py, hue, e * (1 - dt / rad)); }), ); }; const fm = (ms) => { const t = ms / 1000; const re = lt ? Math.min(X, (ms - lt) / 1000) : 0; lt = ms; const k = re * hz; const dK = Math.pow(d, k); const fK = Math.pow(fd, k); const uK = 1 - Math.pow(1 - l, k); const tg = u.over && t >= cu ? M : 0; u = { ...u, mass: u.mass + (tg - u.mass) * uK }; acc += re * s; const ot = O(sim, u, acc); acc = ot.rem; const rs = ot.sim.bodies.map((b) => y(b, ot.sim.sun)); sim = { bodies: rs.map((j) => j.keep).filter(Boolean), sun: ot.sim.sun }; const ea = rs.reduce((a, j) => a + j.ate, 0); if (ea > 0) fl = Math.min(1.6, fl + ea * 0.9); fl *= fK; const cx = (v.cols - 1) / 2; const cy = (v.rows - 1) / 2; const n = sim.sun; f.energy.forEach((j, i) => { G.energy[i] = j * dK; G.hue[i] = f.hue[i]; }); const br = 0.88 + 0.12 * Math.sin(t) + fl; const Fx = cx + n.x * S * cx; const Fy = cy + n.y * S * cy; const bX = Math.round(Fx); const bY = Math.round(Fy); W.forEach(([ox, oy]) => { const px = bX + ox; const py = bY + oy; const dt = Math.hypot(px - Fx, py - Fy); if (dt > R) return; add(G, px, py, 45, br * (0.45 + 0.55 * (1 - dt / R))); }); sim.bodies.forEach((b) => { const dt = Math.hypot(b.x - n.x, b.y - n.y); const ht = c((0.45 - dt) / 0.45); bl(G, cx + b.x * S * cx, cy + b.y * S * cy, b.hue, 0.85 + 0.15 * ht, 0.4 + b.size * 0.5); }); if (pd) { const c2 = c((t - pd.start) / T); const sz = 0.8 + c2 * 2.2; bl(G, cx + u.x * S * cx, cy + u.y * S * cy, pd.hue, 0.55 + 0.45 * c2, 0.4 + sz * 0.5); } else if (u.over) { add(G, Math.round(cx + u.x * S * cx), Math.round(cy + u.y * S * cy), 200, 0.5); } v.cells.forEach((el, i) => { const e = G.energy[i]; if (e >= F) { const Q = Math.round(Math.min(1, e) * q); const hue = G.hue[i]; if (Q !== dr[i] || hue !== dh[i]) { const qe = Q / q; const mi = Math.min(100, qe * 170).toFixed(1); el.style.backgroundColor = `color-mix(in srgb, hsl(${hue} 95% ${dl}%) ${mi}%, ${bs})`; el.style.transform = `scale(${(1 + qe * 0.8).toFixed(3)})`; el.style.border = "0"; dr[i] = Q; dh[i] = hue; } } else if (dr[i] !== -1) { x(el); dr[i] = -1; } }); const tp = f; f = G; G = tp; if (!rd && vs) raf = requestAnimationFrame(fm); }; const io = new IntersectionObserver((en) => { const vi = en.some((E) => E.isIntersecting); if (vi === vs) return; vs = vi; if (vs) { lt = 0; raf = requestAnimationFrame(fm); } else { cancelAnimationFrame(raf); } }); io.observe(N); raf = requestAnimationFrame(fm); return () => { cancelAnimationFrame(raf); io.disconnect(); ac.abort(); }; }; let Y = null; let Z = null; const J = () => { const t = document.querySelector("[data-punchcard]"); if (t === Z) return; if (Y) Y(); Z = t; Y = t ? g(t) : null; }; J(); document.addEventListener("htmx:load", J);