Monorepo for Tangled
0

Configure Feed

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

appview/pages: punchcard easter eggs

Lewis: May this revision serve well! <lewis@tangled.org>

Lewis (Jun 30, 2026, 8:06 PM +0300) 797b0cce cace72b5

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