mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-25 05:35:23 +00:00
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
This commit is contained in:
+2
-2
@@ -22,7 +22,7 @@
|
||||
<meta name="twitter:title" content="MeshCore Analyzer">
|
||||
<meta name="twitter:description" content="Real-time MeshCore LoRa mesh network analyzer — live packet visualization, node tracking, channel decryption, and route analysis.">
|
||||
<meta name="twitter:image" content="https://raw.githubusercontent.com/Kpa-clawbot/meshcore-analyzer/master/public/og-image.png">
|
||||
<link rel="stylesheet" href="style.css?v=1774138896">
|
||||
<link rel="stylesheet" href="style.css?v=1774163228">
|
||||
<link rel="stylesheet" href="home.css">
|
||||
<link rel="stylesheet" href="live.css?v=1774058575">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
@@ -90,7 +90,7 @@
|
||||
<script src="nodes.js?v=1774126708" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774135052" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774126708" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774156086" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774163228" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774290000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774028201" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774126708" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
|
||||
+106
-5
@@ -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 @@
|
||||
<span id="ghostDesc" class="sr-only">Show interpolated ghost markers for unknown hops</span>
|
||||
<label><input type="checkbox" id="liveRealisticToggle" aria-describedby="realisticDesc"> Realistic</label>
|
||||
<span id="realisticDesc" class="sr-only">Buffer packets by hash and animate all paths simultaneously</span>
|
||||
<label><input type="checkbox" id="liveMatrixToggle" aria-describedby="matrixDesc"> Matrix</label>
|
||||
<span id="matrixDesc" class="sr-only">Animate packet hex bytes flowing along paths like the Matrix</span>
|
||||
<label><input type="checkbox" id="liveFavoritesToggle" aria-describedby="favDesc"> ⭐ Favorites</label>
|
||||
<span id="favDesc" class="sr-only">Show only favorited and claimed nodes</span>
|
||||
</div>
|
||||
@@ -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: `<span style="color:${matrixGreen};font-family:'Courier New',monospace;font-size:12px;font-weight:bold;text-shadow:0 0 6px ${matrixGreen},0 0 12px ${matrixGreen}80;pointer-events:none">${bytes[byteIdx]}</span>`,
|
||||
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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user