Search code examples
javascriptcursorzooming

JavaScript zoom to cursor on absolutely positioned element - how to calculate new center coordinates?


I have a specific implementation of a canvas editor, that is positioned in its parent element absolutely, this canvas can be drag and dropped and zoomed, both features handled by CSS translate(). I Cannot use centering on canvas. I cannot get the zooming to work with centering to cursor, specificaly I have difficulties finding new center of the element. I would like the image to be zoomed and pinned, not to move around the cursor.

For simplification i will use image instead of canvas. Also to be noted, I played with many variants how to get the new (x,y), I'm putting a simpler example to the fiddle, because the latest versions are very overcomplicated.

EDIT: working fiddle, details in answer JSFiddle

OBSOLETE Calculations: As you can see, the divergence is increasing between target and actual result.

Cursor X Cursor Y Target coordinates Result coordinates
588 -275 (-97.5 ; 45.5) (-98 ; 45.5)
706.2 -330 (-165 ; 77) (-170.88 ; 79.22)
827.8 -386.8 (-225 ; 105) (-210.225 ; 97.86)

HTML:

<div id="app">
  <div id="editor">
    <img id="image" src="https://placehold.co/600x400"/>
  </div>
</div>

CSS:

#app{
  background: #eee;
  width: 800px;
  height: 500px;
}
#editor{
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  overflow: hidden;
}
#image{
  position: absolute;
  top: 50%;
  left: 50%;
  transform-origin: center center;
  transform: translate(-50%, -50%) scale(1) translate(0px, 0px);
}

JS:

window.onload=function(){
  var scroll_zoom = new ScrollZoom()
}

function ScrollZoom(){
  const editor = document.querySelector('#editor');
  
  var minZoom = 0.2
  var maxZoom = 3
  var zoomStep = 0.2
    
  var currentZoom = 1
  var currentPos = {x:0,y:0}
  
  editor.addEventListener('wheel', scrolled);
  
  function scrolled(e){
    e.preventDefault();
    const direction = Math.max(-1, Math.min(1, e.deltaY));
    
    if (direction > 0 ? currentZoom <= minZoom : currentZoom >= maxZoom) return;
        
    const image = document.querySelector('#image');
    const imageRect = image.getBoundingClientRect();

    const newZoom = parseFloat((direction > 0 ? currentZoom - zoomStep : currentZoom + zoomStep).toFixed(1))
    
    // image center coordinates to count cursor (x, y) relatively from center of the image
    const imageCenterX = imageRect.width / 2;
    const imageCenterY = imageRect.height / 2;
    
    // cursor position relative from image center
    const cursorX = e.clientX - imageRect.left - imageCenterX;
    const cursorY = e.clientY - imageRect.top - imageCenterY;
    

    const scaleRatio = currentZoom / newZoom;
    
    const adjustedX = cursorX * (1 - scaleRatio);
    const adjustedY = cursorY * (1 - scaleRatio);
    
    // find new center of the image
    // !!! probably the wrong calculation
    const newX = -(position.x / newZoom) + adjustedX;
    const newY = -(position.y / newZoom) + adjustedY;
    
    update(newZoom, newPosX, newPosY)
  }
  
  function update(newZoom,newPosX,newPosY){   
    currentZoom = newZoom;
    currentPos = {x:-newPosX,y:-newPosY}
    
      document.getElementById('image').style.transform = `translate(-50%, -50%) translate(${currentPos.x}px, ${-currentPos.y}px) scale(${newZoom})`
  }
}

Image to better see the coordinates: coordinates image

I tried complicated calibrating of the coordinates by calculating displacement, but the gaps were larger on bigger zooms. I expect the image to move around as less as possible.


Solution

  • After turning back to start, I managed to find out the problem with initial computation problems and proved a working solution:

    HTML is the same.

    CSS had an issue! Scale has to be done after the translate!:

    #app{
      ...
    }
    #editor{
      ...
    }
    #image{
      ...
      transform: translate(-50%, -50%) translate(0px, 0px) scale(1);
    }
    

    JS, computing coordinates now work with simply multiplying cursor coordinates by +-stepSize (20%/-20% in this case) and adding them to the previous image center coordinates:

    window.onload=function(){
      var scroll_zoom = new ScrollZoom()
    }
    
    function ScrollZoom(){
      ...
      
      function scrolled(e){
        ...
        
        const scaleRatio = newZoom/currentZoom; 
        
        // image center coordinates to count cursor (x, y) relatively from center of the image
        const imageCenterX = imageRect.width / 2;
        const imageCenterY = imageRect.height / 2;
        
        // cursor position relative from image center
        const cursorX = e.clientX - imageRect.left - imageCenterX;
        const cursorY = e.clientY - imageRect.top - imageCenterY;
        
        // coordinates adjusted by 20%/-20%
        const adjustedX = cursorX * (1 - scaleRatio); 
        const adjustedY = cursorY * (1 - scaleRatio);
        
        // absolute linear movement, simply add to previous position
        const newX = currentPos.x + adjustedX;
        const newY = currentPos.y + adjustedY;
        
        update(newZoom, newX, newY) 
      } 
      
      function update(newZoom,newPosX,newPosY){   
        currentZoom = newZoom;
        currentPos = {x:newPosX,y:newPosY}
        
        document.getElementById('image').style.transform = `translate(-50%, -50%) translate(${currentPos.x}px, ${currentPos.y}px) scale(${newZoom})`
      }
    }
    

    Working playground: JSfiddle