This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / pages / profile-fx / orrery.js
11 kB 406 lines
1const P = [ 2 { r: 0.22, hue: 350, phase: 0.0 }, 3 { r: 0.37, hue: 25, phase: 1.4 }, 4 { r: 0.53, hue: 140, phase: 2.8 }, 5 { r: 0.7, hue: 205, phase: 4.1 }, 6 { r: 0.88, hue: 280, phase: 5.4 }, 7]; 8 9const H = [320, 50, 170, 230, 95, 285]; 10 11const G = 0.15; 12const m = 0.0016; 13const mn = 0.002; 14const mx = 0.08; 15const M = 0.03; 16const e = 0.006; 17const S = 0.92; 18const s = 1.1; 19const h = 1 / 120; 20const X = 0.1; 21const b = 0.08; 22const k = 0.6; 23const z = 0.25; 24const d = 0.8; 25const fd = 0.92; 26const l = 0.12; 27const F = 0.05; 28const E = 0.12; 29const Q = 1.8; 30const R = 1.7; 31const T = 1.4; 32const K = 2.0; 33const L = 0.16; 34const V = 1.5; 35const B = 12; 36const W = [-2, -1, 0, 1, 2].flatMap((ox) => [-2, -1, 0, 1, 2].map((oy) => [ox, oy])); 37 38const cn = 28; 39const cw = 14; 40const bc = "aspect-square rounded-full max-w-full max-h-full size-[4px] bg-gray-200 dark:bg-gray-700"; 41const wq = "(min-width: 768px)"; 42const dq = "(prefers-color-scheme: dark)"; 43const rq = "(prefers-reduced-motion: reduce)"; 44const q = 64; 45const hz = 60; 46 47const c = (f) => Math.max(0, Math.min(1, f)); 48const r = (a, b) => Array.from({ length: b - a + 1 }, (f, i) => a + i); 49const C = (x, y, f) => { 50 const m = Math.hypot(x, y); 51 return m > f ? { x: (x / m) * f, y: (y / m) * f } : { x, y }; 52}; 53const v = (r) => Math.sqrt((G * r * r) / Math.pow(r * r + e, 1.5)); 54 55const p = (b, tx, ty, gm) => { 56 const dx = tx - b.x; 57 const dy = ty - b.y; 58 const r2 = dx * dx + dy * dy + e; 59 const inv = gm / (r2 * Math.sqrt(r2)); 60 return [dx * inv, dy * inv]; 61}; 62 63const U = (b, all, k) => 64 all.reduce( 65 (acc, o, j) => { 66 if (j === k) return acc; 67 const f = p(b, o.x, o.y, o.mass); 68 return [acc[0] + f[0], acc[1] + f[1]]; 69 }, 70 [0, 0], 71 ); 72 73const a = (b, i, all, n, u) => { 74 const s = p(b, n.x, n.y, G); 75 const m = u.mass > 0 ? p(b, u.x, u.y, u.mass) : [0, 0]; 76 const f = U(b, all, i); 77 return [s[0] + m[0] + f[0], s[1] + m[1] + f[1]]; 78}; 79 80const A = (n, all) => { 81 const g = U(n, all, -1); 82 return [b * g[0] - k * n.x - z * n.vx, b * g[1] - k * n.y - z * n.vy]; 83}; 84 85const I = (sim, u, dt) => { 86 const n = sim.sun; 87 const o = sim.bodies.map((b, i) => { 88 const f = a(b, i, sim.bodies, n, u); 89 const vx = b.vx + f[0] * dt; 90 const vy = b.vy + f[1] * dt; 91 return { ...b, vx, vy, x: b.x + vx * dt, y: b.y + vy * dt }; 92 }); 93 const j = A(n, sim.bodies); 94 const vx = n.vx + j[0] * dt; 95 const vy = n.vy + j[1] * dt; 96 return { bodies: o, sun: { x: n.x + vx * dt, y: n.y + vy * dt, vx, vy } }; 97}; 98 99const O = (sim, u, t) => (t >= h ? O(I(sim, u, h), u, t - h) : { sim, rem: t }); 100 101const y = (b, n) => { 102 const f = Math.hypot(b.x - n.x, b.y - n.y); 103 if (f >= E && Math.hypot(b.x, b.y) <= Q) return { keep: b, ate: 0 }; 104 return { keep: null, ate: f < E ? 1 : 0 }; 105}; 106 107const D = () => 108 P.map((p) => { 109 const f = v(p.r); 110 return { 111 x: p.r * Math.cos(p.phase), 112 y: p.r * Math.sin(p.phase), 113 vx: -Math.sin(p.phase) * f, 114 vy: Math.cos(p.phase) * f, 115 hue: p.hue, 116 mass: m, 117 size: 1, 118 }; 119 }); 120 121const w = (arr) => (arr.length <= B ? arr : arr.slice(arr.length - B)); 122 123const o = (N, j) => { 124 const f = Array.from(N.children, (c) => c.firstElementChild).filter(Boolean); 125 f.forEach((el) => { 126 el.className = bc; 127 el.style.transition = "none"; 128 el.style.transformOrigin = "center"; 129 el.style.willChange = "background-color, transform"; 130 }); 131 return { cells: f, cols: j, rows: Math.max(1, Math.ceil(f.length / j)) }; 132}; 133 134const x = (el) => { 135 el.style.backgroundColor = ""; 136 el.style.transform = ""; 137 el.style.border = ""; 138}; 139 140const g = (N) => { 141 const ac = new AbortController(); 142 const op = { signal: ac.signal }; 143 const mq = matchMedia(wq); 144 const dm = matchMedia(dq); 145 const rd = matchMedia(rq).matches; 146 const cl = () => (mq.matches ? cw : cn); 147 148 N.style.userSelect = "none"; 149 N.style.WebkitUserSelect = "none"; 150 N.style.cursor = "crosshair"; 151 152 let v = o(N, cl()); 153 let dk = dm.matches; 154 let bs = dk ? "rgb(55 65 81)" : "rgb(229 231 235)"; 155 let dl = dk ? 60 : 45; 156 157 const al = (i) => ({ energy: new Float32Array(i), hue: new Float32Array(i) }); 158 let f = al(v.cells.length); 159 let G = al(v.cells.length); 160 let dr = new Float32Array(v.cells.length).fill(-1); 161 let dh = new Float32Array(v.cells.length); 162 163 let sim = { bodies: D(), sun: { x: 0, y: 0, vx: 0, vy: 0 } }; 164 let u = { x: 0, y: 0, over: false, mass: 0 }; 165 let pd = null; 166 let vel = { x: 0, y: 0 }; 167 let lm = 0; 168 let cu = 0; 169 let hp = 0; 170 let fl = 0; 171 let acc = 0; 172 let lt = 0; 173 let raf = 0; 174 let vs = true; 175 176 const rl = () => { 177 v = o(N, cl()); 178 v.cells.forEach(x); 179 f = al(v.cells.length); 180 G = al(v.cells.length); 181 dr = new Float32Array(v.cells.length).fill(-1); 182 dh = new Float32Array(v.cells.length); 183 }; 184 185 const os = () => { 186 dk = dm.matches; 187 bs = dk ? "rgb(55 65 81)" : "rgb(229 231 235)"; 188 dl = dk ? 60 : 45; 189 dr.fill(-1); 190 }; 191 192 mq.addEventListener("change", rl, op); 193 dm.addEventListener("change", os, op); 194 195 const ts = (a, e) => { 196 const cx = (v.cols - 1) / 2; 197 const cy = (v.rows - 1) / 2; 198 if (cx === 0 || cy === 0) return { x: 0, y: 0 }; 199 const rc = N.getBoundingClientRect(); 200 const p = ((a - rc.left) / rc.width) * v.cols - 0.5; 201 const q = ((e - rc.top) / rc.height) * v.rows - 0.5; 202 return { x: (p - cx) / (S * cx), y: (q - cy) / (S * cy) }; 203 }; 204 205 const mv = (E) => { 206 const tt = E.timeStamp / 1000; 207 const s = ts(E.clientX, E.clientY); 208 const dtm = tt - lm; 209 if (dtm > 0 && dtm < 0.1) { 210 vel = { 211 x: vel.x * 0.5 + ((s.x - u.x) / dtm) * 0.5, 212 y: vel.y * 0.5 + ((s.y - u.y) / dtm) * 0.5, 213 }; 214 } 215 lm = tt; 216 u = { ...u, x: s.x, y: s.y, over: true }; 217 }; 218 219 const dn = (E) => { 220 if (E.pointerType === "touch") return; 221 E.preventDefault(); 222 N.setPointerCapture(E.pointerId); 223 mv(E); 224 vel = { x: 0, y: 0 }; 225 pd = { start: E.timeStamp / 1000, hue: H[hp % H.length] }; 226 hp += 1; 227 }; 228 229 const up = (E) => { 230 if (!pd) return; 231 if (N.hasPointerCapture(E.pointerId)) N.releasePointerCapture(E.pointerId); 232 const tt = E.timeStamp / 1000; 233 const c2 = c((tt - pd.start) / T); 234 const fs = tt - lm < 0.09; 235 const lc = C(fs ? vel.x * L : 0, fs ? vel.y * L : 0, V); 236 sim = { 237 ...sim, 238 bodies: w([ 239 ...sim.bodies, 240 { 241 x: u.x, 242 y: u.y, 243 vx: lc.x, 244 vy: lc.y, 245 hue: pd.hue, 246 mass: mn + c2 * (mx - mn), 247 size: 0.8 + c2 * 2.2, 248 }, 249 ]), 250 }; 251 cu = tt + K; 252 pd = null; 253 }; 254 255 N.addEventListener("pointermove", mv, { passive: true, signal: ac.signal }); 256 N.addEventListener("pointerdown", dn, op); 257 N.addEventListener("pointerup", up, op); 258 N.addEventListener( 259 "pointerleave", 260 () => { 261 if (!pd) u = { ...u, over: false }; 262 }, 263 op, 264 ); 265 266 const add = (buf, x, y, hue, e) => { 267 if (x < 0 || x >= v.cols || y < 0 || y >= v.rows) return; 268 const i = y * v.cols + x; 269 if (i >= 0 && i < buf.energy.length && e > buf.energy[i]) { 270 buf.energy[i] = e; 271 buf.hue[i] = hue; 272 } 273 }; 274 275 const bl = (buf, fx, fy, hue, e, rad) => { 276 const bx = Math.round(fx); 277 const by = Math.round(fy); 278 const sp = r(-Math.ceil(rad), Math.ceil(rad)); 279 sp.forEach((ox) => 280 sp.forEach((oy) => { 281 const px = bx + ox; 282 const py = by + oy; 283 const dt = Math.hypot(px - fx, py - fy); 284 if (dt > rad) return; 285 add(buf, px, py, hue, e * (1 - dt / rad)); 286 }), 287 ); 288 }; 289 290 const fm = (ms) => { 291 const t = ms / 1000; 292 const re = lt ? Math.min(X, (ms - lt) / 1000) : 0; 293 lt = ms; 294 const k = re * hz; 295 const dK = Math.pow(d, k); 296 const fK = Math.pow(fd, k); 297 const uK = 1 - Math.pow(1 - l, k); 298 299 const tg = u.over && t >= cu ? M : 0; 300 u = { ...u, mass: u.mass + (tg - u.mass) * uK }; 301 302 acc += re * s; 303 const ot = O(sim, u, acc); 304 acc = ot.rem; 305 const rs = ot.sim.bodies.map((b) => y(b, ot.sim.sun)); 306 sim = { bodies: rs.map((j) => j.keep).filter(Boolean), sun: ot.sim.sun }; 307 const ea = rs.reduce((a, j) => a + j.ate, 0); 308 if (ea > 0) fl = Math.min(1.6, fl + ea * 0.9); 309 fl *= fK; 310 311 const cx = (v.cols - 1) / 2; 312 const cy = (v.rows - 1) / 2; 313 const n = sim.sun; 314 315 f.energy.forEach((j, i) => { 316 G.energy[i] = j * dK; 317 G.hue[i] = f.hue[i]; 318 }); 319 320 const br = 0.88 + 0.12 * Math.sin(t) + fl; 321 const Fx = cx + n.x * S * cx; 322 const Fy = cy + n.y * S * cy; 323 const bX = Math.round(Fx); 324 const bY = Math.round(Fy); 325 W.forEach(([ox, oy]) => { 326 const px = bX + ox; 327 const py = bY + oy; 328 const dt = Math.hypot(px - Fx, py - Fy); 329 if (dt > R) return; 330 add(G, px, py, 45, br * (0.45 + 0.55 * (1 - dt / R))); 331 }); 332 333 sim.bodies.forEach((b) => { 334 const dt = Math.hypot(b.x - n.x, b.y - n.y); 335 const ht = c((0.45 - dt) / 0.45); 336 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); 337 }); 338 339 if (pd) { 340 const c2 = c((t - pd.start) / T); 341 const sz = 0.8 + c2 * 2.2; 342 bl(G, cx + u.x * S * cx, cy + u.y * S * cy, pd.hue, 0.55 + 0.45 * c2, 0.4 + sz * 0.5); 343 } else if (u.over) { 344 add(G, Math.round(cx + u.x * S * cx), Math.round(cy + u.y * S * cy), 200, 0.5); 345 } 346 347 v.cells.forEach((el, i) => { 348 const e = G.energy[i]; 349 if (e >= F) { 350 const Q = Math.round(Math.min(1, e) * q); 351 const hue = G.hue[i]; 352 if (Q !== dr[i] || hue !== dh[i]) { 353 const qe = Q / q; 354 const mi = Math.min(100, qe * 170).toFixed(1); 355 el.style.backgroundColor = `color-mix(in srgb, hsl(${hue} 95% ${dl}%) ${mi}%, ${bs})`; 356 el.style.transform = `scale(${(1 + qe * 0.8).toFixed(3)})`; 357 el.style.border = "0"; 358 dr[i] = Q; 359 dh[i] = hue; 360 } 361 } else if (dr[i] !== -1) { 362 x(el); 363 dr[i] = -1; 364 } 365 }); 366 367 const tp = f; 368 f = G; 369 G = tp; 370 371 if (!rd && vs) raf = requestAnimationFrame(fm); 372 }; 373 374 const io = new IntersectionObserver((en) => { 375 const vi = en.some((E) => E.isIntersecting); 376 if (vi === vs) return; 377 vs = vi; 378 if (vs) { 379 lt = 0; 380 raf = requestAnimationFrame(fm); 381 } else { 382 cancelAnimationFrame(raf); 383 } 384 }); 385 io.observe(N); 386 387 raf = requestAnimationFrame(fm); 388 389 return () => { 390 cancelAnimationFrame(raf); 391 io.disconnect(); 392 ac.abort(); 393 }; 394}; 395 396let Y = null; 397let Z = null; 398const J = () => { 399 const t = document.querySelector("[data-punchcard]"); 400 if (t === Z) return; 401 if (Y) Y(); 402 Z = t; 403 Y = t ? g(t) : null; 404}; 405J(); 406document.addEventListener("htmx:load", J);