From 797b0cce291e4052af61025e7a49f0e7bbee91da Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 30 Jun 2026 17:11:08 +0300 Subject: [PATCH] appview/pages: punchcard easter eggs Lewis: May this revision serve well! --- .gitignore | 1 + appview/pages/pages.go | 1 + appview/pages/static/profile-fx/orrery.js | 404 ++++++++++++++++++ .../pages/templates/layouts/profilebase.html | 5 +- appview/state/profile.go | 7 +- 5 files changed, 416 insertions(+), 2 deletions(-) create mode 100644 appview/pages/static/profile-fx/orrery.js diff --git a/.gitignore b/.gitignore index 3b80065c..3bc4f6b5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ spindle/spindle-microvm-run result !.gitkeep !appview/pages/static/topbar-search.js +!appview/pages/static/profile-fx/ out/ node_modules/ patches diff --git a/appview/pages/pages.go b/appview/pages/pages.go index c6df6954..8fa6ad95 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -681,6 +681,7 @@ type ProfileCard struct { Profile *models.Profile Stats ProfileStats Active string + ProfileScript string } type ProfileStats struct { diff --git a/appview/pages/static/profile-fx/orrery.js b/appview/pages/static/profile-fx/orrery.js new file mode 100644 index 00000000..41837549 --- /dev/null +++ b/appview/pages/static/profile-fx/orrery.js @@ -0,0 +1,404 @@ +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 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.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); diff --git a/appview/pages/templates/layouts/profilebase.html b/appview/pages/templates/layouts/profilebase.html index 2f199999..0ee3c31b 100644 --- a/appview/pages/templates/layouts/profilebase.html +++ b/appview/pages/templates/layouts/profilebase.html @@ -54,6 +54,9 @@ {{ template "user/fragments/profileCard" .Card }} {{ if .Card.Punchcard }} {{ block "punchcard" .Card.Punchcard }} {{ end }} + {{ with .Card.ProfileScript }} + + {{ end }} {{ end }} @@ -110,7 +113,7 @@ {{ .Total | int64 | commaFmt }} commits

-
+
{{ range .Punches }} {{ $count := .Count }} {{ $theme := "bg-gray-200 dark:bg-gray-700 size-[4px]" }} diff --git a/appview/state/profile.go b/appview/state/profile.go index 70b3d7c1..a1a23d71 100644 --- a/appview/state/profile.go +++ b/appview/state/profile.go @@ -53,6 +53,10 @@ func (s *State) Profile(w http.ResponseWriter, r *http.Request) { } } +var profileScripts = map[string]string{ + "did:plc:3fwecdnvtcscjnrx2p4n7alz": "/static/profile-fx/orrery.js", +} + func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) { didOrHandle := chi.URLParam(r, "user") if didOrHandle == "" { @@ -136,7 +140,8 @@ func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) { FollowersCount: followStats.Followers, FollowingCount: followStats.Following, }, - Punchcard: punchcard, + Punchcard: punchcard, + ProfileScript: profileScripts[did], }, nil } -- 2.51.2