How do you zoom into a specific point (no canvas)?

2019-07-30 15:27发布

问题:

The goal is simple, using a mousewheel, zoom into a specific point (where the mouse is). This means after zooming the mouse will be in the same roughly the same spot of the picture.

(Purely illustrative, I don't care if you use dolphins, ducks or madonna for the image)

I do not wish to use canvas, and so far I've tried something like this:

HTML

<img src="whatever">

JS

function zoom(e){
    var deltaScale = deltaScale || -e.deltaY / 1000;
    var newScale = scale + deltaScale;
    var newWidth = img.naturalWidth * newScale;
    var newHeight = img.naturalHeight * newScale;
    var x = e.pageX;
    var y = e.pageY;
    var newX = x * newWidth / img.width;
    var newY = y * newHeight / img.height;
    var deltaX = newX - x;
    var deltaY = newY - y;
    setScale(newScale);
    setPosDelta(-deltaX,-deltaY);
}

function setPosDelta(dX, dY) {
    var imgPos = getPosition();
    setPosition(imgPos.x + dX, imgPos.y + dY);
}

function getPosition() {
    var x = parseFloat(img.style.left);
    var y = parseFloat(img.style.top);
    return {
        x: x,
        y: y
    }
}

function setScale(n) {
    scale = n;
    img.width = img.naturalWidth * n;
    img.height = img.naturalHeight * n;
}

What this attempts to do is calculate the x,y coordinates of the dolphin's eye before and after the zoom, and after calculating the distance between those two points, substracts it from the left,top position in order to correct the zoom displacement, with no particular success.

The zoom occurs naturally extending the image to the right and to the bottom, so the correction tries to pull back to the left and to the top in order to keep the mouse on that damn dolphin eye! But it definitely doesn't.

Tell me, what's wrong with the code/math? I feel this question is not too broad, considering I couldn't find any solutions besides the canvas one.

Thanks!

[EDIT] IMPORTANT

CSS transform order matters, if you follow the selected answer, make sure you order the transition first, and then the scale. CSS transforms are executed backwards (right to left) so the scaling would be processed first, and then the translation.

回答1:

Here is an implementation of zooming to a point. The code uses the CSS 2D transform and includes panning the image on a click and drag. This is easy because of no change in scale.

The trick when zooming is to normalize the offset amount using the current scale (in other words: divide it by the current scale) first, then apply the new scale to that normalized offset. This keeps the cursor exactly where it is independent of scale.

var scale = 1,
    panning = false,
    xoff = 0,
    yoff = 0,
    start = {x: 0, y: 0},
    doc = document.getElementById("document");

function setTransform() {
  doc.style.transform = "translate(" + xoff + "px, " + yoff + "px) scale(" + scale + ")";
}

doc.onmousedown = function(e) {
  e.preventDefault();
  start = {x: e.clientX - xoff, y: e.clientY - yoff};    
  panning = true;
}

doc.onmouseup = function(e) {
  panning = false;
}

doc.onmousemove = function(e) {
  e.preventDefault();         
  if (!panning) {
    return;
  }
  xoff = (e.clientX - start.x);
  yoff = (e.clientY - start.y);
  setTransform();
}

doc.onwheel = function(e) {
    e.preventDefault();
    // take the scale into account with the offset
    var xs = (e.clientX - xoff) / scale,
        ys = (e.clientY - yoff) / scale,
        delta = (e.wheelDelta ? e.wheelDelta : -e.deltaY);

    // get scroll direction & set zoom level
    (delta > 0) ? (scale *= 1.2) : (scale /= 1.2);

    // reverse the offset amount with the new scale
    xoff = e.clientX - xs * scale;
    yoff = e.clientY - ys * scale;

    setTransform();          
}
html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

#document {
  width: 100%;
  height: 100%;
  transform-origin: 0px 0px;
  transform: scale(1) translate(0px, 0px);
}
<div id="document">
  <img style="width: 100%"
       src="http://lorempixel.com/output/animals-q-c-1920-1920-9.jpg" />
</div>
    



回答2:

This is an implementation that is closer to your original idea using top and left offsets and modifying the width attribute of the image instead of using the css transform in my other answer.

var scale = 1.0,
  img = document.getElementById("image"),
  deltaX = 0,
  deltaY = 0;

// set the initial scale once the image is loaded
img.onload = function() {
  scale = image.offsetWidth / image.naturalWidth;
}

