From e76e63b80dc367f12cb093304de46474985bf31c Mon Sep 17 00:00:00 2001 From: you Date: Sun, 22 Mar 2026 07:07:21 +0000 Subject: [PATCH] Add Matrix visualization mode for live map New toggle in live map controls: 'Matrix' - animates packet hex bytes flowing along paths in green Matrix-style rain effect. - Hex bytes from actual packet raw_hex data flow along each hop - Green (#00ff41) monospace characters with neon glow/text-shadow - Trail of 8 characters with progressive fade - Dim green trail line underneath - Falls back to random hex if no raw data available - Persists toggle state to localStorage - Works alongside existing Realistic mode --- public/index.html | 4 +- public/live.js | 111 +++++++++++++++++++++++++++++++++++++++++++--- public/style.css | 4 ++ 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/public/index.html b/public/index.html index f8fd9753..f19ee86a 100644 --- a/public/index.html +++ b/public/index.html @@ -22,7 +22,7 @@ - + - + diff --git a/public/live.js b/public/live.js index 7139e8c4..bd6b792f 100644 --- a/public/live.js +++ b/public/live.js @@ -13,6 +13,7 @@ let showGhostHops = localStorage.getItem('live-ghost-hops') !== 'false'; let realisticPropagation = localStorage.getItem('live-realistic-propagation') === 'true'; let showOnlyFavorites = localStorage.getItem('live-favorites-only') === 'true'; + let matrixMode = localStorage.getItem('live-matrix-mode') === 'true'; const propagationBuffer = new Map(); // hash -> {timer, packets[]} let _onResize = null; let _navCleanup = null; @@ -631,6 +632,8 @@ Show interpolated ghost markers for unknown hops Buffer packets by hash and animate all paths simultaneously + + Animate packet hex bytes flowing along paths like the Matrix Show only favorited and claimed nodes @@ -786,6 +789,13 @@ applyFavoritesFilter(); }); + const matrixToggle = document.getElementById('liveMatrixToggle'); + matrixToggle.checked = matrixMode; + matrixToggle.addEventListener('change', (e) => { + matrixMode = e.target.checked; + localStorage.setItem('live-matrix-mode', matrixMode); + }); + // Feed show/hide const feedEl = document.getElementById('liveFeed'); // Keyboard support for feed items (event delegation) @@ -1365,7 +1375,7 @@ const hopPositions = resolveHopPositions(hops, payload); if (hopPositions.length === 0) return; if (hopPositions.length === 1) { pulseNode(hopPositions[0].key, hopPositions[0].pos, typeName); return; } - animatePath(hopPositions, typeName, color); + animatePath(hopPositions, typeName, color, pkt.raw); } function animateRealisticPropagation(packets) { @@ -1452,7 +1462,7 @@ // Animate all paths simultaneously for (const hopPositions of allPaths) { - animatePath(hopPositions, typeName, color); + animatePath(hopPositions, typeName, color, first.raw); } } @@ -1554,7 +1564,7 @@ return raw.filter(h => h.pos != null); } - function animatePath(hopPositions, typeName, color) { + function animatePath(hopPositions, typeName, color, rawHex) { if (!animLayer || !pathsLayer) return; activeAnims++; document.getElementById('liveAnimCount').textContent = activeAnims; @@ -1591,7 +1601,7 @@ const nextGhost = hopPositions[hopIndex + 1].ghost; const lineColor = (isGhost || nextGhost) ? '#94a3b8' : color; const lineOpacity = (isGhost || nextGhost) ? 0.3 : undefined; - drawAnimatedLine(hp.pos, nextPos, lineColor, () => { hopIndex++; nextHop(); }, lineOpacity); + drawAnimatedLine(hp.pos, nextPos, lineColor, () => { hopIndex++; nextHop(); }, lineOpacity, rawHex); } else { if (!isGhost) pulseNode(hp.key, hp.pos, typeName); hopIndex++; nextHop(); @@ -1654,8 +1664,99 @@ nodeActivity[key] = (nodeActivity[key] || 0) + 1; } - function drawAnimatedLine(from, to, color, onComplete, overrideOpacity) { + function drawMatrixLine(from, to, color, onComplete, rawHex) { if (!animLayer || !pathsLayer) { if (onComplete) onComplete(); return; } + // Extract hex bytes from raw packet data, or generate random ones + const hexStr = rawHex || ''; + const bytes = []; + for (let i = 0; i < hexStr.length; i += 2) { + bytes.push(hexStr.slice(i, i + 2).toUpperCase()); + } + if (bytes.length === 0) { + // Fallback: generate random hex if no raw data + for (let i = 0; i < 16; i++) bytes.push(((Math.random() * 256) | 0).toString(16).padStart(2, '0').toUpperCase()); + } + + const matrixGreen = '#00ff41'; + const TRAIL_LEN = Math.min(8, bytes.length); // visible chars at once + const TOTAL_STEPS = 30; + const charMarkers = []; + let step = 0; + + // Dim trail line underneath + const trail = L.polyline([from], { + color: matrixGreen, weight: 1, opacity: 0.15, lineCap: 'round' + }).addTo(pathsLayer); + + const trailCoords = [from]; + + const interval = setInterval(() => { + step++; + const t = step / TOTAL_STEPS; + const lat = from[0] + (to[0] - from[0]) * t; + const lon = from[1] + (to[1] - from[1]) * t; + trailCoords.push([lat, lon]); + trail.setLatLngs(trailCoords); + + // Remove old char markers beyond trail length + while (charMarkers.length > TRAIL_LEN) { + const old = charMarkers.shift(); + try { animLayer.removeLayer(old.marker); } catch {} + } + + // Fade existing chars + for (let i = 0; i < charMarkers.length; i++) { + const age = charMarkers.length - i; + const op = Math.max(0.1, 1 - (age / TRAIL_LEN) * 0.8); + const size = Math.max(7, 12 - age); + charMarkers[i].marker.getElement().style.opacity = op; + charMarkers[i].marker.getElement().style.fontSize = size + 'px'; + } + + // Add new leading character + const byteIdx = step % bytes.length; + const charEl = L.marker([lat, lon], { + icon: L.divIcon({ + className: 'matrix-char', + html: `${bytes[byteIdx]}`, + iconSize: [20, 14], + iconAnchor: [10, 7] + }), + interactive: false + }).addTo(animLayer); + charMarkers.push({ marker: charEl }); + + if (step >= TOTAL_STEPS) { + clearInterval(interval); + + // Fade out everything + setTimeout(() => { + let fadeStep = 0; + const fadeInterval = setInterval(() => { + fadeStep++; + const fadeOp = 1 - fadeStep / 10; + if (fadeOp <= 0) { + clearInterval(fadeInterval); + for (const cm of charMarkers) try { animLayer.removeLayer(cm.marker); } catch {} + try { pathsLayer.removeLayer(trail); } catch {} + charMarkers.length = 0; + } else { + for (const cm of charMarkers) { + try { cm.marker.getElement().style.opacity = fadeOp * 0.5; } catch {} + } + trail.setStyle({ opacity: fadeOp * 0.1 }); + } + }, 50); + }, 400); + + if (onComplete) onComplete(); + } + }, 33); + } + + function drawAnimatedLine(from, to, color, onComplete, overrideOpacity, rawHex) { + if (!animLayer || !pathsLayer) { if (onComplete) onComplete(); return; } + if (matrixMode) return drawMatrixLine(from, to, color, onComplete, rawHex); const steps = 20; const latStep = (to[0] - from[0]) / steps; const lonStep = (to[1] - from[1]) / steps; diff --git a/public/style.css b/public/style.css index c64a1019..37ef1069 100644 --- a/public/style.css +++ b/public/style.css @@ -1530,3 +1530,7 @@ tr[data-hops]:hover { background: rgba(59,130,246,0.1); } .multi-select-item:hover { background: var(--row-hover, #f5f5f5); } .chan-tag { background: var(--accent, #3b82f6); color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 0.9em; font-weight: 600; } + +/* Matrix mode hex animation */ +.matrix-char { background: none !important; border: none !important; } +.matrix-char span { display: block; text-align: center; white-space: nowrap; line-height: 1; }