Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 58

Implementing Pan: Translation Offset

Recommended resources

Hello! Welcome back to our journey in building a high-performance telemetry engine.

In our last lesson, you successfully brought our static plot to life by implementing cursor-centered zooming. You introduced the crucial state variables pan_offset and zoom_level and created the WorldToScreen and ScreenToWorld transformation functions. This was a major step in making our visualization interactive.

Today, we will complete the core navigation functionality by implementing panning. You will learn how to "grab" the plot with the mouse and drag it around. This is achieved by capturing mouse movement and using it to update the pan_offset you created in the last lesson. By the end of this lesson, you will have a fully navigable, infinite 2D canvas—the foundation for our advanced plotter.

The Core Concept: Translating the World

Panning is a translation transformation. In computer graphics, this means shifting the position of every point in a coordinate system by a fixed amount.

In our plotter, the pan_offset variable represents the "world-space" coordinate that is mapped to the top-left corner of our visible canvas. To pan the view, we simply need to change this offset.

Transformations Between 2D Coordinate Systems: Translation and Rotation
This image illustrates the concept of a translated coordinate system. Think of the `xy` system as your screen and the `x'y'` system as your data's "world." Panning is the act of moving the origin of the data world (`x0, y0`), which is exactly what our `pan_offset` variable represents.

The process is straightforward:

  1. Detect when the user is dragging the mouse across the plot.
  2. Get the distance the mouse has moved since the last frame. This is called the mouse delta and is given in screen-space pixels.
  3. Crucially, convert this screen-space delta into a world-space delta. A 10-pixel movement on screen corresponds to a different distance in our data depending on the current zoom level.
  4. Update pan_offset with this calculated world-space delta.

Let's watch a short segment of the video we saw last time, now focusing specifically on the logic behind panning.

Programming Panning & Zooming

This clip from javidx9 explains the fundamental relationship between world space, screen space, and the offset that enables panning. It clearly visualizes the simple subtraction that forms the core of our transformation functions.

Watch the section from 02:04 to 04:17. The key formula presented is ScreenX = WorldX - OffsetX. This directly relates to our WorldToScreen function, where pan_offset is the offset.


From Theory to Code: Implementing Panning

The video establishes the concept. Now, how do we implement the interactive part? We need to handle the mouse input and perform the correct calculation to update our pan_offset.

The most important step is converting the mouse delta from screen space to world space. Since our zoom_level represents "pixels per world unit," the conversion is a simple division:

world_space_delta = screen_space_delta / zoom_level

This ensures that the panning speed feels consistent regardless of how zoomed in or out you are. Let's look at a clean, practical implementation of this.

Implementation of pan and zoom on a 2D canvas

This C++ code gist provides a clear and concise implementation of both pan and zoom logic. We will focus on the panning part, which demonstrates exactly how to calculate and apply the offset.

Read the section titled 'Panning Implementation'. Focus on the OnPanMove function. Notice the key line: const Vector2f offset = (mouse_screen_space - selection.pan_start) / selection.canvas.scale;. This is the screen-to-world delta conversion we just discussed. The next line, selection.canvas.position = selection.canvas.position - offset;, applies this to the canvas's main offset.

Notice the code subtracts the offset: canvas.position = canvas.position - offset. This might seem counter-intuitive at first, but it's correct. If you drag your mouse to the right (a positive X delta), you want the data to appear to move to the left. To achieve this, you must shift your view's origin (the pan_offset) to the right, which means subtracting the negative world-space delta from your view's position. However, a simpler way to think about it is that you are moving the world in the opposite direction of the mouse drag. Drag right, world moves left.


Integrating Panning into our ImGui Plot

Now let's add this logic to our PlotMyData function. We can place it right alongside the zoom handling code from the previous lesson. ImGui makes this very convenient with ImGui::IsMouseDragging and ImGui::GetIO().MouseDelta.

Here is the updated section of the PlotMyData function:

