放大鼠标滚轮指向的点(使用缩放和平移)

14
这个问题与以下问题类似:在点上缩放(使用比例和平移),甚至是这样一个:图像以鼠标位置为中心进行缩放,但我不想在画布上做,而是在普通的图像(或者更确切地说是图像的容器div)上做。所以缩放应该像Google地图一样。实际上,我正在修改/增强iDangerous Swiper zoom (http://idangero.us/swiper/),这是我的起点,这就是我目前得到的结果:https://jsfiddle.net/xta2ccdt/3/ 只使用鼠标滚轮缩放。第一次缩放时它可以完美地缩放,但我无法计算出第一次缩放后每次缩放的比例。下面是我的代码: 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%;
}

我还发现,如果我从当前值中减去先前的translateX和translateY值,我可以在同一点上任意缩放,并且它会完美地缩放,但是如果我在一个点上缩放,然后改变鼠标位置并再次缩放,它将不再按照预期缩放。例如:https://jsfiddle.net/xta2ccdt/4/ 如果我改变鼠标位置,并计算旧鼠标位置和新鼠标位置之间的X和Y差异,并将其添加到差异计算中,第二次它将正确缩放。但第三次看起来仍然会从总计算中减去该差异,这将导致平移再次将图像移开,之后如果我们将鼠标保持在相同的位置,它将再次正确缩放。 所以我想每次计算新的“diff”时都加上旧鼠标位置和新鼠标位置之间的差异,这种方法有点行得通,当我停止添加鼠标位置差异时就不再跳动了,但它仍然不能在同一位置缩放,每次新的缩放都会使图像移动(偏移)一小部分。我认为这是因为每次都有一个新的缩放值,但偏移不是线性的,每次都越来越小,接近于零,我无法弥补这种偏移。 这是新示例:https://jsfiddle.net/xta2ccdt/5/ 示例中的新图像:旧图像不再可用:https://jsfiddle.net/xta2ccdt/14/

不相关,但是<img>没有结束标签。它应该自我关闭<img src="http://via.placeholder.com/350x150" />我还不得不使用类似占位符的东西,因为另一张图片被阻止了。由于某种原因,我无法缩小视图。 - Daniel Gale
我找到了一个插件,看起来可以做你想要的事情:http://www.jqueryscript.net/demo/jQuery-Plugin-for-Image-Zoom-In-Out-With-Mousewheel-imgViewer/ - Isma
顺便说一句,我最终放弃了这个,改用panzoom:https://github.com/timmywil/jquery.panzoom - dari0h
5个回答

23

你已经很接近正确的方法了,不过最好将 x、y 和比例尺分别存储,并根据这些值计算转换。这样做会更加容易,并且可以节省资源(无需一遍又一遍地查找 DOM 属性)。

我将代码放入了一个好的模块中:

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+')')
    }
}

通过调用使用它

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

参数包括:

  1. container:需要缩放的元素的容器。脚本将查找容器的第一个子元素并对其应用变换。
  2. max_scale:最大缩放比例(4 = 400% 缩放)
  3. factor:缩放速度(1 = 每次鼠标滚轮滚动时 +100% 缩放)

JSFiddle 连接在此处


这似乎有效。为什么要将 transform-origin 设置为 0,0?我希望保留它的默认值50%。50%。 - dari0h
逻辑基于变换原点位于x:0,y:0。但是你当然可以修改它。 - Manuel Otto
我在尝试将这个修改为使用中心变换原点时遇到了问题,应该修改什么? - Jsilvermist
@Jsilvermist,您需要将宽度的一半添加到x和y坐标中。例如pos.x += size.w*scale/2 - Manuel Otto
嗯,我觉得我有点傻,似乎无法让它正常工作,你能给我一个稍微详细一些的例子吗?另外,首先感谢您分享这个很棒的答案! - Jsilvermist
2
@Jsilvermist 当然没问题,伙计。https://jsfiddle.net/f9kctwby/ 。公平地说,实际上应该是 size.w*(scale-1)/2,我的错。 - Manuel Otto

2

感谢您提供的所有答案,对我帮助很大。 我已经进行了修改,以允许平移。

$(document).ready(function (){
    var scroll_zoom = new ScrollZoom($('#container'),5,0.5)
})

//The parameters are:
//
//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.
//max_scale: The maximum scale (4 = 400% zoom)
//factor: The zoom-speed (1 = +100% zoom per mouse wheel tick)

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 scale = 1
    var zoom_target = {x:0,y:0}
    var zoom_point = {x:0,y:0}
    var curr_tranform = target.css('transition')
    var last_mouse_position = { x:0, y:0 }
    var drag_started = 0

    target.css('transform-origin','0 0')
    target.on("mousewheel DOMMouseScroll",scrolled)
    target.on('mousemove', moved)
    target.on('mousedown', function() {
        drag_started = 1;
        target.css({'cursor':'move', 'transition': 'transform 0s'});
        /* Save mouse position */
        last_mouse_position = { x: event.pageX, y: event.pageY};
    });

    target.on('mouseup mouseout', function() {
        drag_started = 0;
        target.css({'cursor':'default', 'transition': curr_tranform});
    });

    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

        update()
    }

    function moved(event){
        if(drag_started == 1) {
            var current_mouse_position = { x: event.pageX, y: event.pageY};
            var change_x = current_mouse_position.x - last_mouse_position.x;
            var change_y = current_mouse_position.y - last_mouse_position.y;

            /* Save mouse position */
            last_mouse_position = current_mouse_position;
            //Add the position change
            pos.x += change_x;
            pos.y += change_y;

        update()
        }
    }

    function update(){
        // 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)

        target.css('transform','translate('+(pos.x)+'px,'+(pos.y)+'px) scale('+scale+','+scale+')')
    }
}
#container{
  width:500px;
  height:500px;
  overflow:hidden;
}
#slide{
  width:100%;
  height:100%;
  transition: transform .3s;
}
img{
  width:auto;
  height:auto;
  max-width:100%;
  pointer-events: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="container">
  <div id="slide">
    <img src="https://iso.500px.com/wp-content/uploads/2014/07/big-one.jpg">
  </div>
</div>

在这里测试一下,看看是否符合您的需求: https://jsfiddle.net/Pimigo/sy9c8z5q/8/


2

我认为这会让你更接近你想要实现的目标。

重点更改

  1. 我将比例尺从回调函数中提取出来;我认为你不想在每个滚轮事件中重置比例尺。
  2. 尝试将 transform-origin 设置为以鼠标为中心,而不是手动计算平移(除非你想保持居中,这是默认设置)。

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>


1
我仍然遇到的问题是,如果你在左上角缩放图像,然后在右下角缩放图像,图像会跳动。 - dari0h

1
使用translate3dperspective来处理三维变换,而不是使用scale,这样如何?此外,将缩放与平移解耦使其更简单。

$(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>


我被卡在了缩放上,因为我试图增强iDangerous swiper缩放功能,而他们使用了scale。 - dari0h

0
我使用的代码可以在鼠标位置放大。它不使用 transform / translate3d,而是重新调整图像在 DIV 中的位置并调整其 heightwidth

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>


我被卡住了,因为我试图增强iDangerous swiper缩放功能,而他们使用了translate。 - dari0h

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接