Something went wrong. Try again.
atproto plays agar.io
Something went wrong. Try again.
49 kB · 1189 lines · Created 1y ago
HTML
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Spacedust PDS Visualization</title> <style> :root { --background-color: #111827; --text-color: #e5e7eb; --grid-color: #374151; --panel-bg-color: #1f2937; --border-color: #4b5563; --accent-color-live: #22c55e; --accent-color-reconnecting: #f97316; --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
body { font-family: var(--font-family); background-color: var(--background-color); color: var(--text-color); margin: 0; padding: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; }
.container { display: flex; flex-direction: column; align-items: center; width: 100%; max-width: 1200px; padding: 1rem; box-sizing: border-box; }
header { width: 100%; text-align: center; margin-bottom: 1rem; }
h1 { font-size: 2rem; font-weight: 600; margin: 0; color: #f9fafb; }
p.subtitle { font-size: 1rem; color: #9ca3af; margin: 0.25rem 0 0 0; }
main { position: relative; width: 100%; border: 1px solid var(--border-color); border-radius: 0.75rem; overflow: hidden; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
#canvas { display: block; width: 100%; height: auto; aspect-ratio: 4 / 3; background-color: var(--background-color); cursor: pointer; }
.ui-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; padding: 0.75rem; display: flex; justify-content: space-between; align-items: flex-start; pointer-events: none; }
.stats-panel, .controls-panel, .pds-legend-panel { background-color: rgba(31, 41, 55, 0.8); backdrop-filter: blur(8px); padding: 0.75rem; border-radius: 0.5rem; border: 1px solid var(--border-color); font-size: 0.875rem; pointer-events: auto; transition: opacity 0.3s ease-in-out; }
.stats-panel div, .pds-legend-panel div { margin-bottom: 0.25rem; }
.stats-panel strong { color: #9ca3af; width: 80px; display: inline-block; }
#connection-indicator { font-weight: 600; }
.controls-panel { position: absolute; top: 0.75rem; left: 50%; transform: translateX(-50%); display: flex; gap: 1rem; align-items: center; }
.controls-panel button { background-color: #374151; color: var(--text-color); border: 1px solid var(--border-color); padding: 0.5rem 1rem; border-radius: 0.375rem; cursor: pointer; font-family: var(--font-family); font-size: 0.875rem; transition: background-color 0.2s; }
.controls-panel button:hover { background-color: #4b5563; }
.filter-toggle { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; }
.pds-legend-panel { position: absolute; top: 4.5rem; right: 0.75rem; }
.pds-legend-panel h4 { margin: 0 0 0.5rem 0; font-weight: 600; color: #9ca3af; border-bottom: 1px solid var(--border-color); padding-bottom: 0.25rem; font-size: 0.875rem; }
.legend-item { display: flex; align-items: center; font-size: 0.8rem; }
.legend-color { width: 12px; height: 12px; border-radius: 50%; margin-right: 0.5rem; flex-shrink: 0; border: 1px solid rgba(255, 255, 255, 0.2); }
#event-types { margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--border-color); display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.25rem; font-size: 0.8rem; } #event-types span { text-transform: capitalize; }
#tooltip { position: fixed; display: none; background-color: rgba(17, 24, 39, 0.9); color: white; padding: 0.5rem 0.75rem; border-radius: 0.375rem; font-size: 0.875rem; pointer-events: none; border: 1px solid var(--border-color); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); white-space: nowrap; z-index: 100; }
.ui-toggle-container { position: absolute; top: 0.75rem; right: 0.75rem; pointer-events: auto; }
.ui-toggle-container button { background-color: #374151; color: var(--text-color); border: 1px solid var(--border-color); width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-family: var(--font-family); font-size: 0.875rem; transition: background-color 0.2s, opacity 0.3s ease-in-out; display: flex; align-items: center; justify-content: center; } .ui-toggle-container button:hover { background-color: #4b5563; }
body.ui-hidden .stats-panel, body.ui-hidden .controls-panel, body.ui-hidden .pds-legend-panel { opacity: 0; pointer-events: none; }
@media (max-width: 768px) { h1 { font-size: 1.5rem; } .stats-panel, .controls-panel, .pds-legend-panel { font-size: 0.75rem; padding: 0.5rem; } .stats-panel strong { width: 60px; } .controls-panel button { padding: 0.4rem 0.8rem; } .pds-legend-panel { display: none; } /* Hide legend on small screens */ } </style></head><body>
<div class="container"> <header> <h1>Spacedust PDS Visualization</h1> <p class="subtitle">Real-time AT Protocol network activity, visualized.</p> </header>
<main> <canvas id="canvas"></canvas> <div id="tooltip"></div> <div class="ui-overlay"> <div id="stats" class="stats-panel"> <div><strong>Blobs:</strong> 0</div> <div><strong>Events:</strong> 0</div> <div><strong>FPS:</strong> 0.0</div> <div><strong>Frame:</strong> 0.00ms</div> <div id="connection-indicator" style="color: var(--accent-color-reconnecting);">◌ Connecting...</div> <div id="event-types"></div> </div> <div class="controls-panel"> <button id="pause-resume">Pause</button> <label class="filter-toggle"> <input type="checkbox" id="filter-bluesky"> Filter Bluesky PDS </label> <label class="filter-toggle"> <input type="checkbox" id="toggle-labels"> Show Labels </label> </div> <div id="pds-legend" class="pds-legend-panel"> <h4>Top PDS</h4> <div id="pds-legend-list"></div> </div> <div class="ui-toggle-container"> <button id="toggle-ui" title="Toggle UI (h)">👁️</button> </div> </div> </main> </div>
<script type="module"> /** * An enumeration of all known event types, including official bsky lexicons * and community-driven custom lexicons, used for categorization and applying * physics modifiers. */ const EventType = { Post: "post", Like: "like", Repost: "repost", Follow: "follow", Block: "block", Profile: "profile", List: "list", Threadgate: "threadgate", Other: "other", Statusphere: "statusphere", Tangled: "tangled", Pinksea: "pinksea", Anisota: "anisota", Place: "place", Leaflet: "leaflet", Blog: "blog", Unravel: "unravel", Smokesignal: "smokesignal", Skylights: "skylights", Recipe: "recipe" };
/** * Manages application configuration, providing default values for the simulation. * These can be overridden by a global `window.spacedust_config` object for tuning. */ const DEFAULT_CONFIG = { firehoseUrl: 'wss://spacedust.snek.cc/subscribe', canvasWidth: 800, canvasHeight: 600, friction: 0.95, boundaryBounceDamping: 0.8, minBlobMass: 5, maxBlobMass: 500, maxActiveBlobs: 300, baseGrowthMultiplier: 1.5, decayFactor: 0.995, inactivityThresholdMs: 15000, statsUpdateIntervalMs: 200, logLevel: 'info', enableCollisionDetection: true, enableBlobSplitting: true, enableDecay: true, mushroomThreshold: 250, emojiThreshold: 150, }; function loadConfig() { const userConfig = (typeof window !== 'undefined' && window.spacedust_config) || {}; const mergedConfig = { ...DEFAULT_CONFIG, ...userConfig }; console.log('[CONFIG] Spacedust Configuration Loaded:', mergedConfig); const canvas = document.getElementById('canvas'); if(canvas) { const container = canvas.parentElement; if(container) { const aspectRatio = mergedConfig.canvasWidth / mergedConfig.canvasHeight; canvas.style.aspectRatio = `${aspectRatio}`; } } return mergedConfig; } const CONFIG = loadConfig();
/** * A simple structured logger for categorizing console output. */ const LogLevel = { DEBUG: "DEBUG", INFO: "INFO", WARN: "WARN", ERROR: "ERROR" }; class Logger { minLevel = LogLevel.INFO; constructor(minLevel = LogLevel.INFO) { this.minLevel = minLevel; } setMinLevel(level) { this.minLevel = level; } log(level, context, message, data) { const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR]; if (levels.indexOf(level) < levels.indexOf(this.minLevel)) return; const prefix = `[${level}] ${new Date().toLocaleTimeString()} [${context}]`; if (data) console.log(prefix, message, data); else console.log(prefix, message); } debug(context, message, data) { this.log(LogLevel.DEBUG, context, message, data); } info(context, message, data) { this.log(LogLevel.INFO, context, message, data); } warn(context, message, data) { this.log(LogLevel.WARN, context, message, data); } error(context, message, data) { this.log(LogLevel.ERROR, context, message, data); } } /** * Tracks performance metrics, such as frame time, over a sliding window. */ class PerformanceMonitor { metrics = new Map(); maxSamples = 60; record(label, value) { if (!this.metrics.has(label)) this.metrics.set(label, []); const samples = this.metrics.get(label); samples.push(value); if (samples.length > this.maxSamples) samples.shift(); } getStats(label) { const samples = this.metrics.get(label); if (!samples || samples.length === 0) return null; const sorted = [...samples].sort((a, b) => a - b); const avg = sorted.reduce((a, b) => a + b, 0) / sorted.length; return { avg, p95: sorted[Math.floor(sorted.length * 0.95)] }; } } const logger = new Logger(); logger.setMinLevel(CONFIG.logLevel); const perfMonitor = new PerformanceMonitor();
/** * Implements a "dirty flag" and throttling mechanism to prevent redundant * UI updates, which are expensive DOM operations. */ class UIStateManager { flags = { statsNeedUpdate: true, pdsLegendNeedUpdate: true }; lastStatsUpdateTime = 0; lastLegendUpdateTime = 0; minStatsUpdateInterval = CONFIG.statsUpdateIntervalMs; minLegendUpdateInterval = 1000; invalidateStats() { this.flags.statsNeedUpdate = true; } invalidatePdsLegend() { this.flags.pdsLegendNeedUpdate = true; } shouldUpdateStats(currentTime) { if (!this.flags.statsNeedUpdate) return false; if (currentTime - this.lastStatsUpdateTime < this.minStatsUpdateInterval) return false; this.lastStatsUpdateTime = currentTime; this.flags.statsNeedUpdate = false; return true; } shouldUpdatePdsLegend(currentTime) { if (!this.flags.pdsLegendNeedUpdate) return false; if (currentTime - this.lastLegendUpdateTime < this.minLegendUpdateInterval) return false; this.lastLegendUpdateTime = currentTime; this.flags.pdsLegendNeedUpdate = false; return true; } } class OptimizedUIUpdater { constructor(uiManager) { this.uiManager = uiManager; } tryUpdateStats(currentTime, updateFn) { if (this.uiManager.shouldUpdateStats(currentTime)) updateFn(); } tryUpdatePdsLegend(currentTime, updateFn) { if (this.uiManager.shouldUpdatePdsLegend(currentTime)) updateFn(); } }
/** * The single source of truth for all simulation objects (blobs). * Uses Map-based indices for efficient O(1) lookups by ID and hostname. */ class BlobStore { blobs = new Map(); hostnameIndex = new Map(); addBlob(blob) { if (!blob || !blob.id || this.blobs.has(blob.id)) return; this.blobs.set(blob.id, blob); if (blob.pdsHostname) { if (!this.hostnameIndex.has(blob.pdsHostname)) this.hostnameIndex.set(blob.pdsHostname, new Set()); this.hostnameIndex.get(blob.pdsHostname).add(blob.id); } } getBlobById(blobId) { const blob = this.blobs.get(blobId); return blob ? { ...blob } : null; } getBlobsByHostname(hostname) { const ids = this.hostnameIndex.get(hostname); return ids ? Array.from(ids).map(id => this.blobs.get(id)).filter(Boolean) : []; } updateBlob(blobId, updates) { const blob = this.blobs.get(blobId); if (!blob) return null; const updatedBlob = { ...blob, ...updates }; this.blobs.set(blobId, updatedBlob); return { ...updatedBlob }; } removeBlob(blobId) { const blob = this.blobs.get(blobId); if (!blob) return false; if (blob.pdsHostname) this.hostnameIndex.get(blob.pdsHostname)?.delete(blobId); this.blobs.delete(blobId); return true; } getAllBlobs() { return Array.from(this.blobs.values()); } cleanup(minMass, maxAgeSecs) { const now = Date.now(); const toRemove = []; for (const [id, blob] of this.blobs) { const age = (now - blob.lastActive) / 1000; if (blob.mass < minMass || age > maxAgeSecs) toRemove.push(id); } toRemove.forEach(id => this.removeBlob(id)); return toRemove.length; } size() { return this.blobs.size; } }
/** * Contains functions related to the lifecycle of blobs, such as decay and splitting. */ function applyDecay(blobStore) { const now = Date.now(); for (const blob of blobStore.getAllBlobs()) { if (blob.isFiltered) continue; if (now - blob.lastActive > CONFIG.inactivityThresholdMs) { const newMass = blob.mass * CONFIG.decayFactor; blobStore.updateBlob(blob.id, { mass: Math.max(newMass, CONFIG.minBlobMass), size: Math.sqrt(Math.max(newMass, CONFIG.minBlobMass)) }); } } } function cleanupDeadBlobs(blobStore) { const removedCount = blobStore.cleanup(CONFIG.minBlobMass, 300); if (removedCount > 0) logger.debug('Lifecycle', `Cleaned up ${removedCount} dead blobs.`); } function findSafestSplitDirection(blob, blobStore) { const checkAngles = 8; let bestAngle = Math.random() * Math.PI * 2; let maxClosestDist = 0;
const activeBlobs = blobStore.getAllBlobs().filter(b => !b.isFiltered);
for (let i = 0; i < checkAngles; i++) { const angle = (i / checkAngles) * Math.PI * 2; const checkX = blob.x + Math.cos(angle) * blob.size * 3; const checkY = blob.y + Math.sin(angle) * blob.size * 3;
let closestDistSq = Infinity; for (const other of activeBlobs) { if (blob.id === other.id) continue; const distSq = (checkX - other.x)**2 + (checkY - other.y)**2; if (distSq < closestDistSq) { closestDistSq = distSq; } } if (closestDistSq > maxClosestDist) { maxClosestDist = closestDistSq; bestAngle = angle; } } return bestAngle; } function splitBlobIfNeeded(blobStore, blobId) { const blob = blobStore.getBlobById(blobId); if (!blob || blob.isFiltered || blob.mass < CONFIG.maxBlobMass) return;
const childMass = blob.mass / 2; blobStore.updateBlob(blobId, { mass: childMass, size: Math.sqrt(childMass) });
const angle = findSafestSplitDirection(blob, blobStore); const impulse = 5;
const childBlob = { ...blob, id: `${blob.id}-child-${Date.now()}`, mass: childMass, size: Math.sqrt(childMass), isChild: true, parentId: blobId, vx: blob.vx + Math.cos(angle) * impulse, vy: blob.vy + Math.sin(angle) * impulse }; blobStore.addBlob(childBlob); logger.info('Lifecycle', `Blob ${blobId} split into child ${childBlob.id}`); }
/** * Implements an O(n log n) collision detection system using a QuadTree. */ class Rectangle { constructor(x, y, w, h) { this.x = x; this.y = y; this.w = w; this.h = h; } contains(blob) { return blob.x >= this.x && blob.x < this.x + this.w && blob.y >= this.y && blob.y < this.y + this.h; } intersects(other) { return !(other.x > this.x + this.w || other.x + other.w < this.x || other.y > this.y + this.h || other.y + other.h < this.y); } } class QuadTree { blobs = []; divided = false; constructor(boundary, capacity = 8) { this.boundary = boundary; this.capacity = capacity; } insert(blob) { if (!this.boundary.contains(blob)) return false; if (this.blobs.length < this.capacity) { this.blobs.push(blob); return true; } if (!this.divided) this.subdivide(); return this.northeast.insert(blob) || this.northwest.insert(blob) || this.southeast.insert(blob) || this.southwest.insert(blob); } query(range, found = []) { if (!this.boundary.intersects(range)) return found; for (const blob of this.blobs) { if (range.contains(blob)) found.push(blob); } if (this.divided) { this.northeast.query(range, found); this.northwest.query(range, found); this.southeast.query(range, found); this.southwest.query(range, found); } return found; } subdivide() { const { x, y, w, h } = this.boundary; const hw = w / 2, hh = h / 2; this.northeast = new QuadTree(new Rectangle(x + hw, y, hw, hh), this.capacity); this.northwest = new QuadTree(new Rectangle(x, y, hw, hh), this.capacity); this.southeast = new QuadTree(new Rectangle(x + hw, y + hh, hw, hh), this.capacity); this.southwest = new QuadTree(new Rectangle(x, y + hh, hw, hh), this.capacity); this.divided = true; } } function findCollisions(blobStore, canvasWidth, canvasHeight) { const activeBlobs = blobStore.getAllBlobs().filter(b => !b.isFiltered); const qtree = new QuadTree(new Rectangle(0, 0, canvasWidth, canvasHeight)); for (const blob of activeBlobs) qtree.insert(blob); const collisions = []; const checked = new Set(); for (const blob of activeBlobs) { const range = new Rectangle(blob.x - blob.size, blob.y - blob.size, blob.size * 2, blob.size * 2); const nearby = qtree.query(range); for (const other of nearby) { if (blob.id === other.id) continue; const pairKey = [blob.id, other.id].sort().join('-'); if (checked.has(pairKey)) continue; checked.add(pairKey); const dx = blob.x - other.x, dy = blob.y - other.y; if (dx * dx + dy * dy < (blob.size + other.size) ** 2) { collisions.push([blob, other]); } } } return collisions; } function handleCollision(blobStore, blob1, blob2) { const winner = blob1.mass > blob2.mass ? blob1 : blob2; const loser = blob1.mass > blob2.mass ? blob2 : blob1;
if (blob1.pdsHostname === blob2.pdsHostname) { const massToTransfer = loser.mass * 0.25; blobStore.updateBlob(winner.id, { mass: winner.mass + massToTransfer, size: Math.sqrt(winner.mass + massToTransfer), pulseStartTime: Date.now() }); const remainingMass = loser.mass - massToTransfer; if (remainingMass < CONFIG.minBlobMass) { blobStore.removeBlob(loser.id); } else { blobStore.updateBlob(loser.id, { mass: remainingMass, size: Math.sqrt(remainingMass) }); } return; }
blobStore.updateBlob(winner.id, { mass: winner.mass + loser.mass, size: Math.sqrt(winner.mass + loser.mass), pulseStartTime: Date.now() }); blobStore.removeBlob(loser.id); }
/** * Handles all rendering to the HTML canvas and updating UI panel information. */ function renderFrame(ctx, canvas, blobStore, showLabels) { canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; ctx.fillStyle = '#111827'; ctx.fillRect(0, 0, canvas.width, canvas.height); drawGrid(ctx, canvas.width, canvas.height); const allBlobs = blobStore.getAllBlobs(); allBlobs.sort((a, b) => a.mass - b.mass); for (const blob of allBlobs) drawBlob(ctx, blob, showLabels); } function drawGrid(ctx, width, height) { ctx.strokeStyle = '#374151'; ctx.lineWidth = 0.5; ctx.beginPath(); for (let x = 0; x <= width; x += 40) { ctx.moveTo(x, 0); ctx.lineTo(x, height); } for (let y = 0; y <= height; y += 40) { ctx.moveTo(0, y); ctx.lineTo(width, y); } ctx.stroke(); } function drawBlob(ctx, blob, showLabels) { if (blob.size < 1) return;
let currentSize = blob.size; const now = Date.now();
ctx.save();
if (blob.isFiltered) { ctx.globalAlpha = 0.2; }
if (blob.shakeUntil && now < blob.shakeUntil) { const shakeIntensity = 4; ctx.translate(Math.random() * shakeIntensity - shakeIntensity / 2, Math.random() * shakeIntensity - shakeIntensity / 2); }
if (blob.pulseStartTime) { const pulseDuration = 500; const pulseAge = now - blob.pulseStartTime; if (pulseAge < pulseDuration) { const pulseProgress = pulseAge / pulseDuration; const pulseFactor = 1 + Math.sin(pulseProgress * Math.PI) * 0.3; currentSize *= pulseFactor; } }
const lighterColor = lightenHslColor(blob.color, 20); const gradient = ctx.createRadialGradient(blob.x, blob.y, 0, blob.x, blob.y, currentSize); gradient.addColorStop(0, lighterColor); gradient.addColorStop(1, blob.color);
ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(blob.x, blob.y, currentSize, 0, Math.PI * 2); ctx.fill();
if (showLabels && !blob.isFiltered) { const fontSize = Math.max(8, Math.min(14, blob.size / 4)); ctx.font = `bold ${fontSize}px sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'white'; ctx.strokeStyle = 'black'; ctx.lineWidth = 2; ctx.strokeText(blob.pdsHostname, blob.x, blob.y); ctx.fillText(blob.pdsHostname, blob.x, blob.y); }
if (blob.isBlueskyPDS && blob.mass > CONFIG.mushroomThreshold) { ctx.font = `${blob.size * 1.2}px sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('🍄', blob.x, blob.y); } else if (blob.emoji && blob.mass > CONFIG.emojiThreshold) { ctx.font = `${blob.size * 1.2}px sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(blob.emoji, blob.x, blob.y); }
if (blob.shimmerUntil && now < blob.shimmerUntil) { ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; ctx.fill(); }
if (blob.glow && !blob.isFiltered) { ctx.strokeStyle = lighterColor; ctx.lineWidth = 3; ctx.globalAlpha = ctx.globalAlpha * 0.7; ctx.stroke(); } ctx.restore(); } function lightenHslColor(hsl, percent) { const parts = hsl.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/); if (!parts) return hsl; const l = Math.min(100, parseInt(parts[3]) + percent); return `hsl(${parts[1]}, ${parts[2]}%, ${l}%)`; } function updateStatsUI(stats, eventCounts) { const el = document.getElementById('stats'); if (el) { el.innerHTML = ` <div><strong>Blobs:</strong> ${stats.blobCount.toLocaleString()}</div> <div><strong>Events:</strong> ${stats.eventCount.toLocaleString()}</div> <div><strong>FPS:</strong> ${stats.fps.toFixed(1)}</div> <div><strong>Frame:</strong> ${stats.frameTime.toFixed(2)}ms</div> <div id="connection-indicator"></div> <div id="event-types"></div>`; updateEventCountersUI(eventCounts); } } function updateEventCountersUI(eventCounts) { const el = document.getElementById('event-types'); if (el) { el.innerHTML = Object.entries(eventCounts) .sort(([, a], [, b]) => b - a) .map(([type, count]) => `<div><span>${type}:</span> <strong>${count.toLocaleString()}</strong></div>`) .join(''); } } function updatePdsLegendUI(blobStore) { const el = document.getElementById('pds-legend-list'); if (!el) return;
const pdsMasses = new Map(); const activeBlobs = blobStore.getAllBlobs().filter(blob => !blob.isFiltered);
for (const blob of activeBlobs) { if (blob.pdsHostname) { const current = pdsMasses.get(blob.pdsHostname) || { mass: 0, color: blob.color }; current.mass += blob.mass; pdsMasses.set(blob.pdsHostname, current); } }
const topPDS = Array.from(pdsMasses.entries()) .sort(([, a], [, b]) => b.mass - a.mass) .slice(0, 5);
el.innerHTML = topPDS.map(([hostname, { color }]) => ` <div class="legend-item"> <div class="legend-color" style="background-color: ${color};"></div> <span>${hostname}</span> </div> `).join(''); } function updateConnectionIndicator(isConnected) { const el = document.getElementById('connection-indicator'); if (el) { el.innerHTML = isConnected ? `<strong style="color: var(--accent-color-live);">● Live</strong>` : `<strong style="color: var(--accent-color-reconnecting);">◌ Reconnecting...</strong>`; } }
/** * Networking/api code * Handles network requests for the application. */ const didToPdsCache = new Map();
async function resolveDID(did) { if (didToPdsCache.has(did)) { return didToPdsCache.get(did); } if (!did || !did.startsWith('did:')) { didToPdsCache.set(did, null); return null; }
try { const res = await fetch(`https://plc.directory/${did}`); if (!res.ok) { logger.warn('DID Resolver', `Failed to resolve ${did}, status: ${res.status}`); didToPdsCache.set(did, null); return null; } const data = await res.json(); const service = data.service?.find(s => s.id === '#atproto_pds'); if (service?.serviceEndpoint) { const hostname = new URL(service.serviceEndpoint).hostname; didToPdsCache.set(did, hostname); return hostname; } didToPdsCache.set(did, null); return null; } catch (e) { logger.error('DID Resolver', `Error resolving ${did}`, e); didToPdsCache.set(did, null); return null; } } async function* subscribeToSpacedust() { let reconnectAttempts = 0; while (true) { let ws; try { logger.info('Spacedust', 'Connecting...'); ws = new WebSocket(CONFIG.firehoseUrl); const messageQueue = []; let resolvePromise = null;
ws.onopen = () => { logger.info('Spacedust', 'Connection established.'); updateConnectionIndicator(true); reconnectAttempts = 0; }; ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.kind === 'link' && msg.link) { if (resolvePromise) { resolvePromise(msg.link); resolvePromise = null; } else { messageQueue.push(msg.link); } } } catch (e) { logger.warn('Spacedust', 'Failed to parse message', e); } }; ws.onerror = (err) => { logger.error('Spacedust', 'WebSocket error', err); }; ws.onclose = () => { logger.warn('Spacedust', 'Connection closed.'); updateConnectionIndicator(false); };
while (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { if (messageQueue.length > 0) { yield messageQueue.shift(); } else { yield new Promise(resolve => { resolvePromise = resolve; }); } await new Promise(res => setTimeout(res, 10)); // Prevent tight loop }
} catch (e) { logger.error('Spacedust', 'Subscription error', e); } finally { if (ws && ws.readyState !== WebSocket.CLOSED) ws.close(); updateConnectionIndicator(false); const delay = Math.min(30000, Math.pow(2, reconnectAttempts) * 1000); logger.warn('Spacedust', `Reconnecting in ${delay / 1000}s`); await new Promise(res => setTimeout(res, delay)); reconnectAttempts++; } } }
/** * Spacedust event processing * Contains the core logic for processing a single event from the firehose. */ const pdsEmojiMap = [ { keywords: ['atproto.brid.gy'], emoji: '🌉' }, { keywords: ['ripperoni'], emoji: '🍕' }, { keywords: ['lab.martianbase.net'], emoji: '🇵🇱' }, { keywords: ['keik.info'], emoji: '🇯🇵' }, { keywords: ['tngl.sh'], emoji: '🧶' }, { keywords: ['blacksky.app', 'witchcraft.systems', 'altq.net', 'selfhosted.social', 'peedee.es'], emoji: '☀️' }, { keywords: ['wafrn.net'], emoji: '👽' }, { keywords: ['sprk.so'], emoji: '📱' }, { keywords: ['esnoticia.online', 'marta.fail', 'climateai.org', 'dev-frosting.social', 'akizuki.best', 'shendrick.net'], emoji: '🤖' }, { keywords: ['gay', 'lgbt', 'queer', 'tgirl', 'trans', 'barbequeer'], emoji: '🏳️⚧️' }, { keywords: ['cat', 'meow', 'mraow', 'kat'], emoji: '🐈' }, { keywords: ['dog', 'doggos', 'puppy', 'pup'], emoji: '🐕' }, { keywords: ['fox', 'vixen', 'vulpine'], emoji: '🦊' }, { keywords: ['witch'], emoji: '🧙' }, { keywords: ['climate'], emoji: '🌍' }, { keywords: ['computer', 'dev', 'tech', 'systems', 'codes', 'cloud', 'host'], emoji: '💻' }, { keywords: ['art', 'design', 'photo'], emoji: '🎨' }, { keywords: ['social'], emoji: '👥' }, { keywords: ['blue', 'sky'], emoji: '🌌' }, { keywords: ['cafe', 'coffee'], emoji: '☕' }, { keywords: ['garden'], emoji: '🌱' }, { keywords: ['party'], emoji: '🎉' }, { keywords: ['lol', 'wtf', 'fail'], emoji: '😂' }, { keywords: ['fish'], emoji: '🐠' }, { keywords: ['mom'], emoji: '👩👧' }, { keywords: ['geese', 'bird'], emoji: '🦢' }, { keywords: ['dead'], emoji: '💀' }, { keywords: ['evil'], emoji: '😈' }, { keywords: ['moe'], emoji: '🌸' }, ];
function getEmojiForPDS(hostname) { if (!hostname) return null; for (const rule of pdsEmojiMap) { if (rule.keywords.some(kw => hostname.includes(kw))) { return rule.emoji; } } return null; }
function isBlueskyPDS(hostname) { if (typeof hostname !== 'string' || hostname.length === 0) { return false; } const lowerHostname = hostname.toLowerCase().trim(); return lowerHostname === 'bsky.social' || lowerHostname.endsWith('.bsky.network'); }
function createPDSBlob(pdsHostname, canvas, isFiltered) { const hash = pdsHostname.split('').reduce((acc, char) => char.charCodeAt(0) + ((acc << 5) - acc), 0); const color = `hsl(${hash % 360}, 70%, 50%)`; return { id: `${pdsHostname}-${crypto.randomUUID()}`, pdsHostname: pdsHostname, dids: [], x: Math.random() * canvas.clientWidth, y: Math.random() * canvas.clientHeight, vx: 0, vy: 0, mass: CONFIG.minBlobMass, size: Math.sqrt(CONFIG.minBlobMass), color: color, lastActive: Date.now(), glow: false, isBlueskyPDS: isBlueskyPDS(pdsHostname), isFiltered: isFiltered, emoji: getEmojiForPDS(pdsHostname), }; }
function getEventTypeFromSource(sourceString) { if (!sourceString) return EventType.Other; if (sourceString.includes('bsky.feed.like')) return EventType.Like; if (sourceString.includes('bsky.feed.post')) return EventType.Post; if (sourceString.includes('bsky.feed.repost')) return EventType.Repost; if (sourceString.includes('bsky.graph.follow')) return EventType.Follow; if (sourceString.includes('bsky.graph.block')) return EventType.Block; if (sourceString.includes('bsky.actor.profile')) return EventType.Profile; if (sourceString.includes('bsky.graph.list')) return EventType.List; if (sourceString.includes('bsky.feed.threadgate')) return EventType.Threadgate; if (sourceString.includes('xyz.statusphere.status')) return EventType.Statusphere; if (sourceString.includes('sh.tangled.')) return EventType.Tangled; if (sourceString.includes('art.pinksea.art')) return EventType.Pinksea; if (sourceString.includes('net.anisota.thread')) return EventType.Anisota; if (sourceString.includes('place.stream.broadcast')) return EventType.Place; if (sourceString.includes('pub.leaflet.publication')) return EventType.Leaflet; if (sourceString.includes('com.whtwnd.blog.entry')) return EventType.Blog; if (sourceString.includes('fyi.unravel.frontpage.story')) return EventType.Unravel; if (sourceString.includes('events.smokesignal.signal')) return EventType.Smokesignal; if (sourceString.includes('my.skylights.review')) return EventType.Skylights; if (sourceString.includes('exchange.recipe.recipe')) return EventType.Recipe; return EventType.Other; }
function calculateGrowth(eventType) { const factors = { [EventType.Post]: 2.0, [EventType.Follow]: 2.5, [EventType.Like]: 1.0, [EventType.Repost]: 1.5, [EventType.Block]: 3.0, [EventType.Profile]: 1.0, [EventType.List]: 3.5, [EventType.Threadgate]: 4.0, [EventType.Statusphere]: 2.2, [EventType.Tangled]: 2.8, [EventType.Pinksea]: 3.0, [EventType.Anisota]: 2.7, [EventType.Place]: 2.9, [EventType.Leaflet]: 2.6, [EventType.Blog]: 2.4, [EventType.Unravel]: 3.2, [EventType.Smokesignal]: 3.1, [EventType.Skylights]: 2.3, [EventType.Recipe]: 2.1, [EventType.Other]: 0.2 }; return (factors[eventType] || 0.5) * CONFIG.baseGrowthMultiplier; }
async function handleSpacedustEvent(event, blobStore, canvas, isBlueskyFilterEnabled) { const eventType = getEventTypeFromSource(event.source); try { const atUri = event.source_record; if (!atUri || !atUri.startsWith('at://')) return eventType;
const didMatch = atUri.match(/at:\/\/(did:[^/]+)/); if (!didMatch) return eventType; const did = didMatch[1];
const pdsHostname = await resolveDID(did); if (!pdsHostname) return eventType;
if (isBlueskyFilterEnabled && isBlueskyPDS(pdsHostname)) { return eventType; }
let blobs = blobStore.getBlobsByHostname(pdsHostname);
if (blobs.length === 0) { if (blobStore.size() >= CONFIG.maxActiveBlobs) { let oldestBlob = null; for (const b of blobStore.getAllBlobs()) { if (!oldestBlob || b.lastActive < oldestBlob.lastActive) oldestBlob = b; } if (oldestBlob) blobStore.removeBlob(oldestBlob.id); } const newBlob = createPDSBlob(pdsHostname, canvas, isBlueskyFilterEnabled && isBlueskyPDS(pdsHostname)); blobStore.addBlob(newBlob); blobs = [newBlob]; }
for (const blob of blobs) { const blobToUpdate = blobStore.getBlobById(blob.id);
if (blobToUpdate) { const growth = calculateGrowth(eventType); const newMass = Math.min(blobToUpdate.mass + growth, CONFIG.maxBlobMass);
const angle = Math.random() * Math.PI * 2; const impulse = growth * 0.5;
const updates = { mass: newMass, size: Math.sqrt(newMass), lastActive: Date.now(), vx: blobToUpdate.vx + Math.cos(angle) * impulse, vy: blobToUpdate.vy + Math.sin(angle) * impulse, glow: true, pulseStartTime: Date.now(), shakeUntil: 0, shimmerUntil: 0, isMushroom: false, };
if (blobToUpdate.isBlueskyPDS && newMass > CONFIG.mushroomThreshold) { updates.isMushroom = true; }
if (eventType === EventType.Block) updates.shakeUntil = Date.now() + 300; if (eventType === EventType.List || eventType === EventType.Threadgate) updates.shimmerUntil = Date.now() + 500;
const updatedBlob = blobStore.updateBlob(blobToUpdate.id, updates);
if (updatedBlob) { setTimeout(() => { const currentBlob = blobStore.getBlobById(updatedBlob.id); if (currentBlob && currentBlob.glow) blobStore.updateBlob(updatedBlob.id, { glow: false }); }, 500);
if(CONFIG.enableBlobSplitting) splitBlobIfNeeded(blobStore, updatedBlob.id); } } } return eventType; } catch (e) { logger.error('Event Handler', 'Failed to process event', e); return eventType; } }
/** * The main entry point for the application. It orchestrates the animation loop, * physics updates, event handling, and UI rendering. */ class Application { blobStore = new BlobStore(); uiManager = new UIStateManager(); uiUpdater = new OptimizedUIUpdater(this.uiManager); canvas; ctx; isPaused = false; isBlueskyFilterEnabled = false; showLabels = false; eventCount = 0; eventTypeCounts = {}; lastFrameTime = performance.now(); hoveredBlob = null; mouseX = 0; mouseY = 0;
async init() { logger.info('Application', 'Initializing Spacedust v2.0...'); this.canvas = document.getElementById('canvas'); this.ctx = this.canvas.getContext('2d');
const setupCanvas = () => { const container = this.canvas.parentElement; this.canvas.width = container.clientWidth; this.canvas.height = container.clientHeight; }; setupCanvas(); window.addEventListener('resize', setupCanvas);
this.canvas.addEventListener('mousemove', (e) => { const rect = this.canvas.getBoundingClientRect(); const canvasX = e.clientX - rect.left; const canvasY = e.clientY - rect.top; this.updateHoveredBlob(canvasX, canvasY); this.mouseX = e.clientX; this.mouseY = e.clientY; }); this.canvas.addEventListener('mouseleave', () => { this.hoveredBlob = null; });
document.getElementById('pause-resume')?.addEventListener('click', () => { this.isPaused = !this.isPaused; document.getElementById('pause-resume').textContent = this.isPaused ? 'Resume' : 'Pause'; if (!this.isPaused) requestAnimationFrame(this.animate.bind(this)); });
const toggleUI = () => document.body.classList.toggle('ui-hidden'); document.getElementById('toggle-ui')?.addEventListener('click', toggleUI); window.addEventListener('keydown', (e) => { if(e.key === 'h') toggleUI(); });
document.getElementById('filter-bluesky')?.addEventListener('change', (e) => { this.isBlueskyFilterEnabled = e.target.checked; this.blobStore.getAllBlobs().forEach(blob => { if (blob.isBlueskyPDS) { this.blobStore.updateBlob(blob.id, { isFiltered: this.isBlueskyFilterEnabled }); } }); });
document.getElementById('toggle-labels')?.addEventListener('change', (e) => { this.showLabels = e.target.checked; });
this.startEventSubscription(); requestAnimationFrame(this.animate.bind(this)); }
updateHoveredBlob(canvasX, canvasY) { this.hoveredBlob = null; const blobs = this.blobStore.getAllBlobs().sort((a,b) => b.mass - a.mass); for (const blob of blobs) { if (blob.isFiltered) continue; const dx = canvasX - blob.x; const dy = canvasY - blob.y; if (dx * dx + dy * dy < blob.size * blob.size) { this.hoveredBlob = blob; break; } } }
updateTooltip() { const tooltip = document.getElementById('tooltip'); if (this.hoveredBlob) { tooltip.style.display = 'block'; tooltip.style.left = `${this.mouseX + 15}px`; tooltip.style.top = `${this.mouseY + 15}px`; tooltip.innerHTML = ` <strong>${this.hoveredBlob.pdsHostname}</strong><br> Mass: ${Math.round(this.hoveredBlob.mass)} `; } else { tooltip.style.display = 'none'; } }
async startEventSubscription() { for await (const event of subscribeToSpacedust()) { this.eventCount++; this.uiManager.invalidateStats(); this.uiManager.invalidatePdsLegend(); const eventType = await handleSpacedustEvent(event, this.blobStore, this.canvas, this.isBlueskyFilterEnabled); this.eventTypeCounts[eventType] = (this.eventTypeCounts[eventType] || 0) + 1; } }
updatePhysics() { for (const blob of this.blobStore.getAllBlobs()) { if (blob.isFiltered) continue;
let newX = blob.x + blob.vx, newY = blob.y + blob.vy; let newVx = blob.vx * CONFIG.friction, newVy = blob.vy * CONFIG.friction;
if (newX - blob.size < 0 || newX + blob.size > this.canvas.width) newVx *= -CONFIG.boundaryBounceDamping; if (newY - blob.size < 0 || newY + blob.size > this.canvas.height) newVy *= -CONFIG.boundaryBounceDamping;
newX = Math.max(blob.size, Math.min(this.canvas.width - blob.size, newX)); newY = Math.max(blob.size, Math.min(this.canvas.height - blob.size, newY));
this.blobStore.updateBlob(blob.id, { x: newX, y: newY, vx: newVx, vy: newVy }); } }
animate(currentTime) { if (this.isPaused) return;
this.updatePhysics(); if (CONFIG.enableDecay) applyDecay(this.blobStore); cleanupDeadBlobs(this.blobStore);
if (CONFIG.enableCollisionDetection) { const collisions = findCollisions(this.blobStore, this.canvas.width, this.canvas.height); collisions.forEach(([b1, b2]) => handleCollision(this.blobStore, b1, b2)); } renderFrame(this.ctx, this.canvas, this.blobStore, this.showLabels);
this.updateTooltip();
const frameTime = performance.now() - this.lastFrameTime; this.lastFrameTime = performance.now(); perfMonitor.record('frame-time', frameTime);
this.uiUpdater.tryUpdateStats(currentTime, () => { const perfStats = perfMonitor.getStats('frame-time'); const stats = { blobCount: this.blobStore.size(), eventCount: this.eventCount, fps: perfStats ? 1000 / perfStats.avg : 0, frameTime: perfStats ? perfStats.avg : 0, }; updateStatsUI(stats, this.eventTypeCounts); });
this.uiUpdater.tryUpdatePdsLegend(currentTime, () => { updatePdsLegendUI(this.blobStore); });
requestAnimationFrame(this.animate.bind(this)); } }
document.addEventListener('DOMContentLoaded', () => { new Application().init(); });
</script></body></html>