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();
+ }
}