From 3f4077c8e08cd6e87f738c275f4847a27dbdbd0f Mon Sep 17 00:00:00 2001 From: you Date: Sun, 22 Mar 2026 07:53:50 +0000 Subject: [PATCH] Matrix Rain: canvas overlay with falling hex byte columns New 'Rain' toggle on live map. Each incoming packet spawns a falling column of hex bytes from its raw data: - Fall distance proportional to hop count (8+ hops = full screen) - 5 second fall time for full-height drops, proportional for shorter - Leading char: bright white with green glow - Trail chars: green, progressively fading - Entire column fades out in last 30% of life - Random x position across screen width - Canvas-rendered at 60fps (no DOM overhead) - Works independently of Matrix mode (can combine both) --- public/index.html | 2 +- public/live.js | 140 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index fd9fd141..4f33689f 100644 --- a/public/index.html +++ b/public/index.html @@ -90,7 +90,7 @@ - + diff --git a/public/live.js b/public/live.js index 9e4ac708..1822d595 100644 --- a/public/live.js +++ b/public/live.js @@ -14,6 +14,8 @@ let realisticPropagation = localStorage.getItem('live-realistic-propagation') === 'true'; let showOnlyFavorites = localStorage.getItem('live-favorites-only') === 'true'; let matrixMode = localStorage.getItem('live-matrix-mode') === 'true'; + let matrixRain = localStorage.getItem('live-matrix-rain') === 'true'; + let rainCanvas = null, rainCtx = null, rainDrops = [], rainRAF = null; const propagationBuffer = new Map(); // hash -> {timer, packets[]} let _onResize = null; let _navCleanup = null; @@ -634,6 +636,8 @@ Buffer packets by hash and animate all paths simultaneously Animate packet hex bytes flowing along paths like the Matrix + + Matrix rain overlay — packets fall as hex columns Show only favorited and claimed nodes @@ -811,6 +815,15 @@ if (ht) { ht.checked = false; ht.disabled = true; } } + const rainToggle = document.getElementById('liveMatrixRainToggle'); + rainToggle.checked = matrixRain; + rainToggle.addEventListener('change', (e) => { + matrixRain = e.target.checked; + localStorage.setItem('live-matrix-rain', matrixRain); + if (matrixRain) startMatrixRain(); else stopMatrixRain(); + }); + if (matrixRain) startMatrixRain(); + // Feed show/hide const feedEl = document.getElementById('liveFeed'); // Keyboard support for feed items (event delegation) @@ -1381,6 +1394,7 @@ playSound(typeName); addFeedItem(icon, typeName, payload, hops, color, pkt); + addRainDrop(pkt); // Favorites filter: skip animation if packet doesn't involve a favorited node if (showOnlyFavorites && !packetInvolvesFavorite(pkt)) return; @@ -1420,6 +1434,7 @@ if (showOnlyFavorites && !packets.some(p => packetInvolvesFavorite(p))) return; playSound(typeName); + addRainDrop(first); // Ensure ADVERT nodes appear for (const pkt of packets) { @@ -1688,6 +1703,130 @@ nodeActivity[key] = (nodeActivity[key] || 0) + 1; } + // === Matrix Rain System === + function startMatrixRain() { + const container = document.getElementById('liveMap'); + if (!container || rainCanvas) return; + rainCanvas = document.createElement('canvas'); + rainCanvas.id = 'matrixRainCanvas'; + rainCanvas.style.cssText = 'position:absolute;inset:0;z-index:9998;pointer-events:none;'; + rainCanvas.width = container.clientWidth; + rainCanvas.height = container.clientHeight; + container.appendChild(rainCanvas); + rainCtx = rainCanvas.getContext('2d'); + rainDrops = []; + + // Resize handler + rainCanvas._resizeHandler = () => { + if (rainCanvas) { + rainCanvas.width = container.clientWidth; + rainCanvas.height = container.clientHeight; + } + }; + window.addEventListener('resize', rainCanvas._resizeHandler); + + function renderRain(now) { + if (!rainCanvas || !rainCtx) return; + const W = rainCanvas.width, H = rainCanvas.height; + rainCtx.clearRect(0, 0, W, H); + + for (let i = rainDrops.length - 1; i >= 0; i--) { + const drop = rainDrops[i]; + const elapsed = now - drop.startTime; + const progress = Math.min(1, elapsed / drop.duration); + + // Head position + const headY = progress * drop.maxY; + // Trail length in pixels — proportional to hops + const trailPx = Math.min(H * 0.4, drop.hops * 30); + + const CHAR_H = 18; + const numChars = Math.min(drop.bytes.length, Math.floor(trailPx / CHAR_H)); + + for (let c = 0; c < numChars; c++) { + const charY = headY - c * CHAR_H; + if (charY < -CHAR_H || charY > H) continue; + + // Fade: head is bright, tail fades + const fadeFactor = 1 - (c / numChars); + // Also fade entire drop near end of life + const lifeFade = progress > 0.7 ? 1 - (progress - 0.7) / 0.3 : 1; + const alpha = Math.max(0, fadeFactor * lifeFade); + + if (c === 0) { + // Leading char: bright white with green glow + rainCtx.font = 'bold 16px "Courier New", monospace'; + rainCtx.fillStyle = `rgba(255, 255, 255, ${alpha})`; + rainCtx.shadowColor = '#00ff41'; + rainCtx.shadowBlur = 12; + } else { + // Trail chars: green, dimmer + rainCtx.font = '14px "Courier New", monospace'; + rainCtx.fillStyle = `rgba(0, 255, 65, ${alpha * 0.8})`; + rainCtx.shadowColor = '#00ff41'; + rainCtx.shadowBlur = 4; + } + + rainCtx.fillText(drop.bytes[c % drop.bytes.length], drop.x, charY); + } + + // Remove finished drops + if (progress >= 1) { + rainDrops.splice(i, 1); + } + } + + rainCtx.shadowBlur = 0; // reset + rainRAF = requestAnimationFrame(renderRain); + } + rainRAF = requestAnimationFrame(renderRain); + } + + function stopMatrixRain() { + if (rainRAF) { cancelAnimationFrame(rainRAF); rainRAF = null; } + if (rainCanvas) { + window.removeEventListener('resize', rainCanvas._resizeHandler); + rainCanvas.remove(); + rainCanvas = null; + rainCtx = null; + } + rainDrops = []; + } + + function addRainDrop(pkt) { + if (!rainCanvas || !matrixRain) return; + const decoded = pkt.decoded || {}; + const hops = decoded.path?.hops || []; + const hopCount = Math.max(1, hops.length); + const rawHex = pkt.raw || ''; + const bytes = []; + for (let i = 0; i < rawHex.length; i += 2) { + bytes.push(rawHex.slice(i, i + 2).toUpperCase()); + } + if (bytes.length === 0) { + for (let i = 0; i < 12; i++) bytes.push(((Math.random() * 256) | 0).toString(16).padStart(2, '0').toUpperCase()); + } + + const W = rainCanvas.width; + const H = rainCanvas.height; + // Fall distance proportional to hops: 8+ hops = full height + const maxY = H * Math.min(1, hopCount / 8); + // Duration: 5s for full height, proportional for shorter + const duration = 5000 * (maxY / H); + + // Random x position, avoid edges + const x = 20 + Math.random() * (W - 40); + + rainDrops.push({ + x, + maxY, + duration, + bytes, + hops: hopCount, + startTime: performance.now() + }); + } + function applyMatrixTheme(on) { const container = document.getElementById('liveMap'); if (!container) return; @@ -2047,6 +2186,7 @@ _navCleanup = null; } nodesLayer = pathsLayer = animLayer = heatLayer = null; + stopMatrixRain(); nodeMarkers = {}; nodeData = {}; recentPaths = []; packetCount = 0; activeAnims = 0;