0

I am writing an image viewer JS component and i'm currently stuck on the calculation for the zooming in. I figured out the zooming in or out on one point (instead of just the center) using css transforms. I'm not using transform-origin because there will be multiple points of zooming in or out, and the position must reflect that. However, that's exactly where i'm stuck. The algorithm i'm using doesn't calculate the offset correctly after trying to zoom in (or out) when the mouse has actually moved from the initial position and I can't for the life of me figure out what's missing in the equation.

The goal is whenever the mouse is moved to another position, the whole image should scale with that position as the origin, meaning the image should scale but the point the mouse was over should remain in its place. For reference, look at this JS component. Load an image, zoom in somewhere, move your mouse to another position and zoom in again.

Here's a pen that demonstrates the issue in my code: https://codepen.io/Emberfire/pen/jOWrVKB

I'd really apreciate it if someone has a clue as to what i'm missing :)

Here's the HTML

<div class="container">
    <div class="wrapper">
        <div class="image-wrapper">
            <img class="viewer-img" src="https://cdn.pixabay.com/photo/2020/06/08/17/54/rain-5275506_960_720.jpg" draggable="false" />
        </div>
    </div>
</div>

Here's the css that i'm using for the component:

* {
    box-sizing: border-box;
}

html, body {
    width: 100%;
    height: 100%;
    margin: 0;
}

.container {
    width: 100%;
    height: 100%;
}

.wrapper {
    width: 100%;
    height: 100%;
    overflow: hidden;
    transition: all .1s;
    position: relative;
}

.image-wrapper {
    position: absolute;
}

.wrapper img {
    position: absolute;
    transform-origin: top left;
}

And here's the script that calculates the position and scale:

let wrapper = document.querySelector(".wrapper");
let image = wrapper.querySelector(".image-wrapper");
let scale = 1;
image.querySelector("img").addEventListener("wheel", (e) => {
    if (e.deltaY < 0) {
        scale = Number((scale * 1.2).toFixed(2));

        let scaledX = e.offsetX * scale;
        let scaledY = e.offsetY * scale;

        imageOffsetX = e.offsetX - scaledX;
        imageOffsetY = e.offsetY - scaledY;

        e.target.style.transform = `scale3d(${scale}, ${scale}, ${scale})`;
        image.style.transform = `translate(${imageOffsetX.toFixed(2)}px, ${imageOffsetY.toFixed(2)}px)`;
    } else {
        scale = Number((scale / 1.2).toFixed(2));

        let scaledX = e.offsetX * scale;
        let scaledY = e.offsetY * scale;

        imageOffsetX = e.offsetX - scaledX;
        imageOffsetY = e.offsetY - scaledY;

        e.target.style.transform = `scale3d(${scale}, ${scale}, ${scale})`;
        image.style.transform = `translate(${imageOffsetX}px, ${imageOffsetY}px)`;
    }
});
Emberfire
  • 89
  • 7
  • 1
    Interesting question. Already solved? If you haven't answered yet, I'll try to solve it today – Aks Jacoves Jun 13 '20 at 20:56
  • @AksJacoves I haven't, no. I figured out the dragging of the page smoothly, but the zooming still isn't complete. Thank you for your effort :) – Emberfire Jun 14 '20 at 06:29

2 Answers2

1

Thinking a little, I understood where the algorithm failed. Your calculation is focused on the information of offsetx and offsety of the image, the problem is that you were dealing with scale and with scale the data like offsetx and offsety are not updated, they saw constants. So I stopped using scale to enlarge the image using the "width" method. It was also necessary to create two variables 'accx' and 'accy' to receive the accumulated value of the translations

let wrapper = document.querySelector(".wrapper");
let image = wrapper.querySelector(".image-wrapper");
let img = document.querySelector('img')
let scale = 1;
let accx = 0, accy = 0
image.querySelector("img").addEventListener("wheel", (e) => {
  if (e.deltaY < 0) {
    scale = Number((scale * 1.2));
    accx += Number(e.offsetX * scale/1.2 - e.offsetX * scale)
    accy += Number(e.offsetY * scale/1.2 - e.offsetY * scale)

  } else {
    scale = Number((scale / 1.2));
    accx += Number(e.offsetX * scale * 1.2 - e.offsetX * scale)
    accy += Number(e.offsetY * scale * 1.2 - e.offsetY * scale)
  }
  e.target.style.transform = `scale3D(${scale}, ${scale}, ${scale})`
  image.style.transform = `translate(${accx}px, ${accy}px)`;
});
* {
  box-sizing: border-box;
}

