diff --git a/steps/src/dom.js b/steps/src/dom.js index f7065b4..72a4438 100644 --- a/steps/src/dom.js +++ b/steps/src/dom.js @@ -252,6 +252,7 @@ function getCurrentColorMode() { // Cache for DOM elements to avoid querySelector/getElementById every frame let overlayCache = null; +let videoOverlayCache = null; // Cache for conditional rendering let lastDrawnFrame = -1; @@ -381,8 +382,10 @@ export function updatePersistentOverlays(currentMediaTime) { ctx.clearRect(0, 0, w, h); - // --- 1. Batching Phase --- - const batches = {}; + // --- Optimization: Immediate Mode Drawing (No Allocations) --- + // Instead of batching into objects, we draw directly. + // We iterate through columns. To minimize state changes, we could pre-sort, + // but simply drawing column-by-column is fast enough and avoids GC. for (let offset = -centerCol; offset < centerCol; offset++) { const targetFrameIndex = appState.currentFrame + offset; @@ -396,30 +399,22 @@ export function updatePersistentOverlays(currentMediaTime) { const numBlocks = Math.min(10, Math.max(1, Math.round(ift / msPerBlock))); const color = getTimingColor(ift); - if (!batches[color]) batches[color] = []; - + ctx.fillStyle = color; + ctx.beginPath(); + for (let d = 0; d < numBlocks; d++) { const y = h - (d * (blockSize + vGap)) - 3; - batches[color].push({x, y}); + ctx.rect(x, y, blockSize, blockSize); } + ctx.fill(); } else { // Placeholder blocks - const color = "rgba(255, 255, 255, 0.1)"; - if (!batches[color]) batches[color] = []; + ctx.fillStyle = "rgba(255, 255, 255, 0.1)"; + ctx.beginPath(); 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.rect(x, y, blockSize, blockSize); + ctx.fill(); } - ctx.fill(); } // Draw Center Indicator (Triangle at column 60) @@ -447,11 +442,25 @@ export function updatePersistentOverlays(currentMediaTime) { timeDisplay += ` / ${videoPlayer.duration.toFixed(2)}s`; } - videoInfoOverlay.innerHTML = ` - Frame: ${videoFrame} - | ${timeDisplay} - | Abs Time: ${formatUTCTime(absVideoTime)} - `; + // --- OPTIMIZATION: Video Overlay Caching --- + if (!videoOverlayCache) { + videoInfoOverlay.innerHTML = ` + Frame: + | + | Abs Time: + `; + videoOverlayCache = { + frame: document.getElementById("ov-vid-frame"), + time: document.getElementById("ov-vid-time"), + abs: document.getElementById("ov-vid-abs") + }; + } + + if (videoOverlayCache) { + videoOverlayCache.frame.textContent = videoFrame; + videoOverlayCache.time.textContent = timeDisplay; + videoOverlayCache.abs.textContent = formatUTCTime(absVideoTime); + } } const customTtcInputs = [ diff --git a/steps/src/drawUtils.js b/steps/src/drawUtils.js index bede29d..65ac282 100644 --- a/steps/src/drawUtils.js +++ b/steps/src/drawUtils.js @@ -437,18 +437,18 @@ export function drawTrackMarkers(p, plotScales) { const localStationaryColor = stationaryColor(p); const localMovingColor = movingColor(p); + // Optimization: Batch drawing commands + // We collect all text labels to draw them in a single pass at the end. + // This avoids switching between stroke/fill and push/pop for every track. + const textLabels = []; + + p.push(); + p.strokeWeight(2); + for (const track of appState.vizData.tracks) { - // --- START: Add the Same Safeguard Here --- - // This robust check ensures the track and its historyLog are valid before use. - if (toggleConfirmedOnly.checked && track.isConfirmed === false) { - continue; - } - if (!track || !track.historyLog || !Array.isArray(track.historyLog)) { - // We don't need to log a warning here again, as drawTrajectories already did. - // We can just safely skip this malformed track. - continue; - } - // --- END: Add the Same Safeguard Here --- + if (toggleConfirmedOnly.checked && track.isConfirmed === false) continue; + // Robust check for malformed tracks (same as drawTrajectories) + if (!track || !track.historyLog || !Array.isArray(track.historyLog)) continue; const log = track.historyLog.find( (log) => log.frameIdx === appState.currentFrame @@ -460,63 +460,79 @@ export function drawTrackMarkers(p, plotScales) { const size = 5; const x = pos[0] * plotScales.plotScaleX; const y = pos[1] * plotScales.plotScaleY; - let velocityColor = p.color(255, 0, 255, 200); - - p.push(); - p.strokeWeight(2); + + // --- Draw Marker Shape --- if (useStationary && log.isStationary === true) { p.stroke(localStationaryColor); p.noFill(); p.rectMode(p.CENTER); p.square(x, y, size * 1.5); - velocityColor = localStationaryColor; } else { let markerColor = p.color(0, 0, 255); if (useStationary && log.isStationary === false) { markerColor = localMovingColor; - velocityColor = localMovingColor; } p.stroke(markerColor); p.line(x - size, y, x + size, y); p.line(x, y - size, x, y + size); } - p.pop(); + // --- Draw Velocity Vector & Collect Text --- if ( showDetails && log.predictedVelocity && log.predictedVelocity[0] !== null ) { const [vx, vy] = log.predictedVelocity; + + // Draw velocity line immediately (shares stroke context) if (log.isStationary === false) { - p.push(); - p.stroke(velocityColor); - p.strokeWeight(2); - p.line( + // Determine color again (optimization: could be refactored to avoid recalc) + let velocityColor = p.color(255, 0, 255, 200); + if (useStationary) velocityColor = localMovingColor; + + p.stroke(velocityColor); + p.line( x, y, (pos[0] + vx) * plotScales.plotScaleX, (pos[1] + vy) * plotScales.plotScaleY ); - p.pop(); } - const speed = (p.sqrt(vx * vx + vy * vy) * 3.6).toFixed(1); + + // Defer Text Drawing + const speed = (Math.sqrt(vx * vx + vy * vy) * 3.6).toFixed(1); const ttc = log.ttc !== null && isFinite(log.ttc) && log.ttc < 100 ? `TTC: ${log.ttc.toFixed(1)}s` : ""; const text = `ID: ${track.id} | ${speed} km/h\n${ttc}`; - p.push(); - p.fill(textColor); - p.noStroke(); - p.scale(1, -1); - p.textSize(12); - p.text(text, x + 10, -y); - p.pop(); + + textLabels.push({ x, y, text }); } } } } + p.pop(); // End shape drawing context + + // --- Batch Draw Text --- + if (textLabels.length > 0) { + p.push(); + p.fill(textColor); + p.noStroke(); + p.textSize(12); + // Set alignment once + // Note: we handle the flip manually + + for (const label of textLabels) { + p.push(); + p.translate(label.x + 10, label.y); + p.scale(1, -1); // Flip text back up + p.text(label.text, 0, 0); + p.pop(); + } + p.pop(); + } }