Monorepo for Tangled
0

Configure Feed

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

feat: adds fireworks to the punchcard

usamasulaiman (Jul 14, 2026, 1:30 PM +0200) c0c43c0e a00289e3

+477
+476
appview/pages/profile-fx/usaa.ma.js
··· 1 + const FIREWORK_COLORS = [ 2 + "#e42b2b", 3 + "#22c55e", 4 + "#fffb23", 5 + "#f59e0b", 6 + "#3b82f6", 7 + "#ec8f48", 8 + ]; 9 + 10 + const NARROW_COLUMNS = 28; 11 + const WIDE_COLUMNS = 14; 12 + const WIDE_QUERY = "(min-width: 768px)"; 13 + const DARK_QUERY = "(prefers-color-scheme: dark)"; 14 + const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)"; 15 + const BRIGHTNESS_STEPS = 64; 16 + const ENERGY_DECAY_PER_SECOND = 0.03; 17 + const MIN_LAUNCH_DELAY_MS = 500; 18 + const MAX_LAUNCH_DELAY_MS = 2000; 19 + const MAX_ACTIVE_ROCKETS = 3; 20 + const TRAIL_RADIUS = 0.6; 21 + const TRAIL_ENERGY = 0.8; 22 + const EXPLOSION_ENERGY = 1.3; 23 + const MIN_EXPLOSION_RADIUS = 5; 24 + const EXPLOSION_RADIUS_VARIANCE = 3.5; 25 + const MIN_EXPLOSION_DURATION = 1.2; 26 + const EXPLOSION_DURATION_VARIANCE = 0.6; 27 + const MIN_ROCKET_SPEED = 8; 28 + const ROCKET_SPEED_VARIANCE = 4; 29 + const TARGET_HEIGHT_FRACTION = 0.35; 30 + const VISIBLE_ENERGY_THRESHOLD = 0.05; 31 + 32 + const BASE_DOT_CLASS = 33 + "aspect-square rounded-full transition-all duration-300 bg-gray-200 dark:bg-gray-700 size-[4px] max-w-full max-h-full"; 34 + 35 + const MASTER_VOLUME = 0.18; 36 + const LAUNCH_SOUND_DURATION = 0.35; 37 + const LAUNCH_START_FREQUENCY = 140; 38 + const LAUNCH_END_FREQUENCY = 820; 39 + const EXPLOSION_NOISE_DURATION = 2.0; 40 + const EXPLOSION_POP_DURATION = 1.1; 41 + const EXPLOSION_RUMBLE_DURATION = 1.8; 42 + const EXPLOSION_CRACKLE_RATE = 28; 43 + 44 + let sharedAudioContext = null; 45 + 46 + const getAudioContext = () => { 47 + const AudioContextClass = window.AudioContext || window.webkitAudioContext; 48 + if (!AudioContextClass) return null; 49 + if (!sharedAudioContext) sharedAudioContext = new AudioContextClass(); 50 + if (sharedAudioContext.state === "suspended") sharedAudioContext.resume(); 51 + return sharedAudioContext; 52 + }; 53 + 54 + // Browsers block audio until a user gesture happens. We create the 55 + // AudioContext right away (it starts "suspended" if there's been no 56 + // gesture yet) and then resume it on every subsequent interaction, since 57 + // we can't know in advance which gesture the browser will accept. 58 + const GESTURE_EVENTS = ["pointerdown", "keydown", "touchstart", "click"]; 59 + 60 + const unlockAudioOnFirstGesture = () => { 61 + getAudioContext(); 62 + const resume = () => getAudioContext(); 63 + GESTURE_EVENTS.forEach((eventName) => { 64 + window.addEventListener(eventName, resume, { passive: true }); 65 + }); 66 + }; 67 + 68 + // Retro 8-bit "launch" blip: a square wave that sweeps upward in pitch, 69 + // like a rocket taking off in an 80s arcade game. 70 + const playLaunchSound = () => { 71 + const ctx = getAudioContext(); 72 + if (!ctx) return; 73 + const now = ctx.currentTime; 74 + const pitchWobble = 0.85 + Math.random() * 0.3; 75 + 76 + const oscillator = ctx.createOscillator(); 77 + oscillator.type = "square"; 78 + oscillator.frequency.setValueAtTime( 79 + LAUNCH_START_FREQUENCY * pitchWobble, 80 + now, 81 + ); 82 + oscillator.frequency.exponentialRampToValueAtTime( 83 + LAUNCH_END_FREQUENCY * pitchWobble, 84 + now + LAUNCH_SOUND_DURATION, 85 + ); 86 + 87 + const gain = ctx.createGain(); 88 + gain.gain.setValueAtTime(0.0001, now); 89 + gain.gain.exponentialRampToValueAtTime(MASTER_VOLUME, now + 0.02); 90 + gain.gain.exponentialRampToValueAtTime(0.0001, now + LAUNCH_SOUND_DURATION); 91 + 92 + oscillator.connect(gain).connect(ctx.destination); 93 + oscillator.start(now); 94 + oscillator.stop(now + LAUNCH_SOUND_DURATION + 0.05); 95 + }; 96 + 97 + // Retro 8-bit "explosion" blast: a drawn-out, crackly noise crash with a 98 + // descending rumble tail, closer to the classic Duck Hunt gunshot/explosion 99 + // than a quick arcade pop. 100 + const playExplosionSound = () => { 101 + const ctx = getAudioContext(); 102 + if (!ctx) return; 103 + const now = ctx.currentTime; 104 + 105 + // Main noise crash: white noise shaped with a fast attack and a long 106 + // decay. The crackle is baked directly into the buffer as a slow random 107 + // flutter multiplied over the decay curve, so it doesn't rely on 108 + // real-time gain modulation (which can misbehave across browsers). 109 + const bufferLength = Math.floor(ctx.sampleRate * EXPLOSION_NOISE_DURATION); 110 + const noiseBuffer = ctx.createBuffer(1, bufferLength, ctx.sampleRate); 111 + const noiseData = noiseBuffer.getChannelData(0); 112 + const crackleStep = Math.max( 113 + 1, 114 + Math.floor(ctx.sampleRate / EXPLOSION_CRACKLE_RATE), 115 + ); 116 + let crackleFlutter = 1; 117 + for (let i = 0; i < bufferLength; i++) { 118 + if (i % crackleStep === 0) crackleFlutter = 0.55 + Math.random() * 0.45; 119 + const t = i / bufferLength; 120 + const decay = Math.pow(1 - t, 1.6); 121 + noiseData[i] = (Math.random() * 2 - 1) * decay * crackleFlutter; 122 + } 123 + 124 + const noiseSource = ctx.createBufferSource(); 125 + noiseSource.buffer = noiseBuffer; 126 + 127 + const noiseFilter = ctx.createBiquadFilter(); 128 + noiseFilter.type = "lowpass"; 129 + noiseFilter.Q.value = 0.8; 130 + noiseFilter.frequency.setValueAtTime(2600, now); 131 + noiseFilter.frequency.exponentialRampToValueAtTime( 132 + 120, 133 + now + EXPLOSION_NOISE_DURATION, 134 + ); 135 + 136 + const noiseGain = ctx.createGain(); 137 + noiseGain.gain.setValueAtTime(MASTER_VOLUME * 1.5, now); 138 + 139 + noiseSource.connect(noiseFilter).connect(noiseGain).connect(ctx.destination); 140 + noiseSource.start(now); 141 + noiseSource.stop(now + EXPLOSION_NOISE_DURATION); 142 + 143 + // Sharp initial pop for the moment of impact. 144 + const popOscillator = ctx.createOscillator(); 145 + popOscillator.type = "square"; 146 + const popPitchWobble = 0.8 + Math.random() * 0.4; 147 + popOscillator.frequency.setValueAtTime(420 * popPitchWobble, now); 148 + popOscillator.frequency.exponentialRampToValueAtTime( 149 + 60 * popPitchWobble, 150 + now + EXPLOSION_POP_DURATION, 151 + ); 152 + 153 + const popGain = ctx.createGain(); 154 + popGain.gain.setValueAtTime(MASTER_VOLUME, now); 155 + popGain.gain.exponentialRampToValueAtTime( 156 + 0.0001, 157 + now + EXPLOSION_POP_DURATION, 158 + ); 159 + 160 + popOscillator.connect(popGain).connect(ctx.destination); 161 + popOscillator.start(now); 162 + popOscillator.stop(now + EXPLOSION_POP_DURATION + 0.05); 163 + 164 + // Low rumble tail that lingers underneath, giving the explosion weight 165 + // and length rather than cutting off abruptly. 166 + const rumbleOscillator = ctx.createOscillator(); 167 + rumbleOscillator.type = "sine"; 168 + rumbleOscillator.frequency.setValueAtTime(90, now); 169 + rumbleOscillator.frequency.exponentialRampToValueAtTime( 170 + 35, 171 + now + EXPLOSION_RUMBLE_DURATION, 172 + ); 173 + 174 + const rumbleGain = ctx.createGain(); 175 + rumbleGain.gain.setValueAtTime(0.0001, now); 176 + rumbleGain.gain.exponentialRampToValueAtTime(MASTER_VOLUME * 0.9, now + 0.05); 177 + rumbleGain.gain.exponentialRampToValueAtTime( 178 + 0.0001, 179 + now + EXPLOSION_RUMBLE_DURATION, 180 + ); 181 + 182 + rumbleOscillator.connect(rumbleGain).connect(ctx.destination); 183 + rumbleOscillator.start(now); 184 + rumbleOscillator.stop(now + EXPLOSION_RUMBLE_DURATION + 0.05); 185 + }; 186 + 187 + const readGrid = (punchcard, columnCount) => { 188 + const dots = Array.from( 189 + punchcard.children, 190 + (wrapper) => wrapper.firstElementChild, 191 + ).filter(Boolean); 192 + 193 + dots.forEach((dot) => { 194 + // Overwrite the server-rendered classes (which color dots based on 195 + // commit counts) with a neutral base class, so only the animation 196 + // controls what color shows up on the grid. 197 + dot.className = BASE_DOT_CLASS; 198 + dot.style.transition = "none"; 199 + dot.style.transformOrigin = "center"; 200 + dot.style.willChange = "background-color, transform"; 201 + dot.style.backgroundColor = ""; 202 + dot.style.transform = ""; 203 + dot.style.border = ""; 204 + }); 205 + 206 + return { 207 + dots, 208 + columns: columnCount, 209 + rows: Math.max(1, Math.ceil(dots.length / columnCount)), 210 + }; 211 + }; 212 + 213 + const resetDot = (dot) => { 214 + dot.style.backgroundColor = ""; 215 + dot.style.transform = ""; 216 + dot.style.border = ""; 217 + }; 218 + 219 + const createEnergyBuffer = (size) => ({ 220 + energy: new Float32Array(size), 221 + color: new Array(size).fill(null), 222 + }); 223 + 224 + const startFireworks = (punchcard) => { 225 + const abortController = new AbortController(); 226 + const listenerOptions = { signal: abortController.signal }; 227 + const wideQuery = matchMedia(WIDE_QUERY); 228 + const darkQuery = matchMedia(DARK_QUERY); 229 + const reducedMotion = matchMedia(REDUCED_MOTION_QUERY).matches; 230 + const columnsForViewport = () => 231 + wideQuery.matches ? WIDE_COLUMNS : NARROW_COLUMNS; 232 + 233 + punchcard.style.userSelect = "none"; 234 + punchcard.style.WebkitUserSelect = "none"; 235 + 236 + let grid = readGrid(punchcard, columnsForViewport()); 237 + let isDarkMode = darkQuery.matches; 238 + let baseColor = isDarkMode ? "rgb(55 65 81)" : "rgb(229 231 235)"; 239 + 240 + let currentBuffer = createEnergyBuffer(grid.dots.length); 241 + let nextBuffer = createEnergyBuffer(grid.dots.length); 242 + let renderedBrightness = new Float32Array(grid.dots.length).fill(-1); 243 + let renderedColor = new Array(grid.dots.length).fill(null); 244 + 245 + let rockets = []; 246 + let explosions = []; 247 + let nextLaunchTime = 0; 248 + let lastFrameMs = 0; 249 + let animationFrameId = 0; 250 + let isVisible = true; 251 + 252 + const rebuildGrid = () => { 253 + grid = readGrid(punchcard, columnsForViewport()); 254 + currentBuffer = createEnergyBuffer(grid.dots.length); 255 + nextBuffer = createEnergyBuffer(grid.dots.length); 256 + renderedBrightness = new Float32Array(grid.dots.length).fill(-1); 257 + renderedColor = new Array(grid.dots.length).fill(null); 258 + rockets = []; 259 + explosions = []; 260 + }; 261 + 262 + const updateColorScheme = () => { 263 + isDarkMode = darkQuery.matches; 264 + baseColor = isDarkMode ? "rgb(55 65 81)" : "rgb(229 231 235)"; 265 + }; 266 + 267 + wideQuery.addEventListener("change", rebuildGrid, listenerOptions); 268 + darkQuery.addEventListener("change", updateColorScheme, listenerOptions); 269 + 270 + const depositEnergy = (buffer, column, row, color, energy) => { 271 + if (column < 0 || column >= grid.columns || row < 0 || row >= grid.rows) 272 + return; 273 + const index = row * grid.columns + column; 274 + if ( 275 + index >= 0 && 276 + index < buffer.energy.length && 277 + energy > buffer.energy[index] 278 + ) { 279 + buffer.energy[index] = energy; 280 + buffer.color[index] = color; 281 + } 282 + }; 283 + 284 + const depositCircle = ( 285 + buffer, 286 + centerColumn, 287 + centerRow, 288 + color, 289 + energy, 290 + radius, 291 + ) => { 292 + const baseColumn = Math.round(centerColumn); 293 + const baseRow = Math.round(centerRow); 294 + const cells = Math.ceil(radius); 295 + for (let dx = -cells; dx <= cells; dx++) { 296 + for (let dy = -cells; dy <= cells; dy++) { 297 + const column = baseColumn + dx; 298 + const row = baseRow + dy; 299 + const distance = Math.hypot(column - centerColumn, row - centerRow); 300 + if (distance > radius) continue; 301 + depositEnergy( 302 + buffer, 303 + column, 304 + row, 305 + color, 306 + energy * (1 - distance / radius), 307 + ); 308 + } 309 + } 310 + }; 311 + 312 + const launchRocket = (time) => { 313 + if (rockets.length >= MAX_ACTIVE_ROCKETS) return; 314 + const column = Math.floor(Math.random() * grid.columns); 315 + const targetRow = Math.floor( 316 + Math.random() * (grid.rows * TARGET_HEIGHT_FRACTION), 317 + ); 318 + const color = 319 + FIREWORK_COLORS[Math.floor(Math.random() * FIREWORK_COLORS.length)]; 320 + rockets.push({ 321 + column, 322 + row: grid.rows - 1, 323 + targetRow, 324 + color, 325 + startTime: time, 326 + speed: MIN_ROCKET_SPEED + Math.random() * ROCKET_SPEED_VARIANCE, 327 + }); 328 + playLaunchSound(); 329 + }; 330 + 331 + const spawnExplosion = (time, rocket) => { 332 + explosions.push({ 333 + column: rocket.column, 334 + row: rocket.row, 335 + color: rocket.color, 336 + startTime: time, 337 + radius: 0, 338 + maxRadius: 339 + MIN_EXPLOSION_RADIUS + Math.random() * EXPLOSION_RADIUS_VARIANCE, 340 + duration: 341 + MIN_EXPLOSION_DURATION + Math.random() * EXPLOSION_DURATION_VARIANCE, 342 + }); 343 + playExplosionSound(); 344 + }; 345 + 346 + const advanceRockets = (time) => { 347 + rockets = rockets.filter((rocket) => { 348 + const elapsed = time - rocket.startTime; 349 + rocket.row = grid.rows - 1 - elapsed * rocket.speed; 350 + 351 + if (rocket.row <= rocket.targetRow) { 352 + spawnExplosion(time, rocket); 353 + return false; 354 + } 355 + 356 + depositCircle( 357 + nextBuffer, 358 + rocket.column, 359 + rocket.row, 360 + rocket.color, 361 + TRAIL_ENERGY, 362 + TRAIL_RADIUS, 363 + ); 364 + return true; 365 + }); 366 + }; 367 + 368 + const advanceExplosions = (time) => { 369 + explosions = explosions.filter((explosion) => { 370 + const progress = (time - explosion.startTime) / explosion.duration; 371 + if (progress >= 1) return false; 372 + 373 + explosion.radius = explosion.maxRadius * progress; 374 + const intensity = 1 - progress; 375 + depositCircle( 376 + nextBuffer, 377 + explosion.column, 378 + explosion.row, 379 + explosion.color, 380 + intensity * EXPLOSION_ENERGY, 381 + explosion.radius, 382 + ); 383 + return true; 384 + }); 385 + }; 386 + 387 + const renderDots = () => { 388 + grid.dots.forEach((dot, index) => { 389 + const energy = nextBuffer.energy[index]; 390 + if (energy >= VISIBLE_ENERGY_THRESHOLD) { 391 + const brightness = Math.round(Math.min(1, energy) * BRIGHTNESS_STEPS); 392 + const color = nextBuffer.color[index]; 393 + if ( 394 + brightness !== renderedBrightness[index] || 395 + color !== renderedColor[index] 396 + ) { 397 + const brightnessFraction = brightness / BRIGHTNESS_STEPS; 398 + const mixPercent = Math.min(100, brightnessFraction * 170).toFixed(1); 399 + dot.style.backgroundColor = `color-mix(in srgb, ${color} ${mixPercent}%, ${baseColor})`; 400 + dot.style.transform = `scale(${(1 + brightnessFraction * 0.8).toFixed(3)})`; 401 + dot.style.border = "0"; 402 + renderedBrightness[index] = brightness; 403 + renderedColor[index] = color; 404 + } 405 + } else if (renderedBrightness[index] !== -1) { 406 + resetDot(dot); 407 + renderedBrightness[index] = -1; 408 + renderedColor[index] = null; 409 + } 410 + }); 411 + }; 412 + 413 + const tick = (nowMs) => { 414 + const time = nowMs / 1000; 415 + const deltaSeconds = lastFrameMs 416 + ? Math.min(0.1, (nowMs - lastFrameMs) / 1000) 417 + : 0; 418 + lastFrameMs = nowMs; 419 + const decayFactor = Math.pow(ENERGY_DECAY_PER_SECOND, deltaSeconds * 60); 420 + 421 + if (time >= nextLaunchTime) { 422 + launchRocket(time); 423 + nextLaunchTime = 424 + time + 425 + (MIN_LAUNCH_DELAY_MS + 426 + Math.random() * (MAX_LAUNCH_DELAY_MS - MIN_LAUNCH_DELAY_MS)) / 427 + 1000; 428 + } 429 + 430 + // Decay the previous frame's energy into the next buffer first, 431 + // then let rockets/explosions add fresh energy on top of it. 432 + currentBuffer.energy.forEach((energy, index) => { 433 + nextBuffer.energy[index] = energy * decayFactor; 434 + nextBuffer.color[index] = currentBuffer.color[index]; 435 + }); 436 + 437 + advanceRockets(time); 438 + advanceExplosions(time); 439 + renderDots(); 440 + 441 + const swap = currentBuffer; 442 + currentBuffer = nextBuffer; 443 + nextBuffer = swap; 444 + 445 + if (!reducedMotion && isVisible) 446 + animationFrameId = requestAnimationFrame(tick); 447 + }; 448 + 449 + const visibilityObserver = new IntersectionObserver((entries) => { 450 + const nowVisible = entries.some((entry) => entry.isIntersecting); 451 + if (nowVisible === isVisible) return; 452 + isVisible = nowVisible; 453 + if (isVisible) { 454 + lastFrameMs = 0; 455 + animationFrameId = requestAnimationFrame(tick); 456 + } else { 457 + cancelAnimationFrame(animationFrameId); 458 + } 459 + }); 460 + visibilityObserver.observe(punchcard); 461 + 462 + unlockAudioOnFirstGesture(); 463 + 464 + if (!reducedMotion) animationFrameId = requestAnimationFrame(tick); 465 + 466 + return () => { 467 + cancelAnimationFrame(animationFrameId); 468 + visibilityObserver.disconnect(); 469 + abortController.abort(); 470 + }; 471 + }; 472 + 473 + const punchcard = document.querySelector("[data-punchcard]"); 474 + if (punchcard) { 475 + startFireworks(punchcard); 476 + }
+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:d362ytwol467iybrp3a7evxx": "/static/profile-fx/usaa.ma.js", 58 59 } 59 60 60 61 func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) {