I want to be able to zoom in and out of the map using the scroll wheel. I want to zoom towards the cursor like Google Maps does, but I'm completely lost on how to calculate the movements.

So far, all I have is the resizing, but I now need to change the map position.

What I have: map x and y; map width and height; cursor x and y.

Any help would be most welcome.


4 Answers 4


Assuming that your map_x and map_y mean the top left corner of the screen area over your map in map coordinates and map width and map height are the dimensions of your map at the actual zoom level again in map coordinates (not screen coordinates), first, calculate your zoom ratio:

zoom_x = screen_width / map_width;

Let's assume the aspect ratio does not change: zoom_x = zoom_y = zoom; If you want to change the zoom to next_zoom, first find the coords of the new screen over your map:

new_map_width = screen_width / next_zoom

new_map_height = screen_height / next_zoom

We need to find the change in both values and distribute these depending on the mouse position on the screen. If the mouse position would be in the exact center screen position, then this change would be evenly divided between the top-bottom and left-right sides. Since the mouse is at an arbitrary position, we need to find the ratios for the height and width changes for the sides.

Let's assume that the screen dimensions are screen_max_width and screen_max_height; The new coordinates of your screen (dimensions new_map_width, new_map_height) over your map are:

new_map_x = map_x - (cursor_x / screen_max_width * (new_map_width - map_width))
new_map_y = map_y - (cursor_y / screen_max_height * (new_map_height - map_height))

To convert any map coordinate to a screen coordinate:

screen_x = map_x * zoom

and to convert back:

map_x = screen_x / zoom

I couldn't figure out what Konrad's variables were supposed to mean in my game (map width? My map is infinite, so... that's the same as the screen width then maybe? It didn't work out), so I figured it out from scratch. The way I figured:

(TL;DR: read only the bold parts.)

  • When zooming, some pixels drop off the screen (zooming in) or newly become visible on the screen (zooming out). This number of pixels determines the maximum amount by which you want to shift your view. For example, if you have a view of 800px wide and you have your cursor at 200px, then you want 25% of the new pixels to appear left of the cursor and 75% to the right.
  • To calculate the ratio of pixels that should drop off on the left vs. the right, one has to look from the center and see how far off to the side it is. In the example, the middle is at 400px, so the cursor being at 200px means you're at -25% (-0.25). On the far right (cursor at 800px), you would be at +50% (0.5).
  • view_width / 2 gives us the center and mouse_x - (view_width / 2) gives us the difference, in pixels. To get the (float) ratio, you need to divide that difference by the total again, i.e.
    side_ratio = (mouse_x - (view_width / 2)) / view_width
  • To find the number of pixels that will drop off the side, you want to find the difference between the two zoom levels.
  • The part of the view visible on the current zoom level is view_width * zoom where zoom is 1.0 for no zoom or 0.5 for having everything at half size (zoomed out).
  • The number of pixels newly visible is therefore:
    pixels_difference = (view_width / old_zoom) - (view_width / new_zoom)
  • Finally, multiply the two together:
    view_x_offset += pixels_difference * side_ratio
  • Apply the same to your Y axis, using view_height and mouse_y instead.

Full code to scroll upon mouse wheel for Phaser 3:

function create() { // called upon scene creation
    document.addEventListener('wheel', function(scroll_event) {
        return false;
    }, false);

function handle_scroll(scroll_event) {
    var cam = sceneMain.cameras.cameras[0];

    var oldzoom = cam.zoom;
    var newzoom = oldzoom * (scroll_event.deltaY > 0 ? 1.1 : 0.9); // deltaY>0 means we scrolled down

    var mouse_x = scroll_event.clientX;
    var mouse_y = scroll_event.clientY;

    var pixels_difference_w = (view_width / oldzoom) - (view_width / newzoom);
    var side_ratio_x = (mouse_x - (view_width / 2)) / view_width;
    cam.scrollX += pixels_difference_w * side_ratio_x;

    var pixels_difference_h = (view_height / oldzoom) - (view_height / newzoom);
    var side_ratio_h = (mouse_y - (view_height / 2)) / view_height;
    cam.scrollY += pixels_difference_h * side_ratio_h;


(The code is copied from my game, but I changed some of the structure and edited most variable names to match the explanation, so while I tested my code, please let me know if I introduced any mistakes in this example.)


Or for people using XNA when the system is flipped.

First calculate the mouse cursor in the world. Since the mouse position system in XNA is flipped subtract the mouse Y from the height of the screen.

var mouseinWorld = new Vector2(
    (cursor.X * oldZoomLevel) - world.X, 
    ((cursor.Y - screen.Height) * oldZoomLevel) + world.Y));

Now change your zoomlevel.

Next is to calculate the new world location.

var newX = (cursor.X * newZoomLevel) - mouseinWorld.X;
var newY = mouseinWorld.Y - ((cursor.Y - screen.Height) * newZoomLevel)

Now update the world location with the newly calculated coordinates.

world = new Vector2(newX, newY)

Zoom to cursor for Camera2D script in Godot 4.2 :

extends Camera2D

var zoom_min = Vector2(1, 1)
var zoom_max = Vector2(2, 2)

var zoom_speed = Vector2(0.1, 0.1)

var screen_width = 1920
var screen_height = 1080

func _input(event):
    if event is InputEventMouseButton:
        if event.is_pressed():
            if event.button_index == MOUSE_BUTTON_WHEEL_UP:

            if event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
func zoom_in():
    if zoom < zoom_max:
        var old_zoom = zoom
        var new_zoom = old_zoom + zoom_speed
        update_zoom(old_zoom, new_zoom)

func zoom_out():
    if zoom > zoom_min:
        var old_zoom = zoom
        var new_zoom = old_zoom - zoom_speed
        update_zoom(old_zoom, new_zoom)

func update_zoom(old_zoom: Vector2, new_zoom: Vector2):
    var mouse_x = get_viewport().get_mouse_position().x
    var mouse_y = get_viewport().get_mouse_position().y
    var pixels_difference_x = (screen_width / old_zoom.x) - (screen_width / new_zoom.y)
    var pixels_difference_y = (screen_height / old_zoom.y) - (screen_height / new_zoom.y)
    var side_ratio_x = (mouse_x - (screen_width / 2)) / screen_width
    var side_ratio_y = (mouse_y - (screen_height / 2)) / screen_height
    position.x += pixels_difference_x * side_ratio_x
    position.y += pixels_difference_y * side_ratio_y
    zoom = new_zoom
    if position != get_screen_center_position():
        position = get_screen_center_position() 
  • \$\begingroup\$ I'm not familiar with Godot, and this question is not Godot specific so make sure to make it "idiot proof"; how is zoom initialized? In what situation will the x be different than the y in the cases of zoom_[min, max, speed]? Also, you should make sure that new_zoom is "clamped" between zoom_min and zoom_max to avoid any error that could be introduced with floating point algorithm. \$\endgroup\$
    – Vaillancourt
    May 10 at 16:39

You must log in to answer this question.