Browse Source

feat(zoom): Implement advanced zoom controls and UX improvements

This commit introduces a suite of major enhancements to the "Zoom Mode" functionality, focusing on user experience, interaction smoothness, and bug fixes. It refines the data display logic, adds interactive visual feedback, and implements a more intelligent auto-hide behavior.

#### 1. Mouse Smoothing for Zoom Navigation

To address jittery mouse movements at high magnification, a linear interpolation (lerp) filter has been implemented.

-   **`radarSketch.js`**: A "smoothed" mouse coordinate is now calculated on each frame when zoom mode is active. This smoothed position is used for all zoom-related logic, including positioning the zoom window's view and detecting hovered items.
-   **`drawUtils.js`**: The `handleCloseUpDisplay` function has been updated to accept the smoothed coordinates, ensuring that hover detection is perfectly synchronized with the smoothed visual feedback. This results in a much more fluid and controllable navigation experience in the zoom window.

#### 2. Dynamic and Visual Zoom Feedback

The zoom interaction has been made more intuitive with several visual aids.

-   **Variable Hover Radius**: The hover detection radius is now inversely proportional to the zoom factor. This provides a larger, more forgiving selection area when zoomed out and a smaller, more precise area when zoomed in. The formula `constrain(80 / zoomFactor, 5, 25)` is used to keep the radius within a usable range.
-   **Zoom Area Rectangle**: A semi-transparent, dashed red rectangle is now drawn on the main radar canvas, centered on the smoothed mouse position. This rectangle visually represents the exact area being magnified in the zoom window.
-   **Debug Circle**: For tuning and visualization, a temporary purple circle is drawn on both the main radar canvas and within the zoom sketch. This circle's size dynamically matches the current hover radius, making it easy to see the selection area at any zoom level.

#### 3. Intelligent Auto-Hide with Countdown

The behavior of the zoom window when the user stops hovering over points has been significantly improved.

-   **Grace Period**: A 2-second grace period has been added. When the user stops hovering, the zoom window remains active and continues to follow the mouse for 2 seconds before any closing action begins.
-   **Visual Countdown**: After the grace period, a 3-second countdown is initiated. The zoom window displays a "Closing in 3... 2... 1..." message, clearly communicating its state to the user.
-   **State Management**: The logic in `radarSketch.js` and `state.js` was refactored to correctly manage the delay timer (`zoomHideDelayTimeout`) and the countdown interval (`zoomCountdownInterval`), ensuring the grace period works as intended and the UI updates smoothly.

#### 4. Bug Fixes and Data Consistency

-   **Data Synchronization**: Corrected a critical bug where tooltips and rendered markers were using data from different frames. All drawing and tooltip logic in `radarSketch.js`, `zoomSketch.js`, and `drawUtils.js` has been unified to use data exclusively from the `appState.currentFrame`.
-   **Console Warning Fix**: Resolved a persistent `CanvasTextAlign` error in the console caused by an incorrect `textAlign(p.LEFT - 2)` call in `zoomSketch.js`.

These changes culminate in a more robust, intuitive, and polished zoom feature that is both more powerful for analysis and more pleasant to use.
refactor/modularize
RUSHIL AMBARISH KADU 7 months ago
parent
commit
7344c59ba2
  1. 16
      steps/src/drawUtils.js
  2. 105
      steps/src/p5/radarSketch.js
  3. 40
      steps/src/p5/zoomSketch.js
  4. 4
      steps/src/state.js

16
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;

105
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.
// --- 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.");
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";
appState.zoomHoverTimeout = null;
}, COOLING_PERIOD_MS);
} 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

40
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);

4
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.)

Loading…
Cancel
Save