// ... inside PlotMyData(), after the InvisibleButton ...

    const bool is_hovered = ImGui::IsItemHovered(); // Is the mouse over our canvas?
    const bool is_active = ImGui::IsItemActive();   // Is the user interacting with our canvas (e.g., clicking/dragging)?

    // 2. Handle Zooming 
    if (is_hovered && ImGui::GetIO().MouseWheel != 0)
    {
        // ... same zoom code as the previous lesson ...
        ImVec2 mouse_pos_screen = ImGui::GetMousePos();
        ImVec2 mouse_pos_world_before = ScreenToWorld(mouse_pos_screen, pan_offset, zoom_level, canvas_p1, canvas_p2);

        float zoom_delta = ImGui::GetIO().MouseWheel * 0.1f;
        zoom_level *= std::pow(2.0f, zoom_delta);
        zoom_level = std::clamp(zoom_level, 1.0f, 1000.0f);

        ImVec2 mouse_pos_world_after = ScreenToWorld(mouse_pos_screen, pan_offset, zoom_level, canvas_p1, canvas_p2);
        
        pan_offset.x += mouse_pos_world_before.x - mouse_pos_world_after.x;
        pan_offset.y += mouse_pos_world_before.y - mouse_pos_world_after.y;
    }

    // 3. Handle Panning (NEW CODE)
    if (is_active && ImGui::IsMouseDragging(ImGuiMouseButton_Left))
    {
        // Get mouse delta in screen space
        ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;

        // Convert screen-space delta to world-space delta
        // Note: We negate the delta to move the world in the opposite direction of the mouse
        // We also flip the Y-axis because ImGui's screen coordinates have Y pointing down.
        pan_offset.x -= mouse_delta.x / zoom_level;
        pan_offset.y += mouse_delta.y / zoom_level;
    }
    
    // 4. Draw the grid/data
    // ... rest of the drawing code is the same ...

The InvisibleButton you added in the previous lesson already sets the stage for this. ImGui::IsMouseDragging(ImGuiMouseButton_Left) will only be true if the user started dragging with the left mouse button while the cursor was over that invisible button, which is exactly the behavior we want.

Test your understanding!

You've implemented panning. Now, what would happen if you forgot to divide ImGui::GetIO().MouseDelta by zoom_level before updating pan_offset? Describe the user experience at high and low zoom levels.

Show answer

If you forgot to divide by zoom_level, the panning speed would be tied to pixels, not data units. The interaction would feel broken and uncontrollable:

  • When zoomed far out (low zoom_level): A small mouse drag (e.g., 100 pixels) would cause the view to fly across a huge distance in world space, as those 100 pixels might represent thousands of data points. It would be almost impossible to make fine adjustments.
  • When zoomed far in (high zoom_level): A large mouse drag across the entire screen would barely move the view, because each pixel represents a tiny fraction of a data unit. Panning would feel sluggish and unresponsive.

Scaling the delta by the zoom_level is what makes the panning feel consistent and as if you are "grabbing" the data itself.


Conclusion

Excellent work! You have now implemented the two fundamental navigation tools for any 2D data visualization: zoom and pan. Your plotter is no longer a static image but a truly interactive canvas where you can freely explore a virtual data space.

Key Takeaways:

  • Panning is a translation of the view, implemented by modifying the world-space pan_offset.
  • Mouse movement is reported by ImGui in screen space (pixels) via ImGui::GetIO().MouseDelta.
  • To correctly pan, you must convert the screen-space mouse delta into a world-space delta by dividing by the current zoom_level.
  • The logic for panning fits neatly alongside the zoom logic, with both manipulating the pan_offset and zoom_level state variables to control the view.

What's Next?

We now have a fully navigable, infinite canvas. But without context, it's just a scrolling, scaling line. To make it a truly useful analysis tool, we need reference points.

In the next lesson, we will begin rendering basic axes and labels for our custom plot. These elements will need to adapt dynamically to the current pan and zoom state, providing the crucial context needed to understand the data being displayed. This will mark the transition from a simple drawing canvas to a recognizable and functional plotter.