img.onwheel = function(e) {
  e.preventDefault();

  // first, remove the scale so we have the native offset
  var xoff = (e.clientX - deltaX) / scale,
    yoff = (e.clientY - deltaY) / scale,
    delta = (e.wheelDelta ? e.wheelDelta : -e.deltaY);

  // get scroll direction & set zoom level
  (delta > 0) ? (scale *= 1.05) : (scale /= 1.05);

  // limit the smallest size so the image does not disappear
  if (img.naturalWidth * scale < 16) {
    scale = 16 / img.naturalWidth;
  }

  // apply the new scale to the native offset
  deltaX = e.clientX - xoff * scale;
  deltaY = e.clientY - yoff * scale;

  // now modify the attributes of the image to reflect the changes
  img.style.top = deltaY + "px";
  img.style.left = deltaX + "px";

  img.style.width = (img.naturalWidth * scale) + "px";
}

window.onresize = function(e) {
  document.getElementById("wrapper").style.width = window.innerWidth + "px";
  document.getElementById("wrapper").style.height = window.innerHeight + "px";
}

window.onload = function(e) {
  document.getElementById("wrapper").style.width = window.innerWidth + "px";
  document.getElementById("wrapper").style.height = window.innerHeight + "px";
}
html,
body {
  height: 100%;
  width: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;
}

div {
  overflow: hidden;
}
<div id="wrapper" style="position:relative;">
  <img id="image" style="width:100%;position:absolute;top:0px;left:0px;" src="http://lorempixel.com/output/animals-q-c-1920-1920-9.jpg" alt="">
</div>



回答3:

I liked the both posts from fmacdee. I factored the code he created out to be a reusable version that can be called on any image.

just call:

var imageScaler = new ImageScaler(document.getElementById("image")); 
imageScaler.setup();

and include this code somewhere in your project:

var ImageScaler = function(img)
{ 
    this.img = img; 
    this.scale = this.getImageScale(); 
    this.panning = false; 

    this.start = {x: 0, y: 0};
    this.delta = {x: 0, y: 0}; 
}; 

ImageScaler.prototype = 
{ 
    constructor: ImageScaler, 

    setup: function()
    { 
        this.setupEvents(); 
    }, 

    setupEvents: function()
    { 
        var img = this.img; 
        var callBack = this.onScale.bind(this); 
        var touchDown = this.touchDown.bind(this), 
        touhcMove = this.touchMove.bind(this), 
        touchUp = this.touchUp.bind(this); 

        img.onwheel = callBack; 
        img.onmousedown = touchDown;
        img.onmousemove = touhcMove;
        img.onmouseup = touchUp;
    }, 

    getImageScale: function()
    { 
        var img = this.img; 
        return img.offsetWidth / img.naturalWidth;
    }, 

    getMouseDirection: function(e)
    { 
        return (e.wheelDelta ? e.wheelDelta : -e.deltaY); 
    }, 

    getOffset: function(e)
    { 
        var scale = this.scale, 
        delta = this.delta; 
        // first, remove the scale so we have the native offset
        return {
            x: (e.clientX - delta.x) / scale, 
            y: (e.clientY - delta.y) / scale
        }; 
    }, 

    scaleElement: function(x, y, scale)
    { 
        var img = this.img; 
        img.style.top = y + "px";
        img.style.left = x + "px";
        img.style.width = (img.naturalWidth * scale) + "px";
    }, 

    minScale: 0.2, 

    updateScale: function(delta)
    { 
        // get scroll direction & set zoom level
        var scale = (delta > 0) ? (this.scale *= 1.05) : (this.scale /= 1.05);

        // limit the smallest size so the image does not disappear
        if (scale <= this.minScale) 
        {
            this.scale = this.minScale;
        }
        return this.scale; 
    }, 

    touchDown: function(e)
    { 
        var delta = this.delta; 
        this.start = {x: e.clientX - delta.x, y: e.clientY - delta.y};
        this.panning = true; 
    }, 

    touchMove: function(e)
    { 
        e.preventDefault();         
        if (this.panning === false) 
        {
            return;
        } 

        var delta = this.delta, 
        start = this.start; 
        delta.x = (e.clientX - start.x);
        delta.y = (e.clientY - start.y); 
        console.log(delta, start)

        this.scaleElement(delta.x, delta.y, this.scale);
    }, 

    touchUp: function(e)
    { 
        this.panning = false; 
    }, 

    onScale: function(e)
    { 
        var offset = this.getOffset(e); 
        e.preventDefault(); 

        // get scroll direction & set zoom level
        var delta = this.getMouseDirection(e);
        var scale = this.updateScale(delta); 

        // apply the new scale to the native offset
        delta = this.delta; 
        delta.x = e.clientX - offset.x * scale;
        delta.y = e.clientY - offset.y * scale;

        this.scaleElement(delta.x, delta.y, scale); 
    }
};

I made a fiddle to view the results: http://jsfiddle.net/acqo5n8s/12/