feat: crossfade窗口过渡,末尾20%淡出旧边淡入新边
getWindowMix: 返回crossfade(0→1), window末尾20%线性过渡 renderTrafficCrossfade: 两窗口边集按(1-cf)/cf透明度混合 - 同边去重(取max alpha)避免闪烁 - 粒子透明度跟随窗口混合 - 节点活跃=两窗口并集 验证: cf=0→20条(单窗口), cf=0.45→40条(双窗口混合) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2679,21 +2679,26 @@ function loadTrafficData() {
|
||||
function reloadTrafficData() { trafficStop(); loadTrafficData(); }
|
||||
|
||||
// ── getWindowMix: find window index + crossfade factor ──
|
||||
// crossfade 0→1 over last 20% of window[i], edges from both windows blend
|
||||
function getWindowMix() {
|
||||
if (!_trafficData) return {i:0, t:0};
|
||||
if (!_trafficData) return {i:0, crossfade:0};
|
||||
const wins = _trafficData.windows || [];
|
||||
if (!wins.length) return {i:0, t:0};
|
||||
if (!wins.length) return {i:0, crossfade: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?
|
||||
// How far into this window are we? (0..1)
|
||||
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};
|
||||
let progress = 0;
|
||||
if (ws > 0) progress = Math.max(0, Math.min(1, (_trafficCurrentTime - wTs) / ws));
|
||||
// Crossfade: last 20% → blend from window[idx] to window[idx+1]
|
||||
let crossfade = 0;
|
||||
if (progress > 0.8) {
|
||||
crossfade = Math.min(1, (progress - 0.8) / 0.2);
|
||||
}
|
||||
return {i: idx, crossfade};
|
||||
}
|
||||
|
||||
function trafficSeek(val) {
|
||||
@@ -2777,27 +2782,33 @@ function renderTrafficCrossfade() {
|
||||
if (!wins.length) return;
|
||||
const coords = _trafficData.nodes_coords || {};
|
||||
const mix = getWindowMix();
|
||||
const cf = mix.crossfade; // 0=only w0, 1=only w1
|
||||
const w0 = wins[mix.i] || null;
|
||||
const w1 = mix.i + 1 < wins.length ? wins[mix.i + 1] : null;
|
||||
const t = mix.t;
|
||||
|
||||
// Each frame: add w0 edges with fade-out, w1 edges with fade-in
|
||||
// Only show current window edges — clean switch, no blending
|
||||
// Brief fade-in for newly appearing edges
|
||||
const curEdges = t < 0.5 ? (w0 ? w0.edges : []) : (w1 ? w1.edges : []);
|
||||
// Build edge map from both windows with crossfade weights
|
||||
const edgeMap = {};
|
||||
curEdges.forEach(e => {
|
||||
const k = e.source + '|' + e.target;
|
||||
edgeMap[k] = {source:e.source, target:e.target, weight:1.0, count:e.count||0};
|
||||
});
|
||||
function addEdges(edges, windowAlpha) {
|
||||
if (!edges || windowAlpha <= 0.02) return;
|
||||
edges.forEach(e => {
|
||||
const k = e.source + '|' + e.target;
|
||||
if (!edgeMap[k]) edgeMap[k] = {source:e.source, target:e.target, alpha:0, count:0};
|
||||
// For edges in both windows: keep max alpha (no flicker)
|
||||
if (windowAlpha > edgeMap[k].alpha) edgeMap[k].alpha = windowAlpha;
|
||||
if ((e.count||0) > edgeMap[k].count) edgeMap[k].count = e.count||0;
|
||||
});
|
||||
}
|
||||
addEdges(w0 ? w0.edges : null, 1 - cf); // fading out
|
||||
addEdges(w1 ? w1.edges : null, cf); // fading in
|
||||
|
||||
const edges = Object.values(edgeMap);
|
||||
if (!edges.length) return;
|
||||
const maxCount = Math.max(1, ...edges.map(e => e.count));
|
||||
|
||||
// Active nodes: union of both windows
|
||||
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));
|
||||
if (w0 && 1 - cf > 0.1) (w0.active_nodes||[]).forEach(n => activeNodes.add(n));
|
||||
if (w1 && cf > 0.1) (w1.active_nodes||[]).forEach(n => activeNodes.add(n));
|
||||
|
||||
document.getElementById('saTrafficInfo').textContent =
|
||||
edges.length + ' 边 · ' + activeNodes.size + ' 节点 · ' + tsToBJ(_trafficCurrentTime);
|
||||
@@ -2811,12 +2822,13 @@ function renderTrafficCrossfade() {
|
||||
const midX = (p1.x + p2.x) / 2;
|
||||
const midY = (p1.y + p2.y) / 2 - Math.abs(p2.x - p1.x) * 0.1;
|
||||
const ratio = Math.min(1, e.count / maxCount);
|
||||
const alpha = 0.6 + 0.4 * ratio;
|
||||
const baseAlpha = 0.6 + 0.4 * ratio;
|
||||
const wa = baseAlpha * e.alpha; // window blend
|
||||
|
||||
// Edge trace with src→dst color gradient
|
||||
// Edge trace
|
||||
const grad = ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y);
|
||||
grad.addColorStop(0, 'rgba(46,196,182,' + alpha.toFixed(2) + ')');
|
||||
grad.addColorStop(1, 'rgba(230,57,70,' + alpha.toFixed(2) + ')');
|
||||
grad.addColorStop(0, 'rgba(46,196,182,' + wa.toFixed(2) + ')');
|
||||
grad.addColorStop(1, 'rgba(230,57,70,' + wa.toFixed(2) + ')');
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.x, p1.y);
|
||||
ctx.quadraticCurveTo(midX, midY, p2.x, p2.y);
|
||||
@@ -2824,7 +2836,7 @@ function renderTrafficCrossfade() {
|
||||
ctx.lineWidth = 1.5 + 4 * ratio;
|
||||
ctx.stroke();
|
||||
|
||||
// Flowing particles
|
||||
// Particles with same window blend
|
||||
const speedMul = 0.5 + 1.5 * ratio;
|
||||
const nP = Math.ceil(3 + 6 * ratio);
|
||||
for (let pi = 0; pi < nP; pi++) {
|
||||
@@ -2834,8 +2846,8 @@ function renderTrafficCrossfade() {
|
||||
const cr = Math.round(46 + 184*pt), cg = Math.round(196 - 139*pt), cb = Math.round(182 - 112*pt);
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, 2 + 3*ratio, 0, Math.PI*2);
|
||||
ctx.fillStyle = 'rgba(' + cr + ',' + cg + ',' + cb + ',' + (0.6+0.4*e.weight) + ')';
|
||||
ctx.shadowColor = 'rgba(' + cr + ',' + cg + ',' + cb + ',0.6)';
|
||||
ctx.fillStyle = 'rgba(' + cr + ',' + cg + ',' + cb + ',' + (0.5+0.5*wa) + ')';
|
||||
ctx.shadowColor = 'rgba(' + cr + ',' + cg + ',' + cb + ',' + (0.4+0.4*wa) + ')';
|
||||
ctx.shadowBlur = 5;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
Reference in New Issue
Block a user