diff --git a/simple_analysis/templates/simple_analysis/simple_analysis.html b/simple_analysis/templates/simple_analysis/simple_analysis.html index 2d0d3f1..aedbdaa 100644 --- a/simple_analysis/templates/simple_analysis/simple_analysis.html +++ b/simple_analysis/templates/simple_analysis/simple_analysis.html @@ -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;