Monorepo for Tangled
0

Configure Feed

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

Add wilb.me.js

Wilhelm Berggren (Jul 14, 2026, 12:04 PM +0200) c2025894 a00289e3

+179
+178
appview/pages/profile-fx/wilb.me.js
··· 1 + // fisheye.js — punchcard profile effect 2 + // 3 + // Dots swell as the cursor approaches, like a lens gliding over the grid. 4 + // Scale-only: contribution colors are left untouched so the data stays 5 + // readable under the lens. 6 + 7 + const WIDE_QUERY = "(min-width: 768px)"; 8 + const REDUCED_QUERY = "(prefers-reduced-motion: reduce)"; 9 + 10 + const COLS_NARROW = 28; 11 + const COLS_WIDE = 14; 12 + 13 + const RADIUS = 4.5; // lens radius, in cell units 14 + const MAX_SCALE = 2.4; // scale of a dot directly under the cursor 15 + const EASE = 0.22; // per-frame (at 60hz) approach rate toward target 16 + const REF_HZ = 60; 17 + const MAX_DT = 0.1; // clamp dt across tab-switch gaps 18 + const QUANT = 200; // scale quantization steps (skip redundant writes) 19 + const SETTLE = 0.004; // "close enough to 1" threshold to stop the loop 20 + 21 + const clamp01 = (f) => Math.max(0, Math.min(1, f)); 22 + 23 + // Smooth radial falloff: 1 at the cursor, 0 at RADIUS, C1-continuous at both 24 + // ends (smoothstep), so the lens has no visible rim. 25 + const falloff = (d) => { 26 + const t = clamp01(1 - d / RADIUS); 27 + return t * t * (3 - 2 * t); 28 + }; 29 + 30 + const collectCells = (grid) => { 31 + const cells = Array.from(grid.children, (c) => c.firstElementChild).filter( 32 + Boolean, 33 + ); 34 + cells.forEach((el) => { 35 + el.style.transition = "none"; 36 + el.style.transformOrigin = "center"; 37 + el.style.willChange = "transform"; 38 + }); 39 + return cells; 40 + }; 41 + 42 + const resetCell = (el) => { 43 + el.style.transform = ""; 44 + }; 45 + 46 + const attach = (grid) => { 47 + const ac = new AbortController(); 48 + const opts = { signal: ac.signal }; 49 + const wide = matchMedia(WIDE_QUERY); 50 + 51 + let cells = collectCells(grid); 52 + let scales = new Float32Array(cells.length).fill(1); 53 + let drawn = new Int16Array(cells.length).fill(-1); // quantized last write 54 + let cols = wide.matches ? COLS_WIDE : COLS_NARROW; 55 + let rows = Math.max(1, Math.ceil(cells.length / cols)); 56 + 57 + let pointer = { x: 0, y: 0, over: false }; 58 + let raf = 0; 59 + let running = false; 60 + let visible = true; 61 + let lastMs = 0; 62 + 63 + const relayout = () => { 64 + cells.forEach(resetCell); 65 + cells = collectCells(grid); 66 + scales = new Float32Array(cells.length).fill(1); 67 + drawn = new Int16Array(cells.length).fill(-1); 68 + cols = wide.matches ? COLS_WIDE : COLS_NARROW; 69 + rows = Math.max(1, Math.ceil(cells.length / cols)); 70 + }; 71 + 72 + wide.addEventListener("change", relayout, opts); 73 + 74 + const frame = (ms) => { 75 + const dt = lastMs ? Math.min(MAX_DT, (ms - lastMs) / 1000) : 0; 76 + lastMs = ms; 77 + 78 + // Frame-rate independent easing toward each dot's target scale. 79 + const k = 1 - Math.pow(1 - EASE, dt * REF_HZ); 80 + 81 + const rect = grid.getBoundingClientRect(); 82 + const cw = rect.width / cols; 83 + const ch = rect.height / rows; 84 + 85 + // Cursor position in cell units. 86 + const px = (pointer.x - rect.left) / cw - 0.5; 87 + const py = (pointer.y - rect.top) / ch - 0.5; 88 + 89 + let active = pointer.over; 90 + 91 + for (let i = 0; i < cells.length; i++) { 92 + const dx = (i % cols) - px; 93 + const dy = Math.floor(i / cols) - py; 94 + const target = pointer.over 95 + ? 1 + (MAX_SCALE - 1) * falloff(Math.hypot(dx, dy)) 96 + : 1; 97 + 98 + const s = scales[i] + (target - scales[i]) * k; 99 + scales[i] = s; 100 + if (Math.abs(s - 1) > SETTLE) active = true; 101 + 102 + const q = Math.round((s - 1) * QUANT); 103 + if (q === drawn[i]) continue; 104 + drawn[i] = q; 105 + if (q === 0) { 106 + resetCell(cells[i]); 107 + } else { 108 + cells[i].style.transform = `scale(${(1 + q / QUANT).toFixed(3)})`; 109 + } 110 + } 111 + 112 + if (active && visible) { 113 + raf = requestAnimationFrame(frame); 114 + } else { 115 + running = false; 116 + lastMs = 0; 117 + } 118 + }; 119 + 120 + const wake = () => { 121 + if (running || !visible) return; 122 + running = true; 123 + lastMs = 0; 124 + raf = requestAnimationFrame(frame); 125 + }; 126 + 127 + grid.addEventListener( 128 + "pointermove", 129 + (e) => { 130 + pointer = { x: e.clientX, y: e.clientY, over: true }; 131 + wake(); 132 + }, 133 + { passive: true, signal: ac.signal }, 134 + ); 135 + 136 + grid.addEventListener( 137 + "pointerleave", 138 + () => { 139 + pointer = { ...pointer, over: false }; 140 + wake(); // let dots relax back to rest, then the loop stops itself 141 + }, 142 + opts, 143 + ); 144 + 145 + const io = new IntersectionObserver((entries) => { 146 + visible = entries.some((e) => e.isIntersecting); 147 + if (!visible) { 148 + cancelAnimationFrame(raf); 149 + running = false; 150 + lastMs = 0; 151 + } else { 152 + wake(); 153 + } 154 + }); 155 + io.observe(grid); 156 + 157 + return () => { 158 + cancelAnimationFrame(raf); 159 + io.disconnect(); 160 + ac.abort(); 161 + cells.forEach(resetCell); 162 + }; 163 + }; 164 + 165 + let cleanup = null; 166 + let current = null; 167 + 168 + const init = () => { 169 + if (matchMedia(REDUCED_QUERY).matches) return; 170 + const grid = document.querySelector("[data-punchcard]"); 171 + if (grid === current) return; 172 + if (cleanup) cleanup(); 173 + current = grid; 174 + cleanup = grid ? attach(grid) : null; 175 + }; 176 + 177 + init(); 178 + document.addEventListener("htmx:load", init);
+1
appview/state/profile.go
··· 55 55 56 56 var profileScripts = map[string]string{ 57 57 "did:plc:3fwecdnvtcscjnrx2p4n7alz": "/static/profile-fx/orrery.js", 58 + "did:plc:oxdlsmnvpk2riyyuvq5jtdkd": "/static/profile-fx/wilb.me.js" 58 59 } 59 60 60 61 func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) {