diff --git a/steps/src/drawUtils.js b/steps/src/drawUtils.js index 3ff0d6c..5805f47 100644 --- a/steps/src/drawUtils.js +++ b/steps/src/drawUtils.js @@ -522,7 +522,7 @@ export function drawTrackMarkers(p, plotScales) { * @param {p5} p - The p5 instance. * @param {object} plotScales - The calculated scales for plotting. */ -export function handleCloseUpDisplay(p, plotScales) { +export function handleCloseUpDisplay(p, plotScales, mouseX, mouseY) { // --- Step 1: Gather Hovered Items --- const frameData = appState.vizData.radarFrames[appState.currentFrame]; if (!frameData) return []; // Return empty array if no data @@ -544,7 +544,7 @@ export function handleCloseUpDisplay(p, plotScales) { if (pt.x === null || pt.y === null) continue; const screenX = pt.x * plotScales.plotScaleX + p.width / 2; const screenY = p.height * 0.95 - pt.y * plotScales.plotScaleY; - const d = p.dist(p.mouseX, p.mouseY, screenX, screenY); + const d = p.dist(mouseX, mouseY, screenX, screenY); // Use smoothed values if (d < radius) { // Add the index 'i' to the object we push hoveredItems.push({ @@ -568,7 +568,7 @@ export function handleCloseUpDisplay(p, plotScales) { if (cluster.x === null || cluster.y === null) continue; const screenX = cluster.x * plotScales.plotScaleX + p.width / 2; const screenY = p.height * 0.95 - cluster.y * plotScales.plotScaleY; - const d = p.dist(p.mouseX, p.mouseY, screenX, screenY); + const d = p.dist(mouseX, mouseY, screenX, screenY); // Use smoothed values if (d < radius) { const color = cluster.id > 0 @@ -599,7 +599,7 @@ export function handleCloseUpDisplay(p, plotScales) { const pos = currentLog.correctedPosition; const screenX = pos[0] * plotScales.plotScaleX + p.width / 2; const screenY = p.height * 0.95 - pos[1] * plotScales.plotScaleY; - const d = p.dist(p.mouseX, p.mouseY, screenX, screenY); + const d = p.dist(mouseX, mouseY, screenX, screenY); // Use smoothed values if (d < radius) { hoveredItems.push({ type: "track", @@ -622,7 +622,7 @@ export function handleCloseUpDisplay(p, plotScales) { const pos = currentLog.predictedPosition; const screenX = pos[0] * plotScales.plotScaleX + p.width / 2; const screenY = p.height * 0.95 - pos[1] * plotScales.plotScaleY; - const d = p.dist(p.mouseX, p.mouseY, screenX, screenY); + const d = p.dist(mouseX, mouseY, screenX, screenY); // Use smoothed values if (d < radius) { hoveredItems.push({ type: "prediction", @@ -727,11 +727,11 @@ export function handleCloseUpDisplay(p, plotScales) { const xOffset = 20; let boxX, lineAnchorX; - if (p.mouseX + xOffset + boxWidth > p.width) { - boxX = p.mouseX - boxWidth - xOffset; + if (mouseX + xOffset + boxWidth > p.width) { // Use smoothed values + boxX = mouseX - boxWidth - xOffset; lineAnchorX = boxX + boxWidth; } else { - boxX = p.mouseX + xOffset; + boxX = mouseX + xOffset; lineAnchorX = boxX; } let boxY = p.mouseY - boxHeight / 2; diff --git a/steps/src/p5/radarSketch.js b/steps/src/p5/radarSketch.js index dcf22d4..c41dbc0 100644 --- a/steps/src/p5/radarSketch.js +++ b/steps/src/p5/radarSketch.js @@ -39,6 +39,12 @@ export const radarSketch = function (p) { // p5.Graphics buffers for static elements to optimize drawing let staticBackgroundBuffer, snrLegendBuffer, trackLegendBuffer; + // --- START: Mouse Smoothing Variables --- + let smoothedMouseX = 0; + let smoothedMouseY = 0; + let isFirstFrame = true; // Flag to initialize smoothed position + // --- END: Mouse Smoothing Variables --- + // Helper function to allow other sketches to access the static background p.getStaticBackground = function () { return staticBackgroundBuffer; @@ -221,10 +227,28 @@ export const radarSketch = function (p) { // BUG FIX 1: Call the close-up handler if the mode is active // --- Zoom and Tooltip Logic --- - const COOLING_PERIOD_MS = 2000; + const COOLING_PERIOD_MS = 2000; // Set to 3 seconds for the countdown const zoomPanel = document.getElementById("zoom-panel"); if (appState.isCloseUpMode) { - const hoveredItems = handleCloseUpDisplay(p, plotScales); + // --- START: Mouse Smoothing Logic --- + // On the first frame of zoom, snap the smoothed position to the real mouse position. + if (isFirstFrame) { + smoothedMouseX = p.mouseX; + smoothedMouseY = p.mouseY; + isFirstFrame = false; + } + + // The smoothing factor. A smaller value (e.g., 0.1) means more smoothing. + // This can be adjusted to feel more or less responsive. + const smoothingFactor = 0.5; + + // Linearly interpolate the smoothed position towards the actual mouse position. + smoothedMouseX = p.lerp(smoothedMouseX, p.mouseX, smoothingFactor); + smoothedMouseY = p.lerp(smoothedMouseY, p.mouseY, smoothingFactor); + + // Use the smoothed coordinates for all subsequent zoom-related calculations. + const hoveredItems = handleCloseUpDisplay(p, plotScales, smoothedMouseX, smoothedMouseY); + // --- END: Mouse Smoothing Logic --- // --- START: Draw Zoom Area Rectangle & Debug Circle --- const zoomWindow = document.getElementById("zoom-canvas-container"); @@ -242,7 +266,7 @@ export const radarSketch = function (p) { p.strokeWeight(1); // Reduced thickness. p.drawingContext.setLineDash([5, 3]); // Dashed line. p.rectMode(p.CENTER); - p.rect(p.mouseX, p.mouseY, sourceWidth, sourceHeight); + p.rect(smoothedMouseX, smoothedMouseY, sourceWidth, sourceHeight); // Use smoothed values p.drawingContext.setLineDash([]); // Reset line dash p.pop(); } @@ -254,14 +278,18 @@ export const radarSketch = function (p) { p.noFill(); p.stroke(148, 0, 211, 150); // Deep purple, semi-transparent. p.strokeWeight(1); - p.drawingContext.setLineDash([5,3]) - p.ellipse(p.mouseX, p.mouseY, hoverRadius * 2, hoverRadius * 2); + p.ellipse(smoothedMouseX, smoothedMouseY, hoverRadius * 2, hoverRadius * 2); // Use smoothed values p.pop(); // --- END: Draw Zoom Area Rectangle & Debug Circle --- if (hoveredItems.length > 0) { - clearTimeout(appState.zoomHoverTimeout); // Cancel the timer - appState.zoomHoverTimeout = null; + // If we are hovering, cancel any existing countdown. + clearTimeout(appState.zoomHideDelayTimeout); + appState.zoomHideDelayTimeout = null; + clearInterval(appState.zoomCountdownInterval); + appState.zoomCountdownInterval = null; + appState.zoomCountdown = null; + if (zoomPanel.style.display !== "block") { zoomPanel.style.display = "block"; } @@ -270,41 +298,74 @@ export const radarSketch = function (p) { appState.zoomSketchInstance.updateAndDraw ) { appState.zoomSketchInstance.updateAndDraw( - p.mouseX, - p.mouseY, + smoothedMouseX, // Use smoothed values + smoothedMouseY, // Use smoothed values hoveredItems, plotScales ); } } else if (zoomPanel.style.display === "block") { - // --- THIS BLOCK IS THE FIX --- - // If NOT hovering, but the panel is still visible: - - // 1. Continue to update the zoom sketch's position to follow the mouse. - // We pass an empty array for hoveredItems, so no tooltip is drawn. + // --- START: FIX for Grace Period Freeze --- + // If NOT hovering, but the panel is still visible, we must continue + // to update the zoom sketch so it follows the mouse. + // We pass an empty array for hoveredItems, so no tooltip is drawn. if ( appState.zoomSketchInstance && appState.zoomSketchInstance.updateAndDraw ) { appState.zoomSketchInstance.updateAndDraw( - p.mouseX, - p.mouseY, - [], // Pass empty array + smoothedMouseX, // Use smoothed values + smoothedMouseY, // Use smoothed values + [], // Pass empty array to hide tooltips plotScales ); } - + // --- END: FIX for Grace Period Freeze --- // 2. If a "hide" timer isn't already running, start one. - if (!appState.zoomHoverTimeout) { - appState.zoomHoverTimeout = setTimeout(() => { - console.log("Cooling period ended. Hiding zoom panel."); - zoomPanel.style.display = "none"; - appState.zoomHoverTimeout = null; - }, COOLING_PERIOD_MS); + if (!appState.zoomHideDelayTimeout && !appState.zoomCountdownInterval) { + // Start a 2-second delay before the countdown begins. + appState.zoomHideDelayTimeout = setTimeout(() => { + appState.zoomHideDelayTimeout = null; // Clear the delay timer ID + // Now, start the actual 3-second countdown interval. + appState.zoomCountdown = Math.floor(COOLING_PERIOD_MS / 1000); + appState.zoomCountdownInterval = setInterval(() => { + appState.zoomCountdown--; + if (appState.zoomCountdown <= 0) { + // When countdown finishes, hide panel and clear interval. + clearInterval(appState.zoomCountdownInterval); + appState.zoomCountdownInterval = null; + appState.zoomCountdown = null; + zoomPanel.style.display = "none"; + } else { + // Force a redraw of the zoom sketch to show the new countdown value. + // This call is still needed inside the interval to update the countdown text. + if (appState.zoomSketchInstance && appState.zoomSketchInstance.updateAndDraw) { + // Pass empty hoveredItems to show the countdown text. + appState.zoomSketchInstance.updateAndDraw( + smoothedMouseX, + smoothedMouseY, + [], + plotScales); + } + } + }, 1000); + }, 1000); // 1000ms = 1 second delay } } } else { + // --- START: Cleanup Logic --- + // When zoom mode is turned off, ensure all timers are cleared. + if (appState.zoomHideDelayTimeout) { + clearTimeout(appState.zoomHideDelayTimeout); + appState.zoomHideDelayTimeout = null; + } + if (appState.zoomCountdownInterval) { + clearInterval(appState.zoomCountdownInterval); + appState.zoomCountdownInterval = null; + } + // --- END: Cleanup Logic --- zoomPanel.style.display = "none"; + isFirstFrame = true; // Reset for the next time zoom mode is enabled } // --- Legend Drawing --- // Draw the SNR legend if enabled diff --git a/steps/src/p5/zoomSketch.js b/steps/src/p5/zoomSketch.js index 43fc1bd..d9ff4f4 100644 --- a/steps/src/p5/zoomSketch.js +++ b/steps/src/p5/zoomSketch.js @@ -301,6 +301,22 @@ export const zoomSketch = function (p) { // --- Call the new, self-contained tooltip function --- drawZoomTooltip(p, hoveredItems, mainMouseX); + // --- START: Draw Purple Debug Circle --- + // This circle represents the hover radius, drawn in the zoomed coordinate space. + // The formula must match the one in drawUtils.js. + const hoverRadius = p.constrain(80 / appState.zoomFactor, 5, 25); + p.push(); + p.noFill(); + p.stroke(148, 0, 211, 150); // Deep purple, semi-transparent. + // The stroke weight is divided by the zoom factor to keep it thin. + p.strokeWeight(1 / appState.zoomFactor); + p.drawingContext.setLineDash([5 / appState.zoomFactor, 3 / appState.zoomFactor]); + // The circle is drawn at the mouse position from the main canvas. + p.ellipse(mainMouseX, mainMouseY, hoverRadius * 2, hoverRadius * 2); + p.drawingContext.setLineDash([]); + p.pop(); + // --- END: Draw Purple Debug Circle --- + p.pop(); // End zoom transformations // --- START: DRAW TITLE OVERLAY --- // This code runs *after* the zoom transformations have been popped, @@ -320,6 +336,30 @@ export const zoomSketch = function (p) { p.pop(); // --- END: DRAW TITLE OVERLAY --- + // --- START: Draw Countdown Overlay --- + if (appState.zoomCountdown !== null && appState.zoomCountdown > 0) { + p.push(); + // Semi-transparent black background for readability + p.fill(0, 0, 0, 150); + p.noStroke(); + p.rectMode(p.CENTER); + p.rect(p.width / 2, p.height / 2, p.width, p.height); + + // Draw the text + p.fill(255); + p.textAlign(p.CENTER, p.CENTER); + p.textSize(18); + p.textStyle(p.NORMAL); + p.text( + "Hover over points again to resume the display", + p.width / 2, + p.height / 2 - 15 + ); + p.text(`Closing in ${appState.zoomCountdown}...`, p.width / 2, p.height / 2 + 15); + p.pop(); + } + // --- END: Draw Countdown Overlay --- + // --- Draw Crosshairs --- p.stroke(255, 0, 0, 150); p.strokeWeight(1.5); diff --git a/steps/src/state.js b/steps/src/state.js index 13c9801..18f9e96 100644 --- a/steps/src/state.js +++ b/steps/src/state.js @@ -1,5 +1,7 @@ export const appState = { - zoomHoverTimeout: null, // timeout for hovering over the GOD MODE + zoomHideDelayTimeout: null, // Timeout before the hide countdown begins + zoomCountdown: null, // Holds the number of seconds left before zoom hides + zoomCountdownInterval: null, // The interval timer for the countdown isRawOnlyMode: false, // <-- ADD THIS LINE // Stores the parsed visualization data (radar frames, tracks, etc.)