html,
body {
  width: 100%;
  height: 100%;
  margin: 0;
}

.container {
  width: 100%;
  height: 100%;
  position: relative;
}

.wrapper {
  width: 100%;
  height: 100%;
  overflow: hidden;
  transition: all 0.1s;
  position: relative;
}

.image-wrapper {
  position: absolute;
}

.wrapper img {
  position: absolute;
  transform-origin: top left;
}

.point {
  width: 4px;
  height: 4px;
  background: #f00;
  position: absolute;
}
<div class="container">
  <div class="wrapper">
    <div class="image-wrapper">
      <img class="viewer-img" src="https://cdn.pixabay.com/photo/2020/06/08/17/54/rain-5275506_960_720.jpg" draggable="false" />
    </div>
  </div>
</div>

Preview the effect here: JsFiddle

Aks Jacoves
  • 849
  • 5
  • 9
  • Hey, thanks for getting back and offering a solution :) I tried your version and it worked! However, I initially used transforms exclusively in order to avoid unnecessary browser reflows (since the image is in an absolute container) and to take advantage of GPU rendering as much as possible. Could the same result be achieved by multiplying the offsetX by the current scale? Thank you for your time :) – Emberfire Jun 14 '20 at 18:46
  • 1
    I didn't think about the computational cost side, you're right. I will edit my answer using scale3D, the calculation is very simple – Aks Jacoves Jun 14 '20 at 19:05
  • 1
    This works perfectly! Now to dig in and fully understand what i was doing wrong... anyway, thank you so much for helping me <3 – Emberfire Jun 14 '20 at 19:22
0

You're missing to calculate the difference in the transform points after an element scales.
I would suggest to use transform-origin set to center, that way you can center initially your canvas using CSS flex.
Create an offset {x:0, y:0} that is relative to the canvas transform-origin's center

const elViewport = document.querySelector(".viewport");
const elCanvas = elViewport.querySelector(".canvas");

let scale = 1;
const scaleFactor = 0.2;
const offset = {
  x: 0,
  y: 0
}; // Canvas translate offset

elViewport.addEventListener("wheel", (ev) => {
  ev.preventDefault();
  const delta = Math.sign(-ev.deltaY); // +1 on wheelUp, -1 on wheelDown

  const scaleOld = scale; // Remember the old scale
  scale *= Math.exp(delta * scaleFactor); // Change scale

  // Get pointer origin from canvas center
  const vptRect = elViewport.getBoundingClientRect();
  const cvsW = elCanvas.offsetWidth * scaleOld;
  const cvsH = elCanvas.offsetHeight * scaleOld;
  const cvsX = (elViewport.offsetWidth - cvsW) / 2 + offset.x;
  const cvsY = (elViewport.offsetHeight - cvsH) / 2 + offset.y;
  const originX = ev.x - vptRect.x - cvsX - cvsW / 2;
  const originY = ev.y - vptRect.y - cvsY - cvsH / 2;

  const xOrg = originX / scaleOld;
  const yOrg = originY / scaleOld;

  // Calculate the scaled XY 
  const xNew = xOrg * scale;
  const yNew = yOrg * scale;

  // Retrieve the XY difference to be used as the change in offset
  const xDiff = originX - xNew;
  const yDiff = originY - yNew;

  // Update offset
  offset.x += xDiff;
  offset.y += yDiff;

  // Apply transforms
  elCanvas.style.scale = scale;
  elCanvas.style.translate = `${offset.x}px ${offset.y}px`;
});
* {
  margin: 0;
  box-sizing: border-box;
}

.viewport {
  margin: 20px;
  position: relative;
  overflow: hidden;
  height: 200px;
  transition: all .1s;
  outline: 2px solid red;
  display: flex;
  align-items: center;
  justify-content: center;
}

.canvas {
  flex: none;
}
<div class="viewport">
  <div class="canvas">
    <img src="https://cdn.pixabay.com/photo/2020/06/08/17/54/rain-5275506_960_720.jpg" draggable="false" />
  </div>
</div>

For more info head to this answer: zoom pan mouse wheel with scrollbars

Roko C. Buljan
  • 196,159
  • 39
  • 305
  • 313