= 48 && diffMs <= 52) {
+ return "#00FF00"; // Bright Green (Perfect)
+ } else if (diffMs >= 40 && diffMs <= 60) {
+ return "#98FB98"; // Pale Green (Good)
+ } else if ((diffMs >= 30 && diffMs < 40) || (diffMs > 60 && diffMs <= 70)) {
+ return "#FFEB3B"; // Yellow (Noticeable)
+ } else if ((diffMs >= 20 && diffMs < 30) || (diffMs > 70 && diffMs <= 100)) {
+ return "#FFA500"; // Orange (Warning)
+ } else if (diffMs > 300) {
+ return "#c339ffff"; // Dark Violet (Extreme > 300ms)
+ } else if (diffMs > 150) {
+ return "#8B0000"; // Dark Red (Severe > 150ms)
+ } else {
+ return "#FF4500"; // Red (Critical 100-150ms or < 20ms)
+ }
+}
+
// --- DOM Element References --- //
export const themeToggleBtn = document.getElementById("theme-toggle");
@@ -231,6 +250,10 @@ function getCurrentColorMode() {
return "Default"; // The default mode when no specific color toggle is checked
}
+// Cache for conditional rendering
+let lastDrawnFrame = -1;
+let lastDrawnScale = -1;
+
export function updatePersistentOverlays(currentMediaTime) {
// If the advanced debug overlay is visible, hide the persistent overlays and exit.
if (toggleDebug2Overlay.checked) {
@@ -263,12 +286,127 @@ export function updatePersistentOverlays(currentMediaTime) {
const fps = appState.fps;
const fpsColor = fps >= 58 && fps <= 62 ? "#98FB98" : "#FF6347"; // Pale Green or Tomato
- radarInfoOverlay.innerHTML = `
+ const interFrameTime = currentRadarFrame.interFrameTime;
+ const iftColor = getTimingColor(interFrameTime);
+
+ // --- OPTIMIZATION: One-time DOM Setup ---
+ if (!document.getElementById("ift-dot-matrix")) {
+ radarInfoOverlay.innerHTML = `
+
+
+ `;
+ }
+
+ // --- 1. Smart Smooth Zoom Logic ---
+ // Use pre-calculated maxWindowIFT (computed in fileParsers.js) for O(1) performance.
+ const maxWindowIFT = currentRadarFrame.maxWindowIFT || 0;
+
+ // Calculate Target Scale
+ // Base: 10ms. If max > 100ms, scale up.
+ // Cap: 40ms (3-4x zoom).
+ let targetMsPerBlock = 10;
+ if (maxWindowIFT > 100) {
+ // Example: 900ms spike -> 900/10 = 90. Clamped to 40.
+ targetMsPerBlock = Math.min(40, Math.max(10, maxWindowIFT / 10));
+ }
+
+ // Smooth Interpolation (Lerp)
+ // Move current scale 10% of the way to the target per frame.
+ const smoothingFactor = 0.1;
+ appState.currentGraphScale += (targetMsPerBlock - appState.currentGraphScale) * smoothingFactor;
+
+ // Use the smoothed value for drawing
+ const msPerBlock = appState.currentGraphScale;
+
+ // --- Update Text Content Efficiently ---
+ const textContainer = document.getElementById("radar-text-content");
+ if (textContainer) {
+ textContainer.innerHTML = `
Frame: ${appState.currentFrame + 1}
| Motion State: ${motionState}
| FPS:
${fps.toFixed(1)}
| Color Mode:
${colorMode}
- | Drift:
${driftMs.toFixed(0)}ms`;
+ | Drift:
${driftMs.toFixed(0)}ms
+ | Δt:
${interFrameTime.toFixed(0)}ms`;
+ }
+
+ // --- Draw Optimized Square Block Matrix Graph ---
+ // CONDITIONAL RENDER: Only redraw if frame changed or scale changed significantly
+ if (appState.currentFrame !== lastDrawnFrame || Math.abs(msPerBlock - lastDrawnScale) > 0.01) {
+
+ const dotCanvas = document.getElementById("ift-dot-matrix");
+ if (dotCanvas) {
+ const ctx = dotCanvas.getContext("2d");
+ const w = dotCanvas.width;
+ const h = dotCanvas.height;
+
+ const blockSize = 3;
+ const vGap = 1;
+ const hGap = 2;
+ const stride = blockSize + hGap;
+ // msPerBlock is already set above
+
+ // Calculate columns: 600px / 5px = 120 columns.
+ const totalCols = 140;
+ const centerCol = 70;
+
+ ctx.clearRect(0, 0, w, h);
+
+ // --- 1. Batching Phase ---
+ const batches = {};
+
+ for (let offset = -centerCol; offset < centerCol; offset++) {
+ const targetFrameIndex = appState.currentFrame + offset;
+ const colIndex = offset + centerCol;
+ const x = colIndex * stride + 2;
+
+ if (targetFrameIndex >= 0 && targetFrameIndex < appState.vizData.radarFrames.length) {
+ const ift = appState.vizData.radarFrames[targetFrameIndex].interFrameTime || 0;
+
+ // Use the SMOOTHED dynamic scale here
+ const numBlocks = Math.min(10, Math.max(1, Math.round(ift / msPerBlock)));
+ const color = getTimingColor(ift);
+
+ if (!batches[color]) batches[color] = [];
+
+ for (let d = 0; d < numBlocks; d++) {
+ const y = h - (d * (blockSize + vGap)) - 3;
+ batches[color].push({x, y});
+ }
+ } else {
+ // Placeholder blocks
+ const color = "rgba(255, 255, 255, 0.1)";
+ if (!batches[color]) batches[color] = [];
+ const y = h - 3;
+ batches[color].push({x, y});
+ }
+ }
+
+ // --- 2. Drawing Phase ---
+ // Draw all blocks of the same color in one pass
+ for (const [color, points] of Object.entries(batches)) {
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ for (const p of points) {
+ ctx.rect(p.x, p.y, blockSize, blockSize);
+ }
+ ctx.fill();
+ }
+
+ // Draw Center Indicator (Triangle at column 60)
+ const centerX = centerCol * stride + 2;
+ ctx.fillStyle = "#FFFFFF";
+ ctx.beginPath();
+ ctx.moveTo(centerX - 4, h);
+ ctx.lineTo(centerX + 4, h);
+ ctx.lineTo(centerX, h - 6);
+ ctx.fill();
+ }
+
+ // Update cache
+ lastDrawnFrame = appState.currentFrame;
+ lastDrawnScale = msPerBlock;
+ }
}
// --- Update Video Persistent Overlay ---
diff --git a/steps/src/fileParsers.js b/steps/src/fileParsers.js
index 5429264..199b53b 100644
--- a/steps/src/fileParsers.js
+++ b/steps/src/fileParsers.js
@@ -70,6 +70,36 @@ export async function parseVisualizationJson(
});
});
+ // Calculate interFrameTime for each frame
+ const radarFrames = vizData.radarFrames;
+ for (let i = 0; i < radarFrames.length; i++) {
+ if (i < radarFrames.length - 1) {
+ radarFrames[i].interFrameTime = radarFrames[i + 1].timestampMs - radarFrames[i].timestampMs;
+ } else {
+ // Last frame edge case: set its interFrameTime equal to the previous frame's interFrameTime
+ if (radarFrames.length > 1) {
+ radarFrames[i].interFrameTime = radarFrames[i - 1].interFrameTime;
+ } else {
+ radarFrames[i].interFrameTime = 0; // Only one frame, so interFrameTime is 0
+ }
+ }
+ }
+
+ // --- Pre-calculate Max Window IFT for Smart Zoom (Sliding Window) ---
+ // This eliminates the need for real-time lookahead scanning in the render loop.
+ const lookahead = 80;
+ for (let i = 0; i < radarFrames.length; i++) {
+ let localMax = 0;
+ const start = Math.max(0, i - lookahead);
+ const end = Math.min(radarFrames.length - 1, i + lookahead);
+
+ for (let j = start; j <= end; j++) {
+ const val = radarFrames[j].interFrameTime || 0;
+ if (val > localMax) localMax = val;
+ }
+ radarFrames[i].maxWindowIFT = localMax;
+ }
+
let snrValues = [];
let totalPoints = 0;
await processArrayInChunks(vizData.radarFrames, 5000, (chunk) => {