fix: 恢复Canvas贝塞尔曲线+粒子,保留窗口分裂和交叉淡入淡出
合成两个方案的优点: - Canvas贝塞尔曲线+蓝红渐变粒子(Date.now连续流动) - 窗口分裂(单独窗口15→每组~20边) - 交叉淡入淡出(透明度0.4-1.0) - 边坐标缓存(地图moveend失效) - 节点活跃/非活跃高亮/变暗 验证: win 0→1, t 0.58→0.79, pixels 54-61K, 节点16→17 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2577,10 +2577,9 @@ let _trafficPrevFrameTs = 0;
|
|||||||
let _trafficSeekTarget = null;
|
let _trafficSeekTarget = null;
|
||||||
|
|
||||||
// Canvas overlay
|
// Canvas overlay
|
||||||
let _trafficFlowLayer = null;
|
let _trafficCanvas = null;
|
||||||
let _trafficDashOffset = 0;
|
let _trafficCtx = null;
|
||||||
let _trafficDashId = null;
|
let _trafficEdgeCache = {};
|
||||||
let _trafficActiveEdges = {};
|
|
||||||
|
|
||||||
function toggleTrafficPanel() {
|
function toggleTrafficPanel() {
|
||||||
const panel = document.getElementById('saTrafficPanel');
|
const panel = document.getElementById('saTrafficPanel');
|
||||||
@@ -2588,27 +2587,34 @@ function toggleTrafficPanel() {
|
|||||||
if (panel.classList.contains('open')) {
|
if (panel.classList.contains('open')) {
|
||||||
trafficStop();
|
trafficStop();
|
||||||
panel.classList.remove('open');
|
panel.classList.remove('open');
|
||||||
_removeFlowLayer();
|
_removeFlowCanvas();
|
||||||
} else {
|
} else {
|
||||||
panel.classList.add('open');
|
panel.classList.add('open');
|
||||||
loadTrafficData();
|
loadTrafficData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _ensureFlowLayer() {
|
function _ensureFlowCanvas() {
|
||||||
if (!SA.map) return null;
|
if (!SA.map) return null;
|
||||||
if (!_trafficFlowLayer) {
|
if (_trafficCanvas && _trafficCanvas.parentNode) return _trafficCanvas;
|
||||||
const svgRenderer = L.svg({padding: 0.5});
|
const mapEl = SA.map.getContainer();
|
||||||
_trafficFlowLayer = L.layerGroup([], {renderer: svgRenderer}).addTo(SA.map);
|
const c = document.createElement('canvas');
|
||||||
}
|
c.id = 'saTrafficCanvasOverlay';
|
||||||
return _trafficFlowLayer;
|
c.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:10000;';
|
||||||
|
mapEl.appendChild(c);
|
||||||
|
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';
|
||||||
|
SA.map.on('resize', () => { const sz = SA.map.getSize(); c.width = sz.x; c.height = sz.y; c.style.width = sz.x + 'px'; c.style.height = sz.y + 'px'; });
|
||||||
|
_trafficCtx = c.getContext('2d');
|
||||||
|
_trafficCanvas = c;
|
||||||
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _removeFlowLayer() {
|
function _removeFlowCanvas() {
|
||||||
cancelAnimationFrame(_trafficAnimId); _trafficAnimId = null;
|
cancelAnimationFrame(_trafficAnimId); _trafficAnimId = null;
|
||||||
cancelAnimationFrame(_trafficDashId); _trafficDashId = null;
|
if (_trafficCanvas) { _trafficCanvas.remove(); _trafficCanvas = null; _trafficCtx = null; }
|
||||||
if (_trafficFlowLayer) { SA.map.removeLayer(_trafficFlowLayer); _trafficFlowLayer = null; }
|
_trafficEdgeCache = {};
|
||||||
_trafficActiveEdges = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function tsToBJ(ts) { const d = new Date(ts * 1000); return d.toISOString().replace('T',' ').slice(0,19); }
|
function tsToBJ(ts) { const d = new Date(ts * 1000); return d.toISOString().replace('T',' ').slice(0,19); }
|
||||||
@@ -2724,7 +2730,7 @@ function trafficPause() {
|
|||||||
}
|
}
|
||||||
function trafficStop() {
|
function trafficStop() {
|
||||||
trafficPause(); _trafficData = null;
|
trafficPause(); _trafficData = null;
|
||||||
_removeFlowLayer();
|
_removeFlowCanvas();
|
||||||
document.getElementById('saTrafficTime').textContent = '--:--:--';
|
document.getElementById('saTrafficTime').textContent = '--:--:--';
|
||||||
document.getElementById('saTrafficInfo').textContent = '';
|
document.getElementById('saTrafficInfo').textContent = '';
|
||||||
if (SA.pointLayer) SA.pointLayer.getLayers().forEach(l => {
|
if (SA.pointLayer) SA.pointLayer.getLayers().forEach(l => {
|
||||||
@@ -2763,8 +2769,11 @@ function trafficAnimate(timestamp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderTrafficCrossfade() {
|
function renderTrafficCrossfade() {
|
||||||
const layer = _ensureFlowLayer();
|
const c = _ensureFlowCanvas();
|
||||||
if (!layer || !SA.map || !_trafficData) return;
|
if (!c || !SA.map || !_trafficData) return;
|
||||||
|
const ctx = _trafficCtx;
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.clearRect(0, 0, c.width, c.height);
|
||||||
|
|
||||||
const wins = _trafficData.windows || [];
|
const wins = _trafficData.windows || [];
|
||||||
if (!wins.length) return;
|
if (!wins.length) return;
|
||||||
@@ -2774,7 +2783,6 @@ function renderTrafficCrossfade() {
|
|||||||
const w1 = mix.i + 1 < wins.length ? wins[mix.i + 1] : null;
|
const w1 = mix.i + 1 < wins.length ? wins[mix.i + 1] : null;
|
||||||
const t = mix.t;
|
const t = mix.t;
|
||||||
|
|
||||||
// Build edge map with crossfade weights
|
|
||||||
const edgeMap = {};
|
const edgeMap = {};
|
||||||
function add(edges, w) {
|
function add(edges, w) {
|
||||||
if (!edges) return;
|
if (!edges) return;
|
||||||
@@ -2789,7 +2797,9 @@ function renderTrafficCrossfade() {
|
|||||||
add(w1?w1.edges:[], t);
|
add(w1?w1.edges:[], t);
|
||||||
|
|
||||||
const edges = Object.values(edgeMap);
|
const edges = Object.values(edgeMap);
|
||||||
|
if (!edges.length) return;
|
||||||
const maxCount = Math.max(1, ...edges.map(e => e.count));
|
const maxCount = Math.max(1, ...edges.map(e => e.count));
|
||||||
|
|
||||||
const activeNodes = new Set();
|
const activeNodes = new Set();
|
||||||
if (w0 && 1-t > 0.1) (w0.active_nodes||[]).forEach(n => activeNodes.add(n));
|
if (w0 && 1-t > 0.1) (w0.active_nodes||[]).forEach(n => activeNodes.add(n));
|
||||||
if (w1 && t > 0.1) (w1.active_nodes||[]).forEach(n => activeNodes.add(n));
|
if (w1 && t > 0.1) (w1.active_nodes||[]).forEach(n => activeNodes.add(n));
|
||||||
@@ -2797,61 +2807,48 @@ function renderTrafficCrossfade() {
|
|||||||
document.getElementById('saTrafficInfo').textContent =
|
document.getElementById('saTrafficInfo').textContent =
|
||||||
edges.length + ' 边 · ' + activeNodes.size + ' 节点 · ' + tsToBJ(_trafficCurrentTime);
|
edges.length + ' 边 · ' + activeNodes.size + ' 节点 · ' + tsToBJ(_trafficCurrentTime);
|
||||||
|
|
||||||
// Build/track Leaflet polylines with dash animation
|
|
||||||
const newKeys = new Set();
|
|
||||||
edges.forEach(e => {
|
edges.forEach(e => {
|
||||||
const k = e.source + '|' + e.target;
|
let cached = _trafficEdgeCache[e.source + '|' + e.target];
|
||||||
newKeys.add(k);
|
if (!cached) {
|
||||||
|
const sc = coords[e.source], dc = coords[e.target];
|
||||||
|
if (!sc || !dc) { _trafficEdgeCache[e.source+'|'+e.target] = null; return; }
|
||||||
|
const p1 = SA.map.latLngToContainerPoint([sc[0], sc[1]]);
|
||||||
|
const p2 = SA.map.latLngToContainerPoint([dc[0], dc[1]]);
|
||||||
|
if (!p1 || !p2) return;
|
||||||
|
cached = {p1x:p1.x, p1y:p1.y, p2x:p2.x, p2y:p2.y, midX:(p1.x+p2.x)/2, midY:(p1.y+p2.y)/2 - Math.abs(p2.x-p1.x)*0.1};
|
||||||
|
_trafficEdgeCache[e.source+'|'+e.target] = cached;
|
||||||
|
}
|
||||||
|
if (!cached) return;
|
||||||
const ratio = Math.min(1, e.count / maxCount);
|
const ratio = Math.min(1, e.count / maxCount);
|
||||||
const alpha = 0.4 + 0.6 * e.weight;
|
const alpha = 0.4 + 0.6 * e.weight;
|
||||||
const w = 1.5 + 4 * ratio;
|
|
||||||
const srcC = coords[e.source], dstC = coords[e.target];
|
|
||||||
if (!srcC || !dstC) return;
|
|
||||||
|
|
||||||
let entry = _trafficActiveEdges[k];
|
// Bezier curve
|
||||||
if (entry && entry.poly) {
|
ctx.beginPath();
|
||||||
// Update existing polyline style
|
ctx.moveTo(cached.p1x, cached.p1y);
|
||||||
entry.poly.setStyle({
|
ctx.quadraticCurveTo(cached.midX, cached.midY, cached.p2x, cached.p2y);
|
||||||
opacity: alpha, weight: w,
|
ctx.strokeStyle = 'rgba(67,97,238,' + alpha.toFixed(2) + ')';
|
||||||
dashOffset: String(-_trafficDashOffset),
|
ctx.lineWidth = 1.5 + 4 * ratio;
|
||||||
});
|
ctx.stroke();
|
||||||
entry.lastWindow = mix.i;
|
|
||||||
} else {
|
// Particles flowing along curve (Date.now() driven, continuous)
|
||||||
// Create new polyline
|
const nP = Math.ceil(3 + 5 * ratio);
|
||||||
const poly = L.polyline([[srcC[0],srcC[1]], [dstC[0],dstC[1]]], {
|
for (let pi = 0; pi < nP; pi++) {
|
||||||
color: '#4361ee', weight: w, opacity: alpha,
|
const pt = ((Date.now() / 2000 + pi / nP) % 1);
|
||||||
dashArray: '15 25',
|
const px = (1-pt)*(1-pt)*cached.p1x + 2*(1-pt)*pt*cached.midX + pt*pt*cached.p2x;
|
||||||
dashOffset: String(-_trafficDashOffset),
|
const py = (1-pt)*(1-pt)*cached.p1y + 2*(1-pt)*pt*cached.midY + pt*pt*cached.p2y;
|
||||||
interactive: false, bubblingMouseEvents: false,
|
const r = Math.round(67 + 163*pt), g = Math.round(97 - 40*pt), b = Math.round(238 - 168*pt);
|
||||||
});
|
ctx.beginPath();
|
||||||
poly.addTo(layer);
|
ctx.arc(px, py, 2 + 3*ratio, 0, Math.PI*2);
|
||||||
_trafficActiveEdges[k] = {poly, lastWindow: mix.i};
|
ctx.fillStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + (0.5+0.5*e.weight) + ')';
|
||||||
|
ctx.shadowColor = 'rgba(' + r + ',' + g + ',' + b + ',0.5)';
|
||||||
|
ctx.shadowBlur = 4;
|
||||||
|
ctx.fill();
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove polylines no longer active
|
SA.map.off('moveend.flowCache').on('moveend.flowCache', () => { _trafficEdgeCache = {}; });
|
||||||
Object.keys(_trafficActiveEdges).forEach(k => {
|
|
||||||
if (!newKeys.has(k)) {
|
|
||||||
layer.removeLayer(_trafficActiveEdges[k].poly);
|
|
||||||
delete _trafficActiveEdges[k];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate dash offset for flowing effect
|
|
||||||
if (!_trafficDashId) {
|
|
||||||
function animDash() {
|
|
||||||
_trafficDashOffset = (_trafficDashOffset + 0.3) % 40;
|
|
||||||
if (_trafficFlowLayer) {
|
|
||||||
_trafficFlowLayer.getLayers().forEach(p => {
|
|
||||||
if (p.setStyle) p.setStyle({dashOffset: String(-_trafficDashOffset)});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_trafficDashId = requestAnimationFrame(animDash);
|
|
||||||
}
|
|
||||||
_trafficDashId = requestAnimationFrame(animDash);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node dimming
|
|
||||||
if (SA.pointLayer && !SA._clusterFocus) {
|
if (SA.pointLayer && !SA._clusterFocus) {
|
||||||
SA.pointLayer.getLayers().forEach(l => {
|
SA.pointLayer.getLayers().forEach(l => {
|
||||||
if (!l.setStyle) return;
|
if (!l.setStyle) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user