fix: 流量动画Canvas贝塞尔曲线渲染修复
根因: Canvas不在map容器内,缺少position:relative;z-index不够高 修复: - _ensureTrafficCanvas动态创建canvas为#saMap子元素 - 添加position:relative到#saMap容器 - z-index提升到99999确保在Leaflet画布之上 - requestAnimationFrame替代setTimeout实现平滑粒子动画 - 面板关闭/trafficStop时清理canvas和恢复节点样式 - 粒子位置用Date.now()计算实现连续运动 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -79,7 +79,8 @@
|
||||
|
||||
/* ── Map layout ──────────────────────────────────── */
|
||||
.sa-map-row { display: flex; gap: 1rem; margin: 1rem 0; }
|
||||
.sa-map-container { flex: 1; height: 550px; border-radius: 8px; overflow: hidden; border: 1px solid #e0e0e0; }
|
||||
.sa-map-container { flex: 1; height: 550px; border-radius: 8px; overflow: hidden; border: 1px solid #e0e0e0; position: relative; }
|
||||
#saMap { position: relative; }
|
||||
.sa-legend { width: 240px; max-height: 550px; overflow-y: auto; padding: 0.5rem; background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; }
|
||||
.sa-legend-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0; cursor: pointer; font-size: 0.85rem; }
|
||||
.sa-legend-item .color-dot { width: 14px; height: 14px; border-radius: 50%; flex-shrink: 0; }
|
||||
@@ -2584,6 +2585,7 @@ function toggleTrafficPanel() {
|
||||
const c = document.getElementById('saTrafficCanvas');
|
||||
if (c) c.style.display = 'none';
|
||||
if (_trafficCanvasCtx) { _trafficCanvasCtx.clearRect(0,0,c.width,c.height); }
|
||||
_removeTrafficCanvas();
|
||||
} else {
|
||||
panel.classList.add('open');
|
||||
loadTrafficData();
|
||||
@@ -2648,8 +2650,9 @@ function trafficTogglePlay() {
|
||||
function trafficStart() {
|
||||
if (!_trafficData || _trafficPlaying) return;
|
||||
_trafficPlaying = true;
|
||||
_trafficLastFrameTime = 0;
|
||||
document.getElementById('saTrafficPlayBtn').textContent = '⏸ 暂停';
|
||||
trafficAnimate();
|
||||
_trafficAnimId = requestAnimationFrame(trafficAnimate);
|
||||
}
|
||||
|
||||
function trafficPause() {
|
||||
@@ -2662,10 +2665,18 @@ function trafficStop() {
|
||||
trafficPause();
|
||||
_trafficData = null;
|
||||
_trafficWindowIdx = 0;
|
||||
const c = document.getElementById('saTrafficCanvas');
|
||||
if (c && _trafficCanvasCtx) _trafficCanvasCtx.clearRect(0, 0, c.width, c.height);
|
||||
_removeTrafficCanvas();
|
||||
document.getElementById('saTrafficTime').textContent = '--:--:--';
|
||||
document.getElementById('saTrafficInfo').textContent = '';
|
||||
|
||||
// Restore node styles
|
||||
if (SA.pointLayer) {
|
||||
SA.pointLayer.getLayers().forEach(l => {
|
||||
if (l.setStyle) {
|
||||
try { l.setStyle({fillOpacity: 0.85, opacity: 0.95, weight: 2, color: '#fff'}); } catch(e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function trafficSetSpeed(speed, btn) {
|
||||
@@ -2674,16 +2685,53 @@ function trafficSetSpeed(speed, btn) {
|
||||
if (btn) btn.classList.add('active');
|
||||
}
|
||||
|
||||
function trafficAnimate() {
|
||||
let _trafficLastFrameTime = 0;
|
||||
let _trafficFrameDuration = 1000;
|
||||
|
||||
function trafficAnimate(timestamp) {
|
||||
if (!_trafficPlaying || !_trafficData) return;
|
||||
renderTrafficFrame(_trafficData.windows[_trafficWindowIdx]);
|
||||
const delay = _trafficData.window_seconds * 1000 / _trafficSpeed;
|
||||
_trafficAnimId = setTimeout(() => {
|
||||
_trafficWindowIdx++;
|
||||
if (_trafficWindowIdx >= _trafficData.n_windows) _trafficWindowIdx = 0;
|
||||
|
||||
if (!_trafficLastFrameTime) _trafficLastFrameTime = timestamp;
|
||||
const elapsed = timestamp - _trafficLastFrameTime;
|
||||
|
||||
const frameLen = Math.max(50, _trafficData.window_seconds * 1000 / _trafficSpeed);
|
||||
|
||||
if (elapsed >= frameLen) {
|
||||
_trafficLastFrameTime = timestamp;
|
||||
_trafficWindowIdx = (_trafficWindowIdx + 1) % _trafficData.n_windows;
|
||||
document.getElementById('saTrafficSlider').value = _trafficWindowIdx;
|
||||
trafficAnimate();
|
||||
}, Math.max(100, Math.min(delay, 2000)));
|
||||
}
|
||||
|
||||
// Always re-render for smooth particle animation
|
||||
const w = _trafficData.windows[_trafficWindowIdx];
|
||||
if (w) renderTrafficFrame(w);
|
||||
|
||||
_trafficAnimId = requestAnimationFrame(trafficAnimate);
|
||||
}
|
||||
|
||||
function _ensureTrafficCanvas() {
|
||||
const container = document.getElementById('saMap');
|
||||
if (!container) return null;
|
||||
let c = document.getElementById('saTrafficCanvas');
|
||||
if (!c) {
|
||||
c = document.createElement('canvas');
|
||||
c.id = 'saTrafficCanvas';
|
||||
c.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:99999;';
|
||||
container.appendChild(c);
|
||||
}
|
||||
const rect = container.getBoundingClientRect();
|
||||
if (c.width !== rect.width || c.height !== rect.height) {
|
||||
c.width = rect.width;
|
||||
c.height = rect.height;
|
||||
c.style.width = rect.width + 'px';
|
||||
c.style.height = rect.height + 'px';
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
function _removeTrafficCanvas() {
|
||||
const c = document.getElementById('saTrafficCanvas');
|
||||
if (c) { c.remove(); _trafficCanvasCtx = null; }
|
||||
}
|
||||
|
||||
function renderTrafficFrame(w) {
|
||||
@@ -2692,18 +2740,9 @@ function renderTrafficFrame(w) {
|
||||
document.getElementById('saTrafficInfo').textContent =
|
||||
`${w.edges.length} 条活跃边 · ${w.active_nodes.length} 活跃节点`;
|
||||
|
||||
// Setup canvas
|
||||
const container = document.getElementById('saMap');
|
||||
const c = document.getElementById('saTrafficCanvas');
|
||||
if (!c || !container) return;
|
||||
const c = _ensureTrafficCanvas();
|
||||
if (!c) return;
|
||||
c.style.display = 'block';
|
||||
const rect = container.getBoundingClientRect();
|
||||
c.width = rect.width;
|
||||
c.height = rect.height;
|
||||
c.style.width = rect.width + 'px';
|
||||
c.style.height = rect.height + 'px';
|
||||
c.style.left = rect.left + 'px';
|
||||
c.style.top = rect.top + 'px';
|
||||
_trafficCanvasCtx = c.getContext('2d');
|
||||
_trafficCanvasCtx.clearRect(0, 0, c.width, c.height);
|
||||
|
||||
@@ -2717,12 +2756,12 @@ function renderTrafficFrame(w) {
|
||||
const p1 = SA.map.latLngToContainerPoint([srcC[0], srcC[1]]);
|
||||
const p2 = SA.map.latLngToContainerPoint([dstC[0], dstC[1]]);
|
||||
if (!p1 || !p2) return;
|
||||
const alpha = 0.2 + 0.6 * (e.count / maxCount);
|
||||
const width = 1 + 3 * (e.count / maxCount);
|
||||
const alpha = 0.25 + 0.55 * (e.count / maxCount);
|
||||
const width = 1.5 + 3 * (e.count / maxCount);
|
||||
|
||||
// Draw curved edge
|
||||
// Curved edge
|
||||
const midX = (p1.x + p2.x) / 2;
|
||||
const midY = (p1.y + p2.y) / 2 - Math.abs(p2.x - p1.x) * 0.15;
|
||||
const midY = (p1.y + p2.y) / 2 - Math.abs(p2.x - p1.x) * 0.12;
|
||||
_trafficCanvasCtx.beginPath();
|
||||
_trafficCanvasCtx.moveTo(p1.x, p1.y);
|
||||
_trafficCanvasCtx.quadraticCurveTo(midX, midY, p2.x, p2.y);
|
||||
@@ -2730,28 +2769,33 @@ function renderTrafficFrame(w) {
|
||||
_trafficCanvasCtx.lineWidth = width;
|
||||
_trafficCanvasCtx.stroke();
|
||||
|
||||
// Draw flow particle
|
||||
const phase = (_trafficWindowIdx % 10) / 10;
|
||||
const t = 0.15 + phase * 0.7;
|
||||
// Flow particle
|
||||
const phase = (Date.now() / 2000 + e.count) % 1;
|
||||
const t = 0.1 + phase * 0.8;
|
||||
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;
|
||||
_trafficCanvasCtx.beginPath();
|
||||
_trafficCanvasCtx.arc(px, py, 2 + 2*(e.count/maxCount), 0, Math.PI*2);
|
||||
_trafficCanvasCtx.arc(px, py, 2.5 + 2.5*(e.count/maxCount), 0, Math.PI*2);
|
||||
_trafficCanvasCtx.fillStyle = '#e63946';
|
||||
_trafficCanvasCtx.fill();
|
||||
});
|
||||
|
||||
// Highlight active nodes
|
||||
// Dim inactive nodes
|
||||
const activeSet = new Set(w.active_nodes || []);
|
||||
if (SA.pointLayer && !SA._clusterFocus) {
|
||||
SA.pointLayer.getLayers().forEach(l => {
|
||||
const ip = l._tooltip?.getContent?.()?.match(/\b(\d+\.\d+\.\d+\.\d+)\b/)?.[1];
|
||||
if (ip && l.setStyle) {
|
||||
if (activeSet.has(ip)) {
|
||||
l.setStyle({fillOpacity: 0.95, opacity: 1, weight: 3, color: '#fff'});
|
||||
} else {
|
||||
l.setStyle({fillOpacity: 0.25, opacity: 0.35, weight: 1, color: '#ddd'});
|
||||
}
|
||||
if (l.setStyle && l._tooltip) {
|
||||
try {
|
||||
const content = l.getTooltip()?.getContent?.() || '';
|
||||
const match = content.match(/\b(\d+\.\d+\.\d+\.\d+)\b/);
|
||||
if (match) {
|
||||
if (activeSet.has(match[1])) {
|
||||
l.setStyle({fillOpacity: 0.95, opacity: 1, weight: 3, color: '#fff'});
|
||||
} else {
|
||||
l.setStyle({fillOpacity: 0.2, opacity: 0.3, weight: 1, color: '#999'});
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2869,6 +2913,5 @@ function escapeHtml(str) {
|
||||
<span class="sa-traffic-time" id="saTrafficTime">--:--:--</span>
|
||||
</div>
|
||||
<div class="sa-traffic-info" id="saTrafficInfo"></div>
|
||||
<canvas id="saTrafficCanvas" class="sa-traffic-canvas" style="display:none;"></canvas>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user