canvasluazooming

How could I zoom into a point (mouse positon) on a grid canvas?


(I've seen some similar-ish questions to mine, but they do not really apply to my case.)

I am scripting on a custom canvas for a program plugin using Lua. Drawn onto the canvas is a grid of squares, and the "zoom" is controlled by controlling the width of the squares. There are 7 zoom levels. (If you zoom out, it goes to the next level up; zoom in, it goes to the next level down.) The camera is an xy point that is just offsets from 0,0.

Here is my code sample:

-- Each index is a different zoom level. Lua indexes start at 1. If the user tries zooming past one of the ends, it will stay at said end (clamp).
local squareSizes = {8, 12, 16, 20, 24, 28, 32}
local squareSizeIndex = 3 -- This value is changed externally whenever the user zooms.
local cameraOffset = {x=0, y=0} -- When the user moves/pan's the screen, this value is affected. 

local function drawSquare(x, y, size)
    -- Square drawing placeholder - this function will draw a square at a specified xy location and with the specified size (width and height).
end

local function Update(squares, mousePos) -- Mouse pos is in the same format as the camera variable (eg: mousePos.x is the x pos of the mouse)
    for p, _ in pairs(squares) do -- p is the location a square should be drawn at. p changes based on the squareSize, because number of pixels required to get to the next p (or the square size) is smaller/bigger.
        local x = p.x + cameraOffset.x
        local y = p.y + cameraOffset.y
        drawSquare(x, y, squareSizes[squareSizeIndex])
    end
end

Each square is being drawn at p + camera. If I zoom in/out, the canvas doesn't zoom in or out centered around the mouse—it centers around 0,0 instead.

The only two things changing are p and the square size. How can I implement zooming into the mouse position while retaining my grid system?


Solution

  • After some tinkering, I have got the code to work. Here is my solution/process:

    Before the canvas is zoomed, the mouse is a certain distance away from the camera (for example the x distance would be mousePos.x - cameraOffset.x). The goal of zooming the canvas is to retain this distance. If we divide this by the current square size (squareSizes[squareSizeIndex]), then we get how many squares away the camera is from the mouse.

    local xSquaresAwayFromPoint = (point.x - cameraOffset.x) / squareSizes[squareSizeIndex]
    local ySquaresAwayFromPoint = (point.y - cameraOffset.y) / squareSizes[squareSizeIndex]
    

    After we increment/step the square size index, we can then multiply the value by the new square size.

    -- Increment the square size index. The step is how much we want to increment the squareSizeIndex. Can be a negative value.
    squareSizeIndex = clamp(squareSizeIndex + step, 1, 7)
    -- Get the offset based on the number of squares
    local xOffset = math.floor(squareSizes[squareSizeIndex] * xSquaresAwayFromPoint)
    local yOffset = math.floor(squareSizes[squareSizeIndex] * ySquaresAwayFromPoint)
    

    That gets us the offset from the camera, so we subtract the offset from the camera to get the new camera position.

    cameraOffset.x = point.x - xOffset
    cameraOffset.y = point.y - yOffset
    

    Here is the full code:

    local squareSizes = {8, 12, 16, 20, 24, 28, 32}
    local squareSizeIndex = 3 
    local cameraOffset = {x=0, y=0}
    
    local function clamp(x, min, max)
       -- Clamp number placeholder
    end
    
    local function squareSize()
        -- This is just a getter for squareSizes[squareSizeIndex]
        return clamp(squareSizes[squareSizeIndex], 1, 7)
    end
    
    local function drawSquare(x, y, size)
        -- Square drawing placeholder
    end
    
    local function zoomCanvas(point, step)
        -- Get how many squares away the point is from the camera
        local xSquaresAwayFromPoint = (point.x - cameraOffset.x) / squareSize()
        local ySquaresAwayFromPoint = (point.y - cameraOffset.y) / squareSize()
        -- Increment the square size index
        squareSizeIndex = clamp(squareSizeIndex + step, 1, 7)
        -- Get the offset based on the number of squares
        local xOffset = math.floor(squareSize() * xSquaresAwayFromPoint)
        local yOffset = math.floor(squareSize() * ySquaresAwayFromPoint)
        -- Offset the camera
        cameraOffset.x = point.x - xOffset
        cameraOffset.y = point.y - yOffset
    end
    
    local function Update(squares, step, mousePos) -- Step is the amount to increment the zoom index by. Can be positive or negative.
        zoomCanvas(mousePos, step)
        for p, _ in pairs(squares) do 
            local x = p.x + cameraOffset.x
            local y = p.y + cameraOffset.y
            drawSquare(x, y, squareSize())
        end
    end