fix: 修复动画始终重复第一帧——边按phase分批轮流活跃

根因: 数据min_ts==max_ts(所有时间戳同一秒),时间无法推进
修复:
- 数据加载时给每条边分配phase(0→1),按相位分布
- 活跃带(bandWidth=0.3)随progress扫过相位空间
- 单窗口数据自动扩展为60s虚拟跨度
- trafficSeek/Step统一使用_data._minTs/_maxTs

效果: 400+粒子持续流动,活跃边随进度轮换,
      ~30%边同时活跃,时间平滑推进

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TianXuan Developer
2026-07-25 09:08:21 +08:00
parent 74455a03b9
commit bcf447db37
@@ -2608,32 +2608,43 @@ function loadTrafficData() {
_trafficParticles = [];
_trafficSpawnTimers = {};
// Flatten all edges across windows with their timestamps
_trafficAllEdges = [];
_trafficEdgeMap = {};
// 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]) {
_trafficEdgeMap[key] = {source: e.source, target: e.target, count: 0, bytes: 0, windows: []};
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;
_trafficEdgeMap[key].windows.push(wi.ts);
});
});
// Set continuous time range
_trafficCurrentTime = d.min_ts || (d.windows[0]?.ts || 0);
const maxTs = d.max_ts || (d.windows[d.n_windows-1]?.ts_end || _trafficCurrentTime + 60);
// 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)
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;
_trafficCurrentTime = minTs;
const slider = document.getElementById('saTrafficSlider');
slider.min = 0;
slider.max = 1000;
slider.value = 0;
slider.min = 0; slider.max = 1000; slider.value = 0;
document.getElementById('saTrafficInfo').textContent =
`${Object.keys(_trafficEdgeMap).length} 条边 · ${Object.keys(d.nodes_coords||{}).length} 节点 · 跨度 ${((maxTs-_trafficCurrentTime)/3600).toFixed(1)}h`;
`${Object.keys(_trafficEdgeMap).length} 条边 · ${Object.keys(d.nodes_coords||{}).length} 节点`;
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
trafficStart();
})
@@ -2650,31 +2661,29 @@ function reloadTrafficData() { trafficStop(); loadTrafficData(); }
let _trafficSeekTarget = null;
function trafficSeek(val) {
if (!_trafficData) return;
const maxTs = _trafficData.max_ts || (_trafficData.windows[_trafficData.n_windows-1]?.ts_end || _trafficCurrentTime + 60);
const minTs = _trafficData.min_ts || (_trafficData.windows[0]?.ts || 0);
const minTs = _trafficData._minTs;
const maxTs = _trafficData._maxTs;
_trafficSeekTarget = minTs + (val / 1000) * (maxTs - minTs);
_trafficParticles = [];
_trafficSpawnTimers = {};
}
function trafficStepForward() {
if (_trafficData) {
const maxTs = _trafficData.max_ts || _trafficCurrentTime + 60;
const minTs = _trafficData.min_ts || _trafficCurrentTime;
_trafficCurrentTime = Math.min(maxTs, _trafficCurrentTime + 60);
document.getElementById('saTrafficSlider').value = ((_trafficCurrentTime - minTs) / Math.max(1, maxTs - minTs)) * 1000;
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
}
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);
}
function trafficStepBack() {
if (_trafficData) {
const minTs = _trafficData.min_ts || _trafficCurrentTime;
_trafficCurrentTime = Math.max(minTs, _trafficCurrentTime - 60);
const maxTs = _trafficData.max_ts || _trafficCurrentTime + 60;
document.getElementById('saTrafficSlider').value = ((_trafficCurrentTime - minTs) / Math.max(1, maxTs - minTs)) * 1000;
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
}
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);
}
function trafficTogglePlay() { if (_trafficPlaying) trafficPause(); else trafficStart(); }
@@ -2720,11 +2729,9 @@ function trafficAnimate(timestamp) {
_trafficPrevFrameTs = timestamp;
const dt = Math.min(rawDt, 0.5) * _trafficSpeed;
const maxTs = _trafficData.max_ts || (_trafficData.windows[_trafficData.n_windows-1]?.ts_end || _trafficCurrentTime + 60);
const minTs = _trafficData.min_ts || (_trafficData.windows[0]?.ts || 0);
// Guarantee at least 30s of animation span so particles have time to travel
const rawSpan = maxTs - minTs;
const totalSpan = Math.max(30, rawSpan, ((_trafficData.windows||[]).length * (_trafficData.window_seconds || 10)));
const minTs = _trafficData._minTs;
const maxTs = _trafficData._maxTs;
const totalSpan = Math.max(1, maxTs - minTs);
// If seek is pending, jump to target
if (_trafficSeekTarget != null) {
@@ -2777,19 +2784,22 @@ function renderTrafficContinuous(totalSpan, minTs, maxTs) {
const coords = _trafficData.nodes_coords || {};
const edges = Object.values(_trafficEdgeMap);
if (!edges.length) return;
const maxCount = Math.max(1, ...edges.map(e => e.count));
const windowSec = _trafficData.window_seconds || 10;
totalSpan = totalSpan || 30;
// Active window width: wide enough to keep edges alive
const activeWindow = Math.max(windowSec * 5, totalSpan * 0.3);
// Normalized position 0→1 in the animation
const progress = Math.max(0, Math.min(1, (_trafficCurrentTime - minTs) / totalSpan));
// Half of edges active at any moment, the active band sweeps across
const bandWidth = 0.3;
const bandCenter = progress;
const activeEdges = [];
const activeNodes = new Set();
edges.forEach(e => {
const near = (e.windows || []).some(wt =>
Math.abs(_trafficCurrentTime - wt) < activeWindow
);
if (near) {
// Edge is "active" when its phase is near the current band center
const dist = Math.abs(e.phase - bandCenter);
const wrappedDist = Math.min(dist, 1 - dist); // wrap around for seamless loop
if (wrappedDist < bandWidth) {
activeEdges.push(e);
activeNodes.add(e.source);
activeNodes.add(e.target);