Panning and zooming

Download playground demo application with source code (28 kb)

Description of panning and zooming

Following notes describe the equations to implement panning and zooming. It addresses the situation where only a small cutout of a larger plane (for example, a large image that does not fit onto the display screen) is visible.
In the following, this cutout is called view port and the coordinate system in context of the view port is called screen space. The coordinate system of the larger plane (or canvas) is called global space.

An overview is given in following abstract diagram where the view port is shown in orange inside the global space shown in dark blue.
Figure: Global space and screen space outline

:: The overall question is then how to convert a coordinate in global space to a coordinate in screen space and vice versa.

The following figure shows a screenshot of the demo application. In the right-top corner of the window, there is an mini map of the global (world) space which contains rectangles, lines and circles. The red rectangle inside the minimap shows the view port location inside the global space. The main area of the window shows the actual view port screen space - in this case parts of the line and the circle.

Figure: Pan and zoom demo application screenshot

Panning

Panning means, in our context, actually just the movement of the view port. The view port, more precisely the upper left corner of the view port, has a location inside the global space which is denoted by offset_X and offset_Y. If the viewport is in the upper left corner of the global space, offset_X and offset_Y are zero.

The offsets change when the view port is moved. So what is the location of a point, given in global space, then in screen space?
Let's have a look at an example.

Example: View port moved, point outside
Assume the view port has dimension 640x480 pixels while the global space is far larger.
A point at position p = (1000, 600) is not visible in the view port if it's located at the top left corner of the global space as seen in following diagram (left side).

However if the view port is moved ( = panned), let's say to offset (400, 200), then the point is visible inside the view port.


Figure: Moved viewport

In the moved viewport, the point p = (1000, 600) has an x-coordinate in screen space of 1000 - 400 = 600 and an y-coordinate in screen space of 600 - 200 = 400.
Because the coordinate (600, 400) lies inside the screen space with size (640, 480), the point p is then visible.

For the case that the viewport offset values are zero, it's obvious that the global space and screen space coordinates are the same (of course, not all points are visible in the screen space).
In the general cases where the view port offset is not zero, then the screen space coordinate is calculated by subtracting the view port offset from the global space coordinate:

Panning:
screen_X = global_X - offset_X
screen_Y = global_Y - offset_Y

Zooming

Zooming means to scale the portion of the visible area, thus a coordinate is scaled when transforming between global and screen space. Until now, no explicit scaling has been used, so actually the scale factor was 1.
As definition, a scale factor < 0 means that an object in screen space becomes larger ("zoom in") and a scale factor > 0 means that an object in screen space becomes smaller ("zoom out").
Putting this together results in the fact that during transformation the screen coordinate needs just to be multiplied by the scale factor:

Zooming:
screen_X = (global_X - offset_X) * scale
screen_Y = (global_Y - offset_Y) * scale

This looks pretty simple and still works.
However, when testing this it turns out that the scaling takes place based on the origin (0, 0) which is probably an unwanted behavior. Normally, the scaling shall be performed based on a specific coordinate (e.g. based on the cursor position on screen).
Further, it seems that the viewport also moves during zooming around a defined point while the goal is to obtain a smooth scaling. The basic root cause is that if several small scaling steps are performed, then the actual screen coordinate changes during each scaling step which gives an odd behavior if the scaling is implemented as scrolling with the mouse wheel and the view port seems to move during scaling.
To avoid this behavior, the sequence to implement a scaling step around a defined point out to be a bit more complicated as follows:

  1. Get the world coordinate of the screen coordinate which is used as "scale origin".
  2. Change the scale factor.
  3. Get the world coordinate of the screen coordinate which is used as "scale origin" again - but this time with the new scale factor which will result in a different world coordinate.
  4. So two different world coordinates of the "scale origin" in screen space are calculated: before scaling and after scaling. These can be used to correct the offset factor by adding the difference of both world coordinates to the offset: offset = offset + (origin_before_scaling - origin_after_scaling).

In pseudo code, the implementation may look like the following:

/* get the screen coordinate of cursor in global space _before_ the scale factor is updated */
ConvertScreenToGlobal(x, y, out float gxBefore, out float gyBefore);
/* update scale factor */
UpdateScaleFactor(scale);
/* get new screen position of cursor with applied new zoom factor in global space */
ConvertScreenToGlobal(x, y, out float gxAfter, out float gyAfter);
/* correct global panning offset depending on changed cursor pos caused by zoom */
offset_X = offset_X + (gxBefore - gxAfter);
offset_Y = offset_Y + (gyBefore - gyAfter);

Summary

That is the most important information for panning and zooming. So here the summarized transformations between global space and screen space in a nutshell:


> > Show complete source code < <


That's it! Hope it was interesting for you and have fun and learned something! Keep coding!

Sunshine, June 2022


History