Javascript pan and wheel zoom image

89 Views Asked by At

I have the following code used for panning image. Panning works well, but wheel zoom not. When I start zooming anywhere above the image I want it to zoom from that cursor point outwards. Currently image moves from the mouse.

var mouseX,
  mouseY,
  mouseTX,
  mouseTY,
  panning = false,
  panEventAdded,
  ts = {
    scale: 1,
    translate: {
      x: 0,
      y: 0
    }
  }


var boxChildWrap = document.querySelector(".aplbox-child");

addPanEvents()

function addPanEvents() {

  panEventAdded = true;

  boxChildWrap.onpointerdown = pointerdownHandler;

  boxChildWrap.onpointerup = pointerupHandler;
  boxChildWrap.onpointercancel = pointerupHandler;
  boxChildWrap.onpointerout = pointerupHandler;
  boxChildWrap.onpointerleave = pointerupHandler;
  document.onpointerup = pointerupHandler;

}

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


  panning = true;

  mouseX = e.clientX;
  mouseY = e.clientY;
  mouseTX = ts.translate.x;
  mouseTY = ts.translate.y;

  boxChildWrap.onpointermove = pointermoveHandler
};

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

  let rec = boxChildWrap.getBoundingClientRect();

  const x = e.clientX;
  const y = e.clientY;

  if (!panning) {
    return;
  }

  ts.translate.x = mouseTX + (x - mouseX);
  ts.translate.y = mouseTY + (y - mouseY);

  setTransform();

};

function pointerupHandler(e) {
  panning = false;
  boxChildWrap.onpointermove = null
}

function setTransform() {
  const transform = 'translate(' + ts.translate.x + 'px,' + ts.translate.y + 'px) scale(' + ts.scale + ') translate3d(0,0,0)';
  boxChildWrap.style.transform = transform;
  boxChildWrap.style.transition = '';
}

boxChildWrap.addEventListener("wheel", function(e) {

  var xs = (e.clientX - ts.translate.x) / ts.scale,
    ys = (e.clientY - ts.translate.y) / ts.scale,
    delta = (e.wheelDelta ? e.wheelDelta : -e.deltaY);
  (delta > 0) ? (ts.scale *= 1.2) : (ts.scale /= 1.2);

  ts.translate.x = e.clientX - xs * ts.scale;
  ts.translate.y = e.clientY - ys * ts.scale;

  setTransform();

})
.aplbox-child {
  max-height: 100% !important;
  max-width: 100% !important;
  position: relative;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  transform: scale(1);
  transition: transform;
  touch-action: none;
}

.aplbox-child img {
  user-select: none;
  display: block;
  max-height: 100%;
  max-width: 100%;
}
<div class="aplbox-child aplbox-image"><img src="https://picsum.photos/1280/720"></div>

1

There are 1 best solutions below

6
Roko C. Buljan On
  • Your handler events should originate from a parent, not from the element you need to resize. This way you'll drastically improve the UI/UX.
  • All the calculations should be relative to the canvas center
  • Use a constant scale factor
  • Use scale min / max values, to prevent the canvas getting too small, large

Here's your code (with a slightly modified HTML as well) with some additional functions taken from the ZoomPan project

const el = (sel, par = document) => par.querySelector(sel);
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);

const elViewport = el(".aplbox");
const elCanvas = el(".aplbox-child", elViewport);

const ts = {
  scale: 1,
  scaleMin: 0.2,
  scaleMax: 6,
  scaleFactor: 0.2,
  x: 0,
  y: 0
};

/**
 * Get client size and position of viewport
 * @returns {object} {width,height,x,y} of the viewport Element
 */
const getViewport = () => {
  return elViewport.getBoundingClientRect();
};

/**
 * Get canvas size and position relative to viewport
 * @returns {object} {width,height,x,y} of the (scaled) canvas Element
 */
const getCanvas = () => {
  const vpt = getViewport();
  const width = elCanvas.offsetWidth * ts.scale;
  const height = elCanvas.offsetHeight * ts.scale;
  const x = (vpt.width - width) / 2 + ts.x;
  const y = (vpt.height - height) / 2 + ts.y;
  return { width, height, x, y };
};

/**
 * Get pointer origin XY from pointer position
 * relative to canvas center
 * @param {PointerEvent|Object} ev Event with x,y pointer coordinates of Object {x,y}
 * @return {object} {originX, originY} offsets from canvas center
 */
const getPointerOrigin = ({ x, y }) => {
  const vpt = getViewport();
  const cvs = getCanvas();
  const originX = x - vpt.x - cvs.x - cvs.width / 2;
  const originY = y - vpt.y - cvs.y - cvs.height / 2;
  return { originX, originY }
};

/** Apply transforms to canvas */ 
const transform = () => {
  elCanvas.style.scale = ts.scale;
  elCanvas.style.translate = `${ts.x}px ${ts.y}px`;
};

/**
 * Apply a new scale at a given origin point relative from canvas center
 * Useful when zooming in/out at a specific "anchor" point.
 * @param {number} scaleNew 
 * @param {number} originX Scale to X point (relative to canvas center)
 * @param {number} originY Scale to Y point (relative to canvas center) 
 */
const scaleTo = (scaleNew = 1, originX, originY) => {
  scaleNew = clamp(scaleNew, ts.scaleMin, ts.scaleMax);
  const scaleOld = ts.scale;

  // Calculate the XY as if the element is in its
  // original, non-scaled size: 
  const xOrg = originX / scaleOld;
  const yOrg = originY / scaleOld;

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

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

  ts.scale = scaleNew;
  ts.x = ts.x + xDiff;
  ts.y = ts.y + yDiff
  
  transform();
};


// Event handlers

const pointerDownHandler = (evt) => {
  evt.preventDefault();
  elViewport.setPointerCapture(evt.pointerId);
  elViewport.addEventListener("pointermove", pointerMoveHandler);
  elViewport.addEventListener("pointerup", pointerUpHandler);
}; 

const pointerMoveHandler = (evt) => {
  ts.x += evt.movementX;
  ts.y += evt.movementY;
  transform();
};

const pointerUpHandler = (evt) => {
  elViewport.releasePointerCapture(evt.pointerId);
  elViewport.removeEventListener("pointermove", pointerMoveHandler);
  elViewport.removeEventListener("pointerup", pointerUpHandler);
};

const scaleHandler = (evt) => {
  evt.preventDefault();
  const delta = Math.sign(-evt.deltaY);
  const scaleNew = ts.scale * Math.exp(delta * ts.scaleFactor);
  const {originX, originY} = getPointerOrigin(evt);
  scaleTo(scaleNew, originX, originY);
};

elViewport.addEventListener("pointerdown", pointerDownHandler);
elViewport.addEventListener("wheel", scaleHandler);
.aplbox {
  position: relative;
  outline: 2px solid #000;
}

.aplbox-child {
  position: relative;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  touch-action: none;
  user-select: none;
}

.aplbox-child img {
  display: block;
  max-height: 100%;
  max-width: 100%;
}
<div class="aplbox">
  <div class="aplbox-child">
    <img src="https://picsum.photos/id/1/1280/720">
  </div>
</div>