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(); }
|
function reloadTrafficData() { trafficStop(); loadTrafficData(); }
|
||||||
|
|
||||||
// ── getWindowMix: find window index + crossfade factor ──
|
// ── getWindowMix: find window index + crossfade factor ──
|
||||||
|
// crossfade 0→1 over last 20% of window[i], edges from both windows blend
|
||||||
function getWindowMix() {
|
function getWindowMix() {
|
||||||
if (!_trafficData) return {i:0, t:0};
|
if (!_trafficData) return {i:0, crossfade:0};
|
||||||
const wins = _trafficData.windows || [];
|
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 ws = _trafficData._ws;
|
||||||
const minTs = _trafficData._minTs;
|
const minTs = _trafficData._minTs;
|
||||||
// Which window does currentTime fall into?
|
// Which window does currentTime fall into?
|
||||||
let idx = Math.floor((_trafficCurrentTime - minTs) / ws);
|
let idx = Math.floor((_trafficCurrentTime - minTs) / ws);
|
||||||
idx = Math.max(0, Math.min(wins.length - 1, idx));
|
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 wTs = wins[idx].ts;
|
||||||
const nextTs = idx + 1 < wins.length ? wins[idx+1].ts : wTs + ws;
|
let progress = 0;
|
||||||
let t = 0;
|
if (ws > 0) progress = Math.max(0, Math.min(1, (_trafficCurrentTime - wTs) / ws));
|
||||||
if (nextTs > wTs) t = Math.max(0, Math.min(1, (_trafficCurrentTime - wTs) / (nextTs - wTs)));
|
// Crossfade: last 20% → blend from window[idx] to window[idx+1]
|
||||||
return {i: idx, t};
|
let crossfade = 0;
|
||||||
|
if (progress > 0.8) {
|
||||||
|
crossfade = Math.min(1, (progress - 0.8) / 0.2);
|
||||||
|
}
|
||||||
|
return {i: idx, crossfade};
|
||||||
}
|
}
|
||||||
|
|
||||||
function trafficSeek(val) {
|
function trafficSeek(val) {
|
||||||
@@ -2777,27 +2782,33 @@ function renderTrafficCrossfade() {
|
|||||||
if (!wins.length) return;
|
if (!wins.length) return;
|
||||||
const coords = _trafficData.nodes_coords || {};
|
const coords = _trafficData.nodes_coords || {};
|
||||||
const mix = getWindowMix();
|
const mix = getWindowMix();
|
||||||
|
const cf = mix.crossfade; // 0=only w0, 1=only w1
|
||||||
const w0 = wins[mix.i] || null;
|
const w0 = wins[mix.i] || null;
|
||||||
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;
|
|
||||||
|
|
||||||
// Each frame: add w0 edges with fade-out, w1 edges with fade-in
|
// Build edge map from both windows with crossfade weights
|
||||||
// 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 : []);
|
|
||||||
const edgeMap = {};
|
const edgeMap = {};
|
||||||
curEdges.forEach(e => {
|
function addEdges(edges, windowAlpha) {
|
||||||
const k = e.source + '|' + e.target;
|
if (!edges || windowAlpha <= 0.02) return;
|
||||||
edgeMap[k] = {source:e.source, target:e.target, weight:1.0, count:e.count||0};
|
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);
|
const edges = Object.values(edgeMap);
|
||||||
if (!edges.length) return;
|
if (!edges.length) return;
|
||||||
const maxCount = Math.max(1, ...edges.map(e => e.count));
|
const maxCount = Math.max(1, ...edges.map(e => e.count));
|
||||||
|
|
||||||
|
// Active nodes: union of both windows
|
||||||
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 - cf > 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 && cf > 0.1) (w1.active_nodes||[]).forEach(n => activeNodes.add(n));
|
||||||
|
|
||||||
document.getElementById('saTrafficInfo').textContent =
|
document.getElementById('saTrafficInfo').textContent =
|
||||||
edges.length + ' 边 · ' + activeNodes.size + ' 节点 · ' + tsToBJ(_trafficCurrentTime);
|
edges.length + ' 边 · ' + activeNodes.size + ' 节点 · ' + tsToBJ(_trafficCurrentTime);
|
||||||
@@ -2811,12 +2822,13 @@ function renderTrafficCrossfade() {
|
|||||||
const midX = (p1.x + p2.x) / 2;
|
const midX = (p1.x + p2.x) / 2;
|
||||||
const midY = (p1.y + p2.y) / 2 - Math.abs(p2.x - p1.x) * 0.1;
|
const midY = (p1.y + p2.y) / 2 - Math.abs(p2.x - p1.x) * 0.1;
|
||||||
const ratio = Math.min(1, e.count / maxCount);
|
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);
|
const grad = ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y);
|
||||||
grad.addColorStop(0, 'rgba(46,196,182,' + alpha.toFixed(2) + ')');
|
grad.addColorStop(0, 'rgba(46,196,182,' + wa.toFixed(2) + ')');
|
||||||
grad.addColorStop(1, 'rgba(230,57,70,' + alpha.toFixed(2) + ')');
|
grad.addColorStop(1, 'rgba(230,57,70,' + wa.toFixed(2) + ')');
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(p1.x, p1.y);
|
ctx.moveTo(p1.x, p1.y);
|
||||||
ctx.quadraticCurveTo(midX, midY, p2.x, p2.y);
|
ctx.quadraticCurveTo(midX, midY, p2.x, p2.y);
|
||||||
@@ -2824,7 +2836,7 @@ function renderTrafficCrossfade() {
|
|||||||
ctx.lineWidth = 1.5 + 4 * ratio;
|
ctx.lineWidth = 1.5 + 4 * ratio;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Flowing particles
|
// Particles with same window blend
|
||||||
const speedMul = 0.5 + 1.5 * ratio;
|
const speedMul = 0.5 + 1.5 * ratio;
|
||||||
const nP = Math.ceil(3 + 6 * ratio);
|
const nP = Math.ceil(3 + 6 * ratio);
|
||||||
for (let pi = 0; pi < nP; pi++) {
|
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);
|
const cr = Math.round(46 + 184*pt), cg = Math.round(196 - 139*pt), cb = Math.round(182 - 112*pt);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(px, py, 2 + 3*ratio, 0, Math.PI*2);
|
ctx.arc(px, py, 2 + 3*ratio, 0, Math.PI*2);
|
||||||
ctx.fillStyle = 'rgba(' + cr + ',' + cg + ',' + cb + ',' + (0.6+0.4*e.weight) + ')';
|
ctx.fillStyle = 'rgba(' + cr + ',' + cg + ',' + cb + ',' + (0.5+0.5*wa) + ')';
|
||||||
ctx.shadowColor = 'rgba(' + cr + ',' + cg + ',' + cb + ',0.6)';
|
ctx.shadowColor = 'rgba(' + cr + ',' + cg + ',' + cb + ',' + (0.4+0.4*wa) + ')';
|
||||||
ctx.shadowBlur = 5;
|
ctx.shadowBlur = 5;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.shadowBlur = 0;
|
ctx.shadowBlur = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user