htmlsvgzoomingviewbox

Centering on a specific point when zooming using SVG viewBox with arbitrary initial state


I have a SVG HTML element and I have implemented panning and zooming into it using the mouse. The current implementation of the zooming functionality just multiplies the original width and height of the element by a number that changes when the user scrolls the mouse.

This implementation preserves the origin (0,0) and all other points appear to move closer/further away from it depending on the direction of the zoom.

Intuitively and based o this question. I know, that If I want to zoom in/out on the point the mouse is currently pointing at, I have to pan the viewBox.

I have already looked at the linked questio, as well as two otheres, but I was unable to successfully apply the suggested solutions to my problem. I have also tried to derive the correct formula multiple times, but all my attempts so far have failed.

I am most likely missunderstanding something about the problem and I seem to be unable to generalise the existing answers to my problem.

The following values represent the current state of my viewBox:

I compute the zoomFactor as a function of the scroll variable (Math.exp(scroll/1000)) and set the viewBox property of my SVG as follows: `${offsetX} ${offsetY} ${width * zoomFactor} ${height * zoomFactor}`.

What I am struggling with, is computing the new offsetX and offsetY values based on the previous state and the current position of the mouse inside of the SVG.

processMouseScroll(event: WheelEvent) {
    const oldZoomFactor = zoomFactor(this.scroll);
    const newZoomFactor = zoomFactor(this.scroll + event.deltaY);

    this.scroll = this.scroll + event.deltaY;
    this.offsetX = ???;
    this.offsetY = ???;
}

How do I compute the new offsets, based on the previous state, so that the when scrolling the mouse, the point bellow it will appear to be stationary?

Thank you for your answers.


Solution

  • I have finally managed to get it working. Turns out, the answer from the first question I found was correct, but my understanding of SVG viewBox was incorrect and I used bad mouse coordinates.

    coordinate changes when scaling a viewBox

    the offset (min-x and min-y; drawn green) of a viewBox is abbsolute and does not depend on the width and height of the viewBox. The mouse coordinates relative to the SVG element (coordinates drawn in black, SVG element drawn in red) are relative to the size of the viewBox. If I enlarge the viewBox, then the part of the picture I can see inside of it shrinks and 100px line drawn by the mouse will cover more of the image.

    If we set the size of the dimensions of the viewBox to be the same as the size of the SVG element (initial state), we have a 1:1 scale between the image and the viewBox (the red rectangle would cover the entire image, bordered black). When we make the viewBox smaller we will not fit the entire image into it and therefore the image will appear to be larger.

    If we want to compute the absolute position of our mouse in relation to the entire image we can do it like this (same for Y): position = offsetX + zoomFactor * mouseX (mouseX relative to the SVG element).

    When we zoom, we change the factor, but don't change the position of the mouse. If we want the absolute position under the mouse to remain the same, we have to solve the following set of equations:

    oldPosition = oldOffsetX + oldZoomFactor * mouseX

    newPosition = newOffsetX + newZoomFactor * mouseX

    oldPosition = newPosition

    we know the mouse position, both zoom factors and the old offset, therefore we solve for the new offset and get:

    newOffsetX = oldOffsetX + mouseX * (oldZoomFactor - newZoomFactor)

    which is the final formula and very similar to this answer.

    Put together we get the final working solution:

    processMouseScroll(event: WheelEvent) {
        const oldZoomFactor = zoomFactor(this.scroll);
        const newZoomFactor = zoomFactor(this.scroll + event.deltaY);
    
        // mouse position relative to the SVG element
        const mouseX = event.pageX - (event.target as SVGElement).getBoundingClientRect().x;
        const mouseY = event.pageY - (event.target as SVGElement).getBoundingClientRect().y;
    
        this.scroll = this.scroll + event.deltaY;
        this.offsetX = this.offsetX + mouseX * (oldZoomFactor - newZoomFactor);
        this.offsetY = this.offsetY + mouseY * (oldZoomFactor - newZoomFactor);
    }