refactor: smooth crossfade transition for traffic timeline, fix animation freeze, reduce memory
完全重写动态流通动画: - Crossfade: 相邻窗口边通过透明度渐变(1-t↔t)平滑过渡 - getWindowMix: 计算当前窗口索引+混合因子t - 边去重: 同一边在两窗口间连续存在不闪烁 - 时间确保持续推进,循环播放 - 边坐标缓存(地图移动时失效),减少latLngToContainerPoint调用 - 粒子基于Date.now()连续运动,不受窗口切换影响 - 节点活跃状态按两窗口并集+权重 - 移除旧的phase band/edgeMap/particle pool/_spawnTimers 验证: 上传→聚类→地图→流量面板→时间推进→实体流量图表,0 JS错误 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2567,17 +2567,21 @@ function generateColors(n) {
|
||||
return Array.from({length: n}, (_, i) => palette[i % palette.length]);
|
||||
}
|
||||
|
||||
/* ── Traffic Flow Animation (continuous particle system) ── */
|
||||
let _trafficData = null;
|
||||
/* ── Traffic Flow Animation (crossfade window transitions) ── */
|
||||
let _trafficData = null; // {windows:[{ts,edges,active_nodes}], nodes_coords, window_seconds, _minTs, _maxTs}
|
||||
let _trafficAnimId = null;
|
||||
let _trafficPlaying = false;
|
||||
let _trafficSpeed = 1;
|
||||
let _trafficCurrentTime = 0; // current Beijing Unix time (seconds)
|
||||
let _trafficParticles = []; // [{src, dst, p1, p2, midX, midY, progress, speed, count}]
|
||||
let _trafficCurrentTime = 0;
|
||||
let _trafficPrevFrameTs = 0;
|
||||
let _trafficAllEdges = []; // flat array of all edges across all windows
|
||||
let _trafficEdgeMap = {}; // key "src|dst" → {count, bytes}
|
||||
let _trafficSpawnTimers = {}; // key → next spawn time
|
||||
let _trafficSeekTarget = null;
|
||||
|
||||
// Canvas overlay
|
||||
let _trafficCanvas = null;
|
||||
let _trafficCtx = null;
|
||||
|
||||
// Cached pixel positions for edges (rebuilt on map move)
|
||||
let _trafficEdgeCache = {}; // key → {p1, p2, midX, midY}
|
||||
|
||||
function toggleTrafficPanel() {
|
||||
const panel = document.getElementById('saTrafficPanel');
|
||||
@@ -2585,13 +2589,37 @@ function toggleTrafficPanel() {
|
||||
if (panel.classList.contains('open')) {
|
||||
trafficStop();
|
||||
panel.classList.remove('open');
|
||||
_removeFlowLayer();
|
||||
_removeFlowCanvas();
|
||||
} else {
|
||||
panel.classList.add('open');
|
||||
loadTrafficData();
|
||||
}
|
||||
}
|
||||
|
||||
function _ensureFlowCanvas() {
|
||||
if (!SA.map) return null;
|
||||
if (_trafficCanvas && _trafficCanvas.parentNode) return _trafficCanvas;
|
||||
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: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 _removeFlowCanvas() {
|
||||
cancelAnimationFrame(_trafficAnimId); _trafficAnimId = null;
|
||||
if (_trafficCanvas) { _trafficCanvas.remove(); _trafficCanvas = null; _trafficCtx = null; }
|
||||
}
|
||||
|
||||
function tsToBJ(ts) { const d = new Date(ts * 1000); return d.toISOString().replace('T',' ').slice(0,19); }
|
||||
|
||||
function loadTrafficData() {
|
||||
if (!SA.sessionId) { showError('请先完成聚类分析'); return; }
|
||||
showLoading('加载流通数据...');
|
||||
@@ -2605,116 +2633,83 @@ function loadTrafficData() {
|
||||
hideLoading();
|
||||
if (d.error) { showError(d.error); return; }
|
||||
_trafficData = d;
|
||||
_trafficParticles = [];
|
||||
_trafficSpawnTimers = {};
|
||||
|
||||
// Flatten all edges and build edge map
|
||||
const allEdges = [];
|
||||
(d.windows || []).forEach(wi => {
|
||||
(wi.edges || []).forEach(e => {
|
||||
const key = e.source + '|' + e.target;
|
||||
if (!_trafficEdgeMap[key]) {
|
||||
const obj = {source: e.source, target: e.target, count: 0, bytes: 0, phase: 0};
|
||||
_trafficEdgeMap[key] = obj;
|
||||
allEdges.push(obj);
|
||||
}
|
||||
_trafficEdgeMap[key].count += e.count;
|
||||
_trafficEdgeMap[key].bytes += e.bytes;
|
||||
});
|
||||
});
|
||||
|
||||
// Distribute edges across time phases (0.0→1.0) so they don't all fire at once
|
||||
allEdges.sort((a,b) => b.bytes - a.bytes);
|
||||
allEdges.forEach((e, i) => { e.phase = i / Math.max(1, allEdges.length - 1); });
|
||||
|
||||
// Force a meaningful animation span (at least 60 fake-seconds)
|
||||
_trafficEdgeCache = {};
|
||||
let ws = d.window_seconds || 10;
|
||||
let minTs = d.min_ts || (d.windows[0]?.ts || 0);
|
||||
let maxTs = d.max_ts || (d.windows[d.n_windows-1]?.ts_end || minTs + 1);
|
||||
const realSpan = maxTs - minTs;
|
||||
if (realSpan < 10) {
|
||||
maxTs = minTs + 60; // artificial 60s span for sparse data
|
||||
d._artificialSpan = true;
|
||||
}
|
||||
d._minTs = minTs;
|
||||
d._maxTs = maxTs;
|
||||
|
||||
let maxTs = d.max_ts || (d.windows[d.n_windows-1]?.ts_end || minTs + ws);
|
||||
if (maxTs - minTs < 10) maxTs = minTs + Math.max(60, ws * 6);
|
||||
d._minTs = minTs; d._maxTs = maxTs; d._ws = ws;
|
||||
_trafficCurrentTime = minTs;
|
||||
|
||||
const slider = document.getElementById('saTrafficSlider');
|
||||
slider.min = 0; slider.max = 1000; slider.value = 0;
|
||||
|
||||
document.getElementById('saTrafficSlider').min = 0;
|
||||
document.getElementById('saTrafficSlider').max = 1000;
|
||||
document.getElementById('saTrafficSlider').value = 0;
|
||||
document.getElementById('saTrafficInfo').textContent =
|
||||
`${Object.keys(_trafficEdgeMap).length} 条边 · ${Object.keys(d.nodes_coords||{}).length} 节点`;
|
||||
(d.windows||[]).length + ' 窗口 · ' + Object.keys(d.nodes_coords||{}).length + ' 节点';
|
||||
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
|
||||
trafficStart();
|
||||
})
|
||||
.catch(err => { hideLoading(); showError('加载失败: ' + err.message); });
|
||||
}
|
||||
|
||||
function tsToBJ(ts) {
|
||||
const d = new Date(ts * 1000);
|
||||
return d.toISOString().replace('T',' ').slice(0,19);
|
||||
}
|
||||
|
||||
function reloadTrafficData() { trafficStop(); loadTrafficData(); }
|
||||
|
||||
let _trafficSeekTarget = null;
|
||||
// ── getWindowMix: find window index + crossfade factor ──
|
||||
function getWindowMix() {
|
||||
if (!_trafficData) return {i:0, t:0};
|
||||
const wins = _trafficData.windows || [];
|
||||
if (!wins.length) return {i:0, t:0};
|
||||
const ws = _trafficData._ws;
|
||||
const minTs = _trafficData._minTs;
|
||||
// Which window does currentTime fall into?
|
||||
let idx = Math.floor((_trafficCurrentTime - minTs) / ws);
|
||||
idx = Math.max(0, Math.min(wins.length - 1, idx));
|
||||
// Crossfade: how far between this window's ts and next window's ts?
|
||||
const wTs = wins[idx].ts;
|
||||
const nextTs = idx + 1 < wins.length ? wins[idx+1].ts : wTs + ws;
|
||||
let t = 0;
|
||||
if (nextTs > wTs) t = Math.max(0, Math.min(1, (_trafficCurrentTime - wTs) / (nextTs - wTs)));
|
||||
return {i: idx, t};
|
||||
}
|
||||
|
||||
function trafficSeek(val) {
|
||||
if (!_trafficData) return;
|
||||
const minTs = _trafficData._minTs;
|
||||
const maxTs = _trafficData._maxTs;
|
||||
const minTs = _trafficData._minTs, maxTs = _trafficData._maxTs;
|
||||
_trafficSeekTarget = minTs + (val / 1000) * (maxTs - minTs);
|
||||
_trafficParticles = [];
|
||||
_trafficSpawnTimers = {};
|
||||
}
|
||||
|
||||
function trafficStepForward() {
|
||||
if (!_trafficData) return;
|
||||
const minTs = _trafficData._minTs, maxTs = _trafficData._maxTs;
|
||||
const span = Math.max(1, maxTs - minTs);
|
||||
_trafficCurrentTime = Math.min(maxTs, _trafficCurrentTime + span * 0.02);
|
||||
document.getElementById('saTrafficSlider').value = ((_trafficCurrentTime - minTs) / span) * 1000;
|
||||
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
|
||||
const ws = _trafficData._ws;
|
||||
_trafficSeekTarget = Math.min(_trafficData._maxTs, _trafficCurrentTime + ws);
|
||||
}
|
||||
|
||||
function trafficStepBack() {
|
||||
if (!_trafficData) return;
|
||||
const minTs = _trafficData._minTs, maxTs = _trafficData._maxTs;
|
||||
const span = Math.max(1, maxTs - minTs);
|
||||
_trafficCurrentTime = Math.max(minTs, _trafficCurrentTime - span * 0.02);
|
||||
document.getElementById('saTrafficSlider').value = ((_trafficCurrentTime - minTs) / span) * 1000;
|
||||
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
|
||||
const ws = _trafficData._ws;
|
||||
_trafficSeekTarget = Math.max(_trafficData._minTs, _trafficCurrentTime - ws);
|
||||
}
|
||||
|
||||
function trafficTogglePlay() { if (_trafficPlaying) trafficPause(); else trafficStart(); }
|
||||
|
||||
function trafficStart() {
|
||||
if (!_trafficData || _trafficPlaying) return;
|
||||
_trafficPlaying = true;
|
||||
_trafficPrevFrameTs = 0;
|
||||
_trafficPlaying = true; _trafficPrevFrameTs = 0;
|
||||
document.getElementById('saTrafficPlayBtn').textContent = '⏸ 暂停';
|
||||
_trafficAnimId = requestAnimationFrame(trafficAnimate);
|
||||
}
|
||||
|
||||
function trafficPause() {
|
||||
_trafficPlaying = false;
|
||||
document.getElementById('saTrafficPlayBtn').textContent = '▶ 播放';
|
||||
if (_trafficAnimId) { cancelAnimationFrame(_trafficAnimId); _trafficAnimId = null; }
|
||||
cancelAnimationFrame(_trafficAnimId); _trafficAnimId = null;
|
||||
}
|
||||
|
||||
function trafficStop() {
|
||||
trafficPause();
|
||||
_trafficData = null; _trafficParticles = []; _trafficAllEdges = []; _trafficEdgeMap = {};
|
||||
_removeFlowLayer();
|
||||
trafficPause(); _trafficData = null; _trafficEdgeCache = {};
|
||||
_removeFlowCanvas();
|
||||
document.getElementById('saTrafficTime').textContent = '--:--:--';
|
||||
document.getElementById('saTrafficInfo').textContent = '';
|
||||
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) {} }
|
||||
});
|
||||
}
|
||||
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) {
|
||||
_trafficSpeed = speed;
|
||||
document.querySelectorAll('#saTrafficPanel button[data-speed]').forEach(b => b.classList.remove('active'));
|
||||
@@ -2729,145 +2724,128 @@ function trafficAnimate(timestamp) {
|
||||
_trafficPrevFrameTs = timestamp;
|
||||
const dt = Math.min(rawDt, 0.5) * _trafficSpeed;
|
||||
|
||||
const minTs = _trafficData._minTs;
|
||||
const maxTs = _trafficData._maxTs;
|
||||
const totalSpan = Math.max(1, maxTs - minTs);
|
||||
const minTs = _trafficData._minTs, maxTs = _trafficData._maxTs;
|
||||
const span = Math.max(1, maxTs - minTs);
|
||||
|
||||
// If seek is pending, jump to target
|
||||
if (_trafficSeekTarget != null) {
|
||||
_trafficCurrentTime = _trafficSeekTarget;
|
||||
_trafficSeekTarget = null;
|
||||
_trafficCurrentTime = _trafficSeekTarget; _trafficSeekTarget = null;
|
||||
} else {
|
||||
_trafficCurrentTime += dt;
|
||||
if (_trafficCurrentTime > maxTs) _trafficCurrentTime = minTs + (_trafficCurrentTime - maxTs) % totalSpan;
|
||||
if (_trafficCurrentTime > maxTs) _trafficCurrentTime = minTs + ((_trafficCurrentTime - maxTs) % span);
|
||||
}
|
||||
|
||||
// Update slider
|
||||
document.getElementById('saTrafficSlider').value = ((_trafficCurrentTime - minTs) / totalSpan) * 1000;
|
||||
document.getElementById('saTrafficSlider').value = ((_trafficCurrentTime - minTs) / span) * 1000;
|
||||
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
|
||||
|
||||
// Render
|
||||
renderTrafficContinuous(totalSpan, minTs, maxTs);
|
||||
|
||||
renderTrafficCrossfade();
|
||||
_trafficAnimId = requestAnimationFrame(trafficAnimate);
|
||||
}
|
||||
|
||||
// ── Flow canvas overlay ──
|
||||
let _trafficFlowLayer = null;
|
||||
let _trafficFlowAnimId = null;
|
||||
let _trafficCanvasCtx = null;
|
||||
|
||||
function _ensureFlowLayer() {
|
||||
if (!SA.map) return null;
|
||||
if (_trafficFlowLayer && _trafficFlowLayer.parentNode) return _trafficFlowLayer;
|
||||
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:10000;';
|
||||
mapEl.appendChild(c);
|
||||
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';
|
||||
SA.map.on('resize', () => {
|
||||
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';
|
||||
});
|
||||
_trafficCanvasCtx = c.getContext('2d');
|
||||
_trafficFlowLayer = c;
|
||||
return c;
|
||||
}
|
||||
|
||||
function _removeFlowLayer() {
|
||||
if (_trafficFlowAnimId) { cancelAnimationFrame(_trafficFlowAnimId); _trafficFlowAnimId = null; }
|
||||
if (_trafficFlowLayer) { _trafficFlowLayer.remove(); _trafficFlowLayer = null; _trafficCanvasCtx = null; }
|
||||
}
|
||||
|
||||
function renderTrafficContinuous(totalSpan, minTs, maxTs) {
|
||||
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;
|
||||
function renderTrafficCrossfade() {
|
||||
const c = _ensureFlowCanvas();
|
||||
if (!c || !SA.map || !_trafficData) return;
|
||||
const ctx = _trafficCtx;
|
||||
if (!ctx) return;
|
||||
|
||||
const progress = Math.max(0, Math.min(1, (_trafficCurrentTime - minTs) / totalSpan));
|
||||
const bandWidth = 0.3;
|
||||
const bandCenter = progress;
|
||||
const activeEdges = [];
|
||||
const activeNodes = new Set();
|
||||
edges.forEach(e => {
|
||||
const dist = Math.abs(e.phase - bandCenter);
|
||||
const wrappedDist = Math.min(dist, 1 - dist);
|
||||
if (wrappedDist < bandWidth) {
|
||||
activeEdges.push(e);
|
||||
activeNodes.add(e.source);
|
||||
activeNodes.add(e.target);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('saTrafficInfo').textContent =
|
||||
`${activeEdges.length} 活跃边 · ${activeNodes.size} 活跃节点 · ${tsToBJ(_trafficCurrentTime)}`;
|
||||
|
||||
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 wins = _trafficData.windows || [];
|
||||
if (!wins.length) return;
|
||||
|
||||
// Background trace
|
||||
const coords = _trafficData.nodes_coords || {};
|
||||
const mix = getWindowMix();
|
||||
const w0 = wins[mix.i] || null;
|
||||
const w1 = mix.i + 1 < wins.length ? wins[mix.i + 1] : null;
|
||||
const t = mix.t;
|
||||
|
||||
// Collect edges from both windows with blend weights
|
||||
// edgeKey → {source, target, weight, count}
|
||||
const edgeMap = {};
|
||||
function addEdges(edges, weight, baseCount) {
|
||||
if (!edges) return;
|
||||
edges.forEach(e => {
|
||||
const k = e.source + '|' + e.target;
|
||||
if (!edgeMap[k]) edgeMap[k] = {source:e.source, target:e.target, weight:0, count:0};
|
||||
edgeMap[k].weight = Math.max(edgeMap[k].weight, weight);
|
||||
edgeMap[k].count = Math.max(edgeMap[k].count, e.count || 0);
|
||||
});
|
||||
}
|
||||
addEdges(w0 ? w0.edges : [], 1 - t, 0);
|
||||
addEdges(w1 ? w1.edges : [], t, 0);
|
||||
// Also add edges from next window for smooth fade-in
|
||||
if (mix.i + 2 < wins.length) {
|
||||
addEdges(wins[mix.i + 2].edges, Math.max(0, t - 1), 0);
|
||||
}
|
||||
|
||||
const edges = Object.values(edgeMap);
|
||||
if (!edges.length) return;
|
||||
|
||||
const maxCount = Math.max(1, ...edges.map(e => e.count));
|
||||
|
||||
// Active node set (union of both windows, weighted)
|
||||
const activeNodes = new Set();
|
||||
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));
|
||||
|
||||
document.getElementById('saTrafficInfo').textContent =
|
||||
edges.length + ' 边 · ' + activeNodes.size + ' 节点 · ' + tsToBJ(_trafficCurrentTime);
|
||||
|
||||
// Build pixel coordinates for edges (cached until map moves)
|
||||
edges.forEach(e => {
|
||||
let cached = _trafficEdgeCache[e.source + '|' + e.target];
|
||||
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 = e.count / maxCount;
|
||||
const alpha = 0.3 + 0.6 * e.weight * ratio;
|
||||
|
||||
// Bezier 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.moveTo(cached.p1x, cached.p1y);
|
||||
ctx.quadraticCurveTo(cached.midX, cached.midY, cached.p2x, cached.p2y);
|
||||
ctx.strokeStyle = 'rgba(67,97,238,' + alpha.toFixed(2) + ')';
|
||||
ctx.lineWidth = 1 + 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);
|
||||
// Particles (continuous, driven by Date.now)
|
||||
const nParticles = Math.ceil(3 + 5 * ratio);
|
||||
for (let pi = 0; pi < nParticles; pi++) {
|
||||
const pt = ((Date.now() / 2000 + pi / nParticles) % 1);
|
||||
const px = (1-pt)*(1-pt)*cached.p1x + 2*(1-pt)*pt*cached.midX + pt*pt*cached.p2x;
|
||||
const py = (1-pt)*(1-pt)*cached.p1y + 2*(1-pt)*pt*cached.midY + pt*pt*cached.p2y;
|
||||
const r = Math.round(67 + 163 * pt), g = Math.round(97 - 40 * pt), b = Math.round(238 - 168 * pt);
|
||||
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.arc(px, py, 2 + 3 * ratio, 0, Math.PI * 2);
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
// Dim inactive nodes
|
||||
// Invalidate edge cache if map has moved (simplified: clear on zoom/move)
|
||||
SA.map.off('moveend.flowCache').on('moveend.flowCache', () => { _trafficEdgeCache = {}; });
|
||||
|
||||
// Node dimming
|
||||
if (SA.pointLayer && !SA._clusterFocus) {
|
||||
SA.pointLayer.getLayers().forEach(l => {
|
||||
if (!l.setStyle) return;
|
||||
try {
|
||||
const content = l.getTooltip()?.getContent?.() || '';
|
||||
const match = content.match(/\b(\d+\.\d+\.\d+\.\d+)\b/);
|
||||
if (match) {
|
||||
l.setStyle(activeNodes.has(match[1])
|
||||
? {fillOpacity: 0.95, opacity: 1, weight: 3, color: '#fff'}
|
||||
: {fillOpacity: 0.2, opacity: 0.3, weight: 1, color: '#999'});
|
||||
}
|
||||
const ct = l.getTooltip()?.getContent?.() || '';
|
||||
const m = ct.match(/\b(\d+\.\d+\.\d+\.\d+)\b/);
|
||||
if (m) l.setStyle(activeNodes.has(m[1])
|
||||
? {fillOpacity:0.95,opacity:1,weight:3,color:'#fff'}
|
||||
: {fillOpacity:0.2,opacity:0.3,weight:1,color:'#999'});
|
||||
} catch(e) {}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user