Search code examples
javascripthtmlcsssvgcss-transforms

Scaling SVG with cursor position as transform origin


I am trying to scale an SVG cirle with trackpad (by moving two fingers up/down) and the origin of the transform must be the position of the cursor. The scaling performs well for the first time, and for every other attempt the circle changes position (it should not). This can be seen clearly if the cursor is inside the circle and near the perimeter of it. Below is the code.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" content="width=device-width">

    <style>
        .container
        {
            position: fixed;
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
        }

    </style>
</head>
<body>


<div class="container">
    <svg id="svg" height="600" width="600">
        <circle cx="300" cy="300" r="300" stroke="black" stroke-width="3" fill="white"/>
    </svg>
</div>

<script>
    let scale = 1;
    const e = document.getElementById("svg");

    function wheelZoom(event)
    {
        event.preventDefault();

        scale += event.deltaY * -0.01;
        scale = Math.min(Math.max(.5, scale), 2);

        x = 100*(event.clientX-e.getBoundingClientRect().x)/e.getBoundingClientRect().width;
        y = 100*(event.clientY-e.getBoundingClientRect().y)/e.getBoundingClientRect().height;

        e.style.transformOrigin = `${x}% ${y}%`;
        e.style.transform = `scale(${scale})`;
    }

    e.addEventListener("wheel", wheelZoom);
</script>


</body>
</html>


Solution

  • I'm not sure about a couple of things:

    • what you mean by you don't want the circle to change position
    • also whether you actually want the whole SVG to scale, or just the circle inside the SVG

    In the following demo, I went with keeping the SVG unchanged, but scaling the circle based on where inside the SVG you are when you roll the mouse wheel

    Hopefully it is what you were after.

    //    let scale = 1;
    const svg = document.getElementById("svg");
    const circle = document.querySelector("svg circle");
    
    // Circle transform. Inits to 1:1 scale (called an "identity transform").
    var   circleTransform = svg.createSVGMatrix();  // start
    
    svg.addEventListener("wheel", wheelZoom);
    
    
    function wheelZoom(event)
    {
       event.preventDefault();
    
       // Get the mouse position as SVG coordinates
       var coords = convertScreenCoordsToSvgCoords(event.clientX, event.clientY);
    
       // Calculate an appropriate scale adjustment
       var scale = 1.0 + (event.deltaY * 0.001);
    
       // To scale around the mouse coords, first we transform the coordinate
       // system so that the origin is at the mouse coords.
       circleTransform = circleTransform.translate(coords.x, coords.y);
       // Then we apply the scale
       circleTransform = circleTransform.scale(scale, scale);
       // Finally we move the coordinate system back to where it was
       circleTransform = circleTransform.translate(-coords.x, -coords.y);
    
       // Now we need to update the circle's transform
       var transform = svg.createSVGTransform();        // An SVGTransform DOM object...
       transform.setMatrix(circleTransform);            // set to the new circleTransform...
       circle.transform.baseVal.initialize(transform);  // and used to update the circle transform property
    }
    
    
    function convertScreenCoordsToSvgCoords(x, y) {
       var pt = svg.createSVGPoint();  // An SVGPoint SVG DOM object
       pt.x = x;
       pt.y = y;
       // getScreenCTM tells us the combined transform that determines where 
       // the circle is rendered. Including any viewBox.
       // We use the inverse of that to convert the mouse X and Y to their
       // equivalent values inside the SVG.
       pt = pt.matrixTransform(circle.getScreenCTM().inverse());
       return {'x': pt.x, 'y': pt.y};
    }
    svg {
      background-color: linen;
    }
    <div class="container">
       <svg id="svg" height="600" width="600">
          <circle cx="300" cy="300" r="300" stroke="black" stroke-width="3" fill="white"/>
       </svg>
    </div>