fix: Canvas粒子流动——插入到map容器外部确保在最上层渲染
关键修复: canvas直接插入到map容器parent,绝对定位覆盖地图, 不再受Leaflet内部图层堆叠影响。 粒子沿贝塞尔曲线流动,颜色从蓝渐变到红(3-8个/边) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2753,32 +2753,61 @@ function trafficAnimate(timestamp) {
|
||||
_trafficAnimId = requestAnimationFrame(trafficAnimate);
|
||||
}
|
||||
|
||||
// ── Flow polylines system ──
|
||||
let _trafficFlowLayer = null; // L.layerGroup for flowing polylines
|
||||
// ── Flow canvas overlay ──
|
||||
let _trafficFlowLayer = null;
|
||||
let _trafficFlowAnimId = null;
|
||||
let _trafficFlowOffset = 0;
|
||||
let _trafficCanvasCtx = null;
|
||||
|
||||
function _ensureFlowLayer() {
|
||||
if (!SA.map) return null;
|
||||
if (_trafficFlowLayer) return _trafficFlowLayer;
|
||||
_trafficFlowLayer = L.layerGroup().addTo(SA.map);
|
||||
return _trafficFlowLayer;
|
||||
// Insert canvas AFTER map container, positioned to cover the map
|
||||
const mapEl = SA.map.getContainer();
|
||||
const c = document.createElement('canvas');
|
||||
c.id = 'saTrafficCanvasOverlay';
|
||||
c.style.cssText = 'position:absolute; top:0; left:0; pointer-events:none; z-index:99999;';
|
||||
// Insert after map container in DOM
|
||||
mapEl.parentNode.insertBefore(c, mapEl.nextSibling);
|
||||
// Size to match map
|
||||
const size = SA.map.getSize();
|
||||
c.width = size.x; c.height = size.y;
|
||||
c.style.width = size.x + 'px'; c.style.height = size.y + 'px';
|
||||
// Position overlay over map
|
||||
const mapRect = mapEl.getBoundingClientRect();
|
||||
const parentRect = mapEl.parentNode.getBoundingClientRect();
|
||||
c.style.left = (mapRect.left - parentRect.left) + 'px';
|
||||
c.style.top = (mapRect.top - parentRect.top) + 'px';
|
||||
// Update on map changes
|
||||
SA.map.on('resize move zoomend', () => {
|
||||
const s = SA.map.getSize();
|
||||
c.width = s.x; c.height = s.y;
|
||||
c.style.width = s.x + 'px'; c.style.height = s.y + 'px';
|
||||
const mr = mapEl.getBoundingClientRect();
|
||||
const pr = mapEl.parentNode.getBoundingClientRect();
|
||||
c.style.left = (mr.left - pr.left) + 'px';
|
||||
c.style.top = (mr.top - pr.top) + 'px';
|
||||
});
|
||||
_trafficCanvasCtx = c.getContext('2d');
|
||||
_trafficFlowLayer = c;
|
||||
return c;
|
||||
}
|
||||
|
||||
function _removeFlowLayer() {
|
||||
if (_trafficFlowAnimId) { cancelAnimationFrame(_trafficFlowAnimId); _trafficFlowAnimId = null; }
|
||||
if (_trafficFlowLayer && SA.map) { SA.map.removeLayer(_trafficFlowLayer); _trafficFlowLayer = null; }
|
||||
if (_trafficFlowLayer) { _trafficFlowLayer.remove(); _trafficFlowLayer = null; _trafficCanvasCtx = null; }
|
||||
}
|
||||
|
||||
function renderTrafficContinuous(totalSpan, minTs, maxTs) {
|
||||
const layer = _ensureFlowLayer();
|
||||
if (!layer || !SA.map) return;
|
||||
const c = _ensureFlowLayer();
|
||||
if (!c || !SA.map) return;
|
||||
|
||||
const coords = _trafficData.nodes_coords || {};
|
||||
const edges = Object.values(_trafficEdgeMap);
|
||||
if (!edges.length) return;
|
||||
const maxCount = Math.max(1, ...edges.map(e => e.count));
|
||||
totalSpan = totalSpan || 30;
|
||||
const ctx = _trafficCanvasCtx;
|
||||
if (!ctx) return;
|
||||
|
||||
const progress = Math.max(0, Math.min(1, (_trafficCurrentTime - minTs) / totalSpan));
|
||||
const bandWidth = 0.3;
|
||||
@@ -2798,34 +2827,48 @@ function renderTrafficContinuous(totalSpan, minTs, maxTs) {
|
||||
document.getElementById('saTrafficInfo').textContent =
|
||||
`${activeEdges.length} 活跃边 · ${activeNodes.size} 活跃节点 · ${tsToBJ(_trafficCurrentTime)}`;
|
||||
|
||||
// Use Leaflet polylines with animated dash-array for visible flow effect
|
||||
layer.clearLayers();
|
||||
ctx.clearRect(0, 0, c.width, c.height);
|
||||
|
||||
// Draw bezier curves THEN particles
|
||||
activeEdges.slice(0, 300).forEach(e => {
|
||||
const srcC = coords[e.source]; const dstC = coords[e.target];
|
||||
if (!srcC || !dstC) return;
|
||||
const p1 = SA.map.latLngToContainerPoint([srcC[0], srcC[1]]);
|
||||
const p2 = SA.map.latLngToContainerPoint([dstC[0], dstC[1]]);
|
||||
if (!p1 || !p2) return;
|
||||
const midX = (p1.x + p2.x) / 2;
|
||||
const midY = (p1.y + p2.y) / 2 - Math.abs(p2.x - p1.x) * 0.1;
|
||||
const ratio = e.count / maxCount;
|
||||
const w = 2 + 5 * ratio;
|
||||
L.polyline([[srcC[0], srcC[1]], [dstC[0], dstC[1]]], {
|
||||
color: `hsl(${220 + 15 * ratio}, 70%, ${45 + 20 * ratio}%)`,
|
||||
weight: w, opacity: 0.85,
|
||||
dashArray: `${10 + 15 * ratio} ${10 + 15 * (1 - ratio)}`,
|
||||
dashOffset: String(-_trafficFlowOffset),
|
||||
interactive: false, bubblingMouseEvents: false,
|
||||
}).addTo(layer);
|
||||
});
|
||||
|
||||
// Animate dash offset
|
||||
if (!_trafficFlowAnimId) {
|
||||
_trafficFlowAnimId = requestAnimationFrame(function loop() {
|
||||
_trafficFlowOffset = (_trafficFlowOffset + 0.5) % 40;
|
||||
if (_trafficFlowLayer) {
|
||||
_trafficFlowLayer.getLayers().forEach(p => {
|
||||
if (p.setStyle) p.setStyle({dashOffset: String(-_trafficFlowOffset)});
|
||||
});
|
||||
}
|
||||
_trafficFlowAnimId = requestAnimationFrame(loop);
|
||||
});
|
||||
}
|
||||
// Background trace
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.x, p1.y);
|
||||
ctx.quadraticCurveTo(midX, midY, p2.x, p2.y);
|
||||
ctx.strokeStyle = `rgba(67,97,238,${0.4 + 0.5 * ratio})`;
|
||||
ctx.lineWidth = 1.5 + 4 * ratio;
|
||||
ctx.stroke();
|
||||
|
||||
// FLOWING PARTICLES
|
||||
const particleCount = Math.ceil(3 + 5 * ratio);
|
||||
for (let pi = 0; pi < particleCount; pi++) {
|
||||
// Each particle at a different phase along the curve
|
||||
const t = ((Date.now() / 2000 + pi / particleCount) % 1);
|
||||
const px = (1-t)*(1-t)*p1.x + 2*(1-t)*t*midX + t*t*p2.x;
|
||||
const py = (1-t)*(1-t)*p1.y + 2*(1-t)*t*midY + t*t*p2.y;
|
||||
|
||||
// Color changes along the flow - start blue, end red
|
||||
const r = Math.round(67 + (230 - 67) * t);
|
||||
const gVal = Math.round(97 + (57 - 97) * t);
|
||||
const bVal = Math.round(238 + (70 - 238) * t);
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, 2.5 + 3 * ratio, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgb(${r},${gVal},${bVal})`;
|
||||
ctx.shadowColor = `rgba(${r},${gVal},${bVal},0.6)`;
|
||||
ctx.shadowBlur = 4;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
});
|
||||
|
||||
// Dim inactive nodes
|
||||
if (SA.pointLayer && !SA._clusterFocus) {
|
||||
|
||||
Reference in New Issue
Block a user