Thursday, October 19, 2017

Zoom in on a mousewheel point (using scale and translate)

Leave a Comment

This question is similar to this one: Zoom in on a point (using scale and translate) or even this one: Image zoom centered on mouse position but I don't want to do it on a canvas but a normal image (or rather the container div of the image). So zooming should be as google maps. I am actually hacking/enhancing iDangerous Swiper zoom (http://idangero.us/swiper/), and that is my starting point, and this is is what I got so far: https://jsfiddle.net/xta2ccdt/3/

Zoom only with the mouse wheel. The first time you zoom in it zooms perfectly, but I can't figure out how to calculate every zoom after first one.

Here's my code: JS:

$(document).ready(function(){     $("#slideContainer").on("mousewheel DOMMouseScroll", function (e) {     e.preventDefault();     var delta = e.delta || e.originalEvent.wheelDelta;     var zoomOut;     if (delta === undefined) {       //we are on firefox       delta = e.originalEvent.detail;       zoomOut = delta ? delta < 0 : e.originalEvent.deltaY > 0;       zoomOut = !zoomOut;     } else {       zoomOut = delta ? delta < 0 : e.originalEvent.deltaY > 0;     }     var touchX = e.type === 'touchend' ? e.changedTouches[0].pageX : e.pageX;     var touchY = e.type === 'touchend' ? e.changedTouches[0].pageY : e.pageY;     var scale = 1, translateX, translateY;     if(zoomOut){         //we are zooming out       //not interested in this yet     }else{         //we are zooming in       scale = scale + 0.5;       var dimensionMultiplier = scale - 0.5;//when image is scaled up offsetWidth/offsetHeight doesn't take this into account so we must multiply by scale to get the correct width/height       var slideWidth = $("#slide")[0].offsetWidth * dimensionMultiplier;       var slideHeight = $("#slide")[0].offsetHeight * dimensionMultiplier;        var offsetX = $("#slide").offset().left;//distance from the left of the viewport to the slide       var offsetY = $("#slide").offset().top;//distance from the top of the viewport to the slide       var diffX = offsetX + slideWidth / 2 - touchX;//this is distance from the mouse to the center of the image       var diffY = offsetY + slideHeight / 2 - touchY;//this is distance from the mouse to the center of the image        //how much to translate by x and y so that poin on image is alway under the mouse       //we must multiply by 0.5 because the difference between previous and current scale is always 0.5       translateX = ((diffX) * (0.5));       translateY = ((diffY) * (0.5));         }     $("#slide").css("transform", 'translate3d(' + translateX + 'px, ' + translateY + 'px,0) scale(' + scale + ')').css('transition-duration', '300ms');   });   }); 

HTML:

<div id="slideContainer">   <div id="slide">     <img src="http://content.worldcarfans.co/2008/6/medium/9080606.002.1M.jpg"></img>   </div> </div> 

CSS:

#slideContainer{   width:500px;   height:500px;   overflow:hidden; } #slide{   width:100%;   height:100%; } img{   width:auto;   height:auto;   max-width:100%; } 

I also figured out if I subtract previous translateX and translateY values from the current ones, I can zoom on the same point as much as I want and it will zoom perfectly, but if I zoom on one point and then change the mouse position and zoom in again, it will no longer zoom as it's supposed to. Example: https://jsfiddle.net/xta2ccdt/4/

If I change mouse position, and calculate the X and Y difference between old and new mouse position and add that into the diff calculation it will zoom correctly the second time. But the third time looks like that difference still gets subtracted from the total calculation and this will cause the translate to move the image away again, after that if we hold the mouse in the same position it will zoom correctly again. So I figured I'll just add the difference between old and new mouse position every time I calculate the new "diff", and this kind of works, there is no longer a jump like it was when I stopped adding the mouse position difference, but it's still not zooming on the same position, with each new zoom it moves (offsets) the image by a small amount. I figure this is because there is a new zoom value each time, but the offset is not linear, it's everytime smaller approaching zero, and I can't figure out how to offset the offset. Here is the new example: https://jsfiddle.net/xta2ccdt/5/

4 Answers

Answers 1

I think this will get you a little closer to what you are trying to achieve.

Key Changes

  1. I pulled scale outside of the callback; I don't think you want to be resetting your scale on each wheel event.
  2. Instead of calculating the translation manually, try setting the transform-origin to be centered on your mouse (unless you want to keep it centered which is the default)

var scale = 1;    $(document).ready(function(){      $("#slideContainer").on("mousewheel DOMMouseScroll", function (e) {      e.preventDefault();      var delta = e.delta || e.originalEvent.wheelDelta;      var zoomOut;      if (delta === undefined) {        //we are on firefox        delta = e.originalEvent.detail;        zoomOut = delta ? delta < 0 : e.originalEvent.deltaY > 0;        zoomOut = !zoomOut;      } else {        zoomOut = delta ? delta < 0 : e.originalEvent.deltaY > 0;      }      var touchX = e.type === 'touchend' ? e.changedTouches[0].pageX : e.pageX;      var touchY = e.type === 'touchend' ? e.changedTouches[0].pageY : e.pageY;      var translateX, translateY;      if(zoomOut){        // we are zooming out        scale = scale - 0.01;                var offsetWidth = $("#slide")[0].offsetWidth;        var offsetHeight = $("#slide")[0].offsetHeight;          $("#slide")          .css("transform-origin", touchX + 'px ' + touchY + 'px')          .css("transform", 'scale(' + scale + ')');              }else{        // we are zooming in        scale = scale + 0.01;                var offsetWidth = $("#slide")[0].offsetWidth;        var offsetHeight = $("#slide")[0].offsetHeight;          $("#slide")          .css("transform-origin", touchX + 'px ' + touchY + 'px')          .css("transform", 'scale(' + scale + ')');      }          });      });
#slideContainer{    width:200px;    height:200px;    overflow:hidden;  }  #slide{    width:100%;    height:100%;  }  img{    width:auto;    height:auto;    max-width:100%;  }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>  <div id="slideContainer">    <div id="slide">      <img src="https://via.placeholder.com/200x200"></img>    </div>  </div>

Answers 2

What about using translate3d and perspective, to handle the 3d transformations, instead of using scale? Also, decoupling zoom from translation makes it simpler.

$(document).ready(function() {      var translateX = 0,      translateY = 0,      translateZ = 0,      stepZ = 10,      initial_obj_X = 0,      initial_obj_Y = 0,      initial_mouse_X = 0,      initial_mouse_Y = 0;      function apply_coords() {      $("#slide").css("transform", 'perspective(100px) translate3d(' + translateX + 'px, ' + translateY + 'px, ' + translateZ + 'px)');    }        $("#slideContainer").on("mousewheel DOMMouseScroll", function(e) {        e.preventDefault();      var delta = e.delta || e.originalEvent.wheelDelta;      var zoomOut;      if (delta === undefined) {        delta = e.originalEvent.detail;        zoomOut = delta ? delta < 0 : e.originalEvent.deltaY > 0;        zoomOut = !zoomOut;      } else {        zoomOut = delta ? delta < 0 : e.originalEvent.deltaY > 0;      }      if (zoomOut) {        translateZ = translateZ - stepZ;      } else {        translateZ = translateZ + stepZ;      }      apply_coords();      });        var is_dragging = false;    $("#slideContainer")      .mousedown(function(e) {        is_dragging = true;      })      .mousemove(function(e) {        if (is_dragging) {          e.preventDefault();          var currentX = e.type === 'touchend' ? e.changedTouches[0].pageX : e.pageX;          var currentY = e.type === 'touchend' ? e.changedTouches[0].pageY : e.pageY;          translateX = initial_obj_X + (currentX - initial_mouse_X);          translateY = initial_obj_Y + (currentY - initial_mouse_Y);          apply_coords();        } else {          initial_mouse_X = e.type === 'touchend' ? e.changedTouches[0].pageX : e.pageX;          initial_mouse_Y = e.type === 'touchend' ? e.changedTouches[0].pageY : e.pageY;          initial_obj_X = translateX;          initial_obj_Y = translateY;        }      })      .mouseup(function() {        is_dragging = false;      });      });
#slideContainer {    width: 200px;    height: 200px;    overflow: hidden;    position: relative;  }    #slide {    width: 100%;    height: 100%;    background: red;  }    img {    width: auto;    height: auto;    max-width: 100%;  }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>  <div id="slideContainer">    <div id="slide">    </div>  </div>

Answers 3

The code I use to zoom in at the mouse position is below. It does not use transform / translate3d but readjusts the position of the image within the div and adjusts its height and width.

var zoom = 1;  var img, div;  window.onload = function() {    window.addEventListener('DOMMouseScroll', wheel, false)    img = document.getElementById("img");    div = document.getElementById("div");  }    function wheel(event) {    event.preventDefault();    var delta = 0;    if (!event) /* For IE. */      event = window.event;    if (event.wheelDelta) { /* IE/Opera. */      delta = event.wheelDelta / 120;    } else if (event.detail) { /** Mozilla case. */      /** In Mozilla, sign of delta is different than in IE.       * Also, delta is multiple of 3.       */      delta = -event.detail / 3;    }    /** If delta is nonzero, handle it.     * Positive Delta = wheel scrolled up,     * Negative Delte = wheel scrolled down.     */    if (delta) {      // will pass 1 to zoom in and -1 to zoom out	          delta = delta / Math.abs(delta)      zoomImage(delta == 1, event);    }  }    function zoomImage(zoomIn, e) {    var oldZoom = zoom;    var direction = 1 * (zoomIn ? 1 : -1);    zoom += direction * .2;    // range = 50% => 600%    zoom = round(Math.min(6, Math.max(.5, zoom)), 1);        if (zoom == 1) {      // For a zoom = 1, we reset      resetZoom(div, img);      return;    }      // make the position of the mouse the center,     // or as close as can with keeping maximum image viewable    // e == div[this.slide]    // gets the top and left of the div    var divOffset = getOffset(div);    var imgStyles = getComputedStyle(img);    var divStyles = getComputedStyle(div);    var imgOffset = {      x: parseInt(imgStyles.left),      y: parseInt(imgStyles.top)    };      // where clicked relative in div    var yTravel = e.pageY - divOffset.y;    var xTravel = e.pageX - divOffset.x;      // where clicked    var xOldImg = -imgOffset.x + xTravel;    var yOldImg = -imgOffset.y + yTravel;      // the clicked position relative to the image 0,0    // clicked position will remain at the cursor position while image zoom changes      // calc the same position at the new zoom level    var ratio = zoom / oldZoom;    var xNewImg = xOldImg * ratio;    var yNewImg = yOldImg * ratio;      // calc new top / left    var xStart = -(xNewImg - xTravel);    var yStart = -(yNewImg - yTravel);      img.style.height = parseInt(divStyles.height) * (zoom) + "px";    img.style.width = parseInt(divStyles.width) * (zoom) + "px";      img.style.top = yStart + "px";    img.style.left = xStart + "px";    img.style.cursor = "grab";  }    function resetZoom(div, img) {    img.style.top = "0px";    img.style.left = "0px";    img.style.height = div.style.height;    img.style.width = div.style.width;    img.style.cursor = "default";    zoom = 1;  }    function getOffset(element) {    var rect = element.getBoundingClientRect();    var posX = rect.left + window.pageXOffset; // alias for window.scrollX;     var posY = rect.top + window.pageYOffset; // alias for window.scrollY;	      return {      x: posX,      y: posY,      left: posX,      top: posY,      width: rect.width,      height: rect.height    };  }    function round(number, precision) {    precision = precision ? +precision : 0;      var sNumber = number + '',      periodIndex = sNumber.indexOf('.'),      factor = Math.pow(10, precision);      if (periodIndex === -1 || precision < 0) {      return Math.round(number * factor) / factor;    }      number = +number;      // sNumber[periodIndex + precision + 1] is the last digit    if (sNumber[periodIndex + precision + 1] >= 5) {      // Correcting float error      // factor * 10 to use one decimal place beyond the precision      number += (number < 0 ? -1 : 1) / (factor * 10);    }      return +number.toFixed(precision);  }
#div {    width: 350px;    height: 262px;    border: 1px solid black;    overflow: hidden;  }    #img {    width: 350px;    height: 262px;    position: relative;  }
<div id='div'>    <img id='img' src="https://www.design.mseifert.com/git-slideshow/img-demo/images01.jpg">  </div>

Answers 4

You were close to it, however it's better to store the x,y and scale separately and calculate the transforms based on those values. It makes things alot easier + saves resources (no need to lookup the dom properties over and over),

I've put the code into a nice module:

function ScrollZoom(container,max_scale,factor){     var target = container.children().first()     var size = {w:target.width(),h:target.height()}     var pos = {x:0,y:0}     var zoom_target = {x:0,y:0}     var zoom_point = {x:0,y:0}     var scale = 1     target.css('transform-origin','0 0')     target.on("mousewheel DOMMouseScroll",scrolled)      function scrolled(e){         var offset = container.offset()         zoom_point.x = e.pageX - offset.left         zoom_point.y = e.pageY - offset.top          e.preventDefault();         var delta = e.delta || e.originalEvent.wheelDelta;         if (delta === undefined) {           //we are on firefox           delta = e.originalEvent.detail;         }         delta = Math.max(-1,Math.min(1,delta)) // cap the delta to [-1,1] for cross browser consistency          // determine the point on where the slide is zoomed in         zoom_target.x = (zoom_point.x - pos.x)/scale         zoom_target.y = (zoom_point.y - pos.y)/scale          // apply zoom         scale += delta*factor * scale         scale = Math.max(1,Math.min(max_scale,scale))          // calculate x and y based on zoom         pos.x = -zoom_target.x * scale + zoom_point.x         pos.y = -zoom_target.y * scale + zoom_point.y           // Make sure the slide stays in its container area when zooming out         if(pos.x>0)             pos.x = 0         if(pos.x+size.w*scale<size.w)             pos.x = -size.w*(scale-1)         if(pos.y>0)             pos.y = 0          if(pos.y+size.h*scale<size.h)             pos.y = -size.h*(scale-1)          update()     }      function update(){         target.css('transform','translate('+(pos.x)+'px,'+(pos.y)+'px) scale('+scale+','+scale+')')     } } 

Use it by calling

new ScrollZoom($('#container'),4,0.5) 

The parameters are:

  1. container: The wrapper of the element to be zoomed. The script will look for the first child of the container and apply the transforms to it.
  2. max_scale: The maximum scale (4 = 400% zoom)
  3. factor: The zoom-speed (1 = +100% zoom per mouse wheel tick)

JSFiddle here

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment