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

179 Views Asked by At

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.

2

There are 2 best solutions below

0
D8tectx On BEST ANSWER

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

1
Jimmy Ramani On

Js :

const editor = document.querySelector('.editor');
const image = document.getElementById('image');
const minZoom = 0.5;
const maxZoom = 2;
const zoomStep = 0.1;

let currentZoom = 1;

editor.addEventListener('wheel', scrolled);

function scrolled(e) {
  e.preventDefault();

  const direction = Math.max(-1, Math.min(1, e.deltaY || e.originalEvent.wheelDelta));

  if ((direction > 0 && currentZoom >= maxZoom) || (direction < 0 && currentZoom <= minZoom)) {
    return;
  }

  const rect = editor.getBoundingClientRect();
  const cursorX = e.clientX - rect.left;
  const cursorY = e.clientY - rect.top;

  const scale = direction > 0 ? 1 + zoomStep : 1 - zoomStep;
  const newZoom = currentZoom * scale;

  const deltaX = cursorX * (1 - scale);
  const deltaY = cursorY * (1 - scale);

  const newPosX = (cursorX - deltaX) / currentZoom;
  const newPosY = (cursorY - deltaY) / currentZoom;

  update(newZoom, newPosX, newPosY);
}

function update(zoom, posX, posY) {
  currentZoom = zoom;

  image.style.transform = `translate(-50%, -50%) scale(${zoom}) translate(${posX}px, ${posY}px)`;
}

CSS :

#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);
}

Html :

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

This code calculates the new center coordinates (newPosX and newPosY) based on the cursor position and the current zoom level. It then updates the transform property of the image accordingly.

Please note that this code assumes that the image is initially centered in the editor, and the editor has the same dimensions as the image. If your actual implementation differs, you may need to adjust the code accordingly.