mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 19:34:20 +00:00
Make packet movement on the live map hardware-accelerated using HTML5
Instead of forcing Leaflet to recalculate and paint heavy SVG DOM nodes 60 times a second for every moving packet, we will draw the flying dots and lines directly onto a hardware-accelerated HTML5 <canvas> overlaid on the map. Once the animation finishes, it will drop a static Leaflet line to handle the fading tail effect.
This commit is contained in:
1 parent
b2f0be994d
commit
3fb7ff138c
1 file changed
+169
-85
+169
-85
@@ -10,6 +10,9 @@
|
||||
function statusGreen() { return cssVar('--status-green') || '#22c55e'; }
|
||||
|
||||
let map, ws, nodesLayer, pathsLayer, animLayer, heatLayer, geoFilterLayer, clickablePathsLayer;
|
||||
// New animation canvas
|
||||
let animCanvas, animCtx;
|
||||
let activeAnimations = [];
|
||||
let clickablePaths = [];
|
||||
const CLICKABLE_PATH_TTL_MS = 30000;
|
||||
const CLICKABLE_PATH_MAX = 50;
|
||||
@@ -1165,10 +1168,53 @@
|
||||
} catch {}
|
||||
|
||||
map = L.map('liveMap', {
|
||||
zoomControl: false, attributionControl: false,
|
||||
zoomAnimation: true, markerZoomAnimation: true
|
||||
zoomControl: false,
|
||||
attributionControl: false,
|
||||
zoomAnimation: true,
|
||||
markerZoomAnimation: true,
|
||||
preferCanvas: true
|
||||
}).setView(mapCenter, mapZoom);
|
||||
|
||||
// Create a raw canvas for high-performance animations
|
||||
animCanvas = document.createElement('canvas');
|
||||
animCanvas.style.cssText = 'position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:650;';
|
||||
|
||||
const mapContainer = document.getElementById('liveMap');
|
||||
mapContainer.appendChild(animCanvas);
|
||||
animCtx = animCanvas.getContext('2d');
|
||||
|
||||
function resizeAnimCanvas() {
|
||||
if (!animCanvas || !mapContainer) return;
|
||||
|
||||
// Get the exact dimensions of the map container right now
|
||||
const rect = mapContainer.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
animCanvas.width = rect.width * dpr;
|
||||
animCanvas.height = rect.height * dpr;
|
||||
|
||||
// Reset the scale so everything renders sharply on high-DPI/Retina screens
|
||||
animCtx.scale(dpr, dpr);
|
||||
}
|
||||
|
||||
// 1. Catch DOM Element resizes (e.g., when the user drags your feed panel)
|
||||
if (window.ResizeObserver) {
|
||||
const ro = new ResizeObserver(resizeAnimCanvas);
|
||||
ro.observe(mapContainer);
|
||||
} else {
|
||||
// Fallback for very old browsers
|
||||
window.addEventListener('resize', resizeAnimCanvas);
|
||||
}
|
||||
|
||||
// 2. Catch Leaflet's internal resize events just to be completely safe
|
||||
map.on('resize', resizeAnimCanvas);
|
||||
|
||||
// Set the initial size immediately
|
||||
resizeAnimCanvas();
|
||||
|
||||
// Start the master animation engine
|
||||
requestAnimationFrame(renderAnimations);
|
||||
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark' ||
|
||||
(document.documentElement.getAttribute('data-theme') !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
// #1420 — multi-provider dark-tile picker. Light mode unchanged.
|
||||
@@ -3380,21 +3426,121 @@
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
function renderAnimations(now) {
|
||||
if (!animCtx) return;
|
||||
|
||||
// Clear the canvas for the next frame
|
||||
animCtx.clearRect(0, 0, animCanvas.clientWidth, animCanvas.clientHeight);
|
||||
|
||||
for (let i = activeAnimations.length - 1; i >= 0; i--) {
|
||||
const anim = activeAnimations[i];
|
||||
const elapsed = now - anim.startTime;
|
||||
const t = Math.min(1, elapsed / anim.duration);
|
||||
|
||||
// Calculate current screen pixel position (keeps animations glued to the map even while panning)
|
||||
const fromPt = map.latLngToContainerPoint(anim.from);
|
||||
const toPt = map.latLngToContainerPoint(anim.to);
|
||||
const currentX = fromPt.x + (toPt.x - fromPt.x) * t;
|
||||
const currentY = fromPt.y + (toPt.y - fromPt.y) * t;
|
||||
|
||||
// Draw Contrail (glow)
|
||||
animCtx.beginPath();
|
||||
animCtx.moveTo(fromPt.x, fromPt.y);
|
||||
animCtx.lineTo(currentX, currentY);
|
||||
animCtx.strokeStyle = anim.contrailColor;
|
||||
animCtx.lineWidth = 6;
|
||||
animCtx.globalAlpha = anim.opacity * 0.2;
|
||||
animCtx.lineCap = 'round';
|
||||
animCtx.stroke();
|
||||
|
||||
// Draw Core Line
|
||||
animCtx.beginPath();
|
||||
animCtx.moveTo(fromPt.x, fromPt.y);
|
||||
animCtx.lineTo(currentX, currentY);
|
||||
if (anim.isDashed) {
|
||||
animCtx.setLineDash([4, 6]);
|
||||
animCtx.lineWidth = 1.5;
|
||||
} else {
|
||||
animCtx.lineWidth = 2;
|
||||
}
|
||||
animCtx.strokeStyle = anim.lineColor;
|
||||
animCtx.globalAlpha = anim.opacity;
|
||||
animCtx.stroke();
|
||||
animCtx.setLineDash([]); // Reset for next draw
|
||||
|
||||
// Draw Leading Dot
|
||||
animCtx.beginPath();
|
||||
animCtx.arc(currentX, currentY, 3.5, 0, Math.PI * 2);
|
||||
animCtx.fillStyle = anim.hashFill;
|
||||
animCtx.fill();
|
||||
animCtx.lineWidth = 1.5;
|
||||
animCtx.strokeStyle = anim.hashOutline;
|
||||
animCtx.stroke();
|
||||
animCtx.globalAlpha = 1.0; // Reset
|
||||
|
||||
// Handle completion
|
||||
if (t >= 1) {
|
||||
createFadingLeafletLine(anim);
|
||||
if (anim.onComplete) anim.onComplete();
|
||||
activeAnimations.splice(i, 1);
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(renderAnimations);
|
||||
}
|
||||
|
||||
function createFadingLeafletLine(anim) {
|
||||
if (!pathsLayer) return;
|
||||
|
||||
const contrail = L.polyline([anim.from, anim.to], {
|
||||
// pane: 'animationsPane', // Uncomment if you created the custom pane in the previous step
|
||||
color: anim.contrailColor, weight: 6, opacity: anim.opacity * 0.2, lineCap: 'round'
|
||||
}).addTo(pathsLayer);
|
||||
|
||||
const line = L.polyline([anim.from, anim.to], {
|
||||
// pane: 'animationsPane', // Uncomment if you created the custom pane in the previous step
|
||||
color: anim.lineColor, weight: anim.isDashed ? 1.5 : 2, opacity: anim.opacity,
|
||||
lineCap: 'round', dashArray: anim.isDashed ? '4 6' : null
|
||||
}).addTo(pathsLayer);
|
||||
|
||||
recentPaths.push({ line, glowLine: contrail, time: Date.now() });
|
||||
while (recentPaths.length > 5) {
|
||||
const old = recentPaths.shift();
|
||||
if (pathsLayer) { pathsLayer.removeLayer(old.line); pathsLayer.removeLayer(old.glowLine); }
|
||||
}
|
||||
|
||||
// Fade out logic
|
||||
let fadeOp = anim.opacity;
|
||||
let lastFade = performance.now();
|
||||
function animateFade(now) {
|
||||
if (!pathsLayer) return;
|
||||
const fadeElapsed = now - lastFade;
|
||||
if (fadeElapsed >= 52) {
|
||||
const fadeTicks = Math.min(Math.floor(fadeElapsed / 52), 4);
|
||||
lastFade = now;
|
||||
fadeOp -= 0.1 * fadeTicks;
|
||||
if (fadeOp <= 0) {
|
||||
if (pathsLayer) { pathsLayer.removeLayer(line); pathsLayer.removeLayer(contrail); }
|
||||
recentPaths = recentPaths.filter(p => p.line !== line);
|
||||
return;
|
||||
}
|
||||
line.setStyle({ opacity: fadeOp });
|
||||
contrail.setStyle({ opacity: fadeOp * 0.15 });
|
||||
}
|
||||
requestAnimationFrame(animateFade);
|
||||
}
|
||||
requestAnimationFrame(animateFade);
|
||||
}
|
||||
|
||||
function drawAnimatedLine(from, to, color, onComplete, overrideOpacity, rawHex, hash) {
|
||||
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;
|
||||
let step = 0;
|
||||
let currentCoords = [from];
|
||||
|
||||
const mainOpacity = overrideOpacity ?? 0.8;
|
||||
const isDashed = overrideOpacity != null;
|
||||
|
||||
// Hash-derived color for fill + contrail + outline (when toggle ON and not ghost/dashed line)
|
||||
var hashFill = '#fff';
|
||||
var hashOutline = color;
|
||||
var contrailColor = color;
|
||||
|
||||
if (colorByHash && hash && !isDashed && window.HashColor) {
|
||||
var hsl = HashColor.hashToHsl(hash, _liveTheme());
|
||||
hashFill = hsl;
|
||||
@@ -3402,82 +3548,20 @@
|
||||
contrailColor = hsl;
|
||||
}
|
||||
|
||||
const contrail = L.polyline([from], {
|
||||
color: contrailColor, weight: 6, opacity: mainOpacity * 0.2, lineCap: 'round'
|
||||
}).addTo(pathsLayer);
|
||||
|
||||
const line = L.polyline([from], {
|
||||
color: (colorByHash && hash && !isDashed && window.HashColor) ? hashFill : color,
|
||||
weight: isDashed ? 1.5 : 2, opacity: mainOpacity, lineCap: 'round',
|
||||
dashArray: isDashed ? '4 6' : null,
|
||||
className: 'live-packet-trace'
|
||||
}).addTo(pathsLayer);
|
||||
|
||||
const dot = L.circleMarker(from, {
|
||||
radius: 3.5, fillColor: hashFill, fillOpacity: 1, color: hashOutline, weight: 1.5
|
||||
}).addTo(animLayer);
|
||||
|
||||
let lastStep = performance.now();
|
||||
function animateLine(now) {
|
||||
if (!animLayer || !pathsLayer) {
|
||||
if (onComplete) onComplete();
|
||||
return;
|
||||
}
|
||||
const elapsed = now - lastStep;
|
||||
const stepMs = VCR.mode === 'REPLAY' ? 33 / VCR.speed : 33;
|
||||
if (elapsed >= stepMs) {
|
||||
const ticks = Math.min(Math.floor(elapsed / stepMs), 4);
|
||||
lastStep = now;
|
||||
for (let t = 0; t < ticks && step < steps; t++) {
|
||||
step++;
|
||||
const lat = from[0] + latStep * step;
|
||||
const lon = from[1] + lonStep * step;
|
||||
currentCoords.push([lat, lon]);
|
||||
}
|
||||
const lastPt = currentCoords[currentCoords.length - 1];
|
||||
line.setLatLngs(currentCoords);
|
||||
contrail.setLatLngs(currentCoords);
|
||||
dot.setLatLng(lastPt);
|
||||
|
||||
if (step >= steps) {
|
||||
if (animLayer) animLayer.removeLayer(dot);
|
||||
|
||||
recentPaths.push({ line, glowLine: contrail, time: Date.now() });
|
||||
while (recentPaths.length > 5) {
|
||||
const old = recentPaths.shift();
|
||||
if (pathsLayer) { pathsLayer.removeLayer(old.line); pathsLayer.removeLayer(old.glowLine); }
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
let fadeOp = mainOpacity;
|
||||
let lastFade = performance.now();
|
||||
function animateFade(now) {
|
||||
if (!pathsLayer) return;
|
||||
const fadeElapsed = now - lastFade;
|
||||
if (fadeElapsed >= 52) {
|
||||
const fadeTicks = Math.min(Math.floor(fadeElapsed / 52), 4);
|
||||
lastFade = now;
|
||||
fadeOp -= 0.1 * fadeTicks;
|
||||
if (fadeOp <= 0) {
|
||||
if (pathsLayer) { pathsLayer.removeLayer(line); pathsLayer.removeLayer(contrail); }
|
||||
recentPaths = recentPaths.filter(p => p.line !== line);
|
||||
return;
|
||||
}
|
||||
line.setStyle({ opacity: fadeOp });
|
||||
contrail.setStyle({ opacity: fadeOp * 0.15 });
|
||||
}
|
||||
requestAnimationFrame(animateFade);
|
||||
}
|
||||
requestAnimationFrame(animateFade);
|
||||
}, 800);
|
||||
|
||||
if (onComplete) onComplete();
|
||||
return;
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(animateLine);
|
||||
}
|
||||
requestAnimationFrame(animateLine);
|
||||
// Push to the hardware-accelerated canvas engine
|
||||
activeAnimations.push({
|
||||
from: from,
|
||||
to: to,
|
||||
startTime: performance.now(),
|
||||
duration: 660 / VCR.speed, // Matches your original duration (33ms * 20 steps)
|
||||
opacity: mainOpacity,
|
||||
isDashed: isDashed,
|
||||
lineColor: (colorByHash && hash && !isDashed && window.HashColor) ? hashFill : color,
|
||||
contrailColor: contrailColor,
|
||||
hashFill: hashFill,
|
||||
hashOutline: hashOutline,
|
||||
onComplete: onComplete
|
||||
});
|
||||
}
|
||||
|
||||
function showHeatMap() {
|
||||
|
||||
Reference in new issue
Block a user