22

This is a followup question to How to zoom to mouse pointer while using my own mousewheel smoothscroll?

I am using css transforms to zoom an image to the mouse pointer. I am also using my own smooth scroll algorithm to interpolate and provide momentum to the mousewheel.

With Bali Balo's help in my previous question I have managed to get 90% of the way there.

You can now zoom the image all the way in to the mouse pointer while still having smooth scrolling as the following JSFiddle illustrates:

http://jsfiddle.net/qGGwx/7/

However, the functionality is broken when the mouse pointer is moved.

To further clarify, If I zoom in one notch on the mousewheel the image is zoomed around the correct position. This behavior continues for every notch I zoom in on the mousewheel, completely as intended. If however, after zooming part way in, I move the mouse to a different position, the functionality breaks and I have to zoom out completely in order to change the zoom position.

The intended behavior is for any changes in mouse position during the zooming process to be correctly reflected in the zoomed image.

The two main functions that control the current behavior are as follows:

self.container.on('mousewheel', function (e, delta) {
        var offset = self.image.offset();

        self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
        self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;

        if (!self.running) {
            self.running = true;
            self.animateLoop();
        }
        self.delta = delta
        self.smoothWheel(delta);
        return false;
    });

This function collects the current position of the mouse at the current scale of the zoomed image.

It then starts my smooth scroll algorithm which results in the next function being called for every interpolation:

zoom: function (scale) {

    var self = this;
    self.currentLocation.x += ((self.mouseLocation.x - self.currentLocation.x) / self.currentscale);
    self.currentLocation.y += ((self.mouseLocation.y - self.currentLocation.y) / self.currentscale);

    var compat = ['-moz-', '-webkit-', '-o-', '-ms-', ''];
    var newCss = {};
    for (var i = compat.length - 1; i; i--) {
        newCss[compat[i] + 'transform'] = 'scale(' + scale + ')';
        newCss[compat[i] + 'transform-origin'] = self.currentLocation.x + 'px ' + self.currentLocation.y + 'px';
    }
    self.image.css(newCss);


    self.currentscale = scale;
},

This function takes the scale amount (1-10) and applies the css transforms, repositioning the image using transform-origin.

Although this works perfectly for a stationary mouse position chosen when the image is completely zoomed out; as stated above it breaks when the mouse cursor is moved after a partial zoom.

Huge thanks in advance to anyone who can help.

Community
  • 1
  • 1
gordyr
  • 6,078
  • 14
  • 65
  • 123
  • the demo seems to work fine according to your description. please elaborate on the exact problem. what do you mean by "correctly reflected"? – Eliran Malka Jan 23 '13 at 10:15
  • The zoom scale is set to from 1-10. Try zooming in only part way, say to around 3 via the mousewheel. Then move the mouse to a different pixel and continue zooming the rest of the way. You will see that it does not zoom to the NEW mouse position correctly. The zoom origin only maintains a correct position when the mouse pointer is kept stationary for the duration of the whole zoom, it should correctly change position when the mouse is moved. – gordyr Jan 23 '13 at 10:18
  • ok i get it, so a "correct" behavior will be zooming to the new pointed location (extracted from the current scale level) rather than zooming to the relative location in the image in scale 1. am i there yet? – Eliran Malka Jan 23 '13 at 10:23
  • Exactly... My apologies for not making this clear. – gordyr Jan 23 '13 at 10:32

5 Answers5

8

Actually, not too complicated. You just need to separate the mouse location updating logic from the zoom updating logic. Check out my fiddle: http://jsfiddle.net/qGGwx/41/

All I have done here is add a 'mousemove' listener on the container, and put the self.mouseLocation updating logic in there. Since it is no longer required, I also took out the mouseLocation updating logic from the 'mousewheel' handler. The animation code stays the same, as does the decision of when to start/stop the animation loop.

here's the code:

    self.container.on('mousewheel', function (e, delta) {
        if (!self.running) {
            self.running = true;
            self.animateLoop();
        }
        self.delta = delta
        self.smoothWheel(delta);
        return false;
    });

    self.container.on('mousemove', function (e) {
        var offset = self.image.offset();

        self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
        self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
    });
jordancpaul
  • 2,954
  • 1
  • 18
  • 27
  • Thanks jordancpaul, this is very close. However, although the new zoom position is correct now, the whole thing seems to move away from the mouse pointer, meaning the the zoom isn't matching where the mouse is exactly. Sorry this is a little hard to articulate. You have provided the closest answer so far but i'm going to leave it open a little longer in case someone can get it perfect, if not I will award you the answer. Regardless great work, thank you! – gordyr Jan 27 '13 at 14:19
5

Before you check this fiddle out; I should mention:

First of all, within your .zoom() method; you shouldn't divide by currentscale:

self.currentLocation.x += ((self.mouseLocation.x - self.currentLocation.x) / self.currentscale);
self.currentLocation.y += ((self.mouseLocation.y - self.currentLocation.y) / self.currentscale);

because; you already use that factor when calculating the mouseLocation inside the initmousewheel() method like this:

self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;

So instead; (in the .zoom() method), you should:

self.currentLocation.x += (self.mouseLocation.x - self.currentLocation.x);
self.currentLocation.y += (self.mouseLocation.y - self.currentLocation.y);

But (for example) a += b - a will always produce b so the code above equals to:

self.currentLocation.x = self.mouseLocation.x;
self.currentLocation.y = self.mouseLocation.y;

in short:

self.currentLocation = self.mouseLocation;

Then, it seems you don't even need self.currentLocation. (2 variables for the same value). So why not use mouseLocation variable in the line where you set the transform-origin instead and get rid of currentLocation variable?

newCss[compat[i] + 'transform-origin'] = self.mouseLocation.x + 'px ' + self.mouseLocation.y + 'px';

Secondly, you should include a mousemove event listener within the initmousewheel() method (just like other devs here suggest) but it should update the transform continuously, not just when the user wheels. Otherwise the tip of the pointer will never catch up while you're zooming out on "any" random point.

self.container.on('mousemove', function (e) {
    var offset = self.image.offset();
    self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
    self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
    self.zoom(self.currentscale);
});

So; you wouldn't need to calculate this anymore within the mousewheel event handler so, your initmousewheel() method would look like this:

initmousewheel: function () {
    var self = this;

    self.container.on('mousewheel', function (e, delta) {
        if (!self.running) {
            self.running = true;
            self.animateLoop();
        }
        self.delta = delta;
        self.smoothWheel(delta);
        return false;
    });

    self.container.on('mousemove', function (e) {
        var offset = self.image.offset();
        self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
        self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
        self.zoom(self.currentscale); // <--- update transform origin dynamically
    });
}

One Issue:

This solution works as expected but with a small issue. When the user moves the mouse in regular or fast speed; the mousemove event seems to miss the final position (tested in Chrome). So the zooming will be a little off the pointer location. Otherwise, when you move the mouse slowly, it gets the exact point. It should be easy to workaround this though.

Other Notes and Suggestions:

  • You have a duplicate property (prevscale).
  • I suggest you always use JSLint or JSHint (which is available on jsFiddle too) to validate your code.
  • I highly suggest you to use closures (often refered to as Immediately Invoked Function Expression (IIFE)) to avoid the global scope when possible; and hide your internal/private properties and methods.
Onur Yıldırım
  • 32,327
  • 12
  • 84
  • 98
  • 1
    Fantastic!!! A superbly written answer well deserving of the bounty. Your comments regarding the code itself are totally correct and I appreciate them, though the jsFiddle itself is not the actual code I am using in the app, it was a quick simplified mock up I wrote for demonstration purposes and as such there was several oversights in the translation. Regardless I really should get into the habit of using the provided JSLint within jSFiddle. Again, however, I genuinely cannot thank you enough. :) – gordyr Feb 12 '13 at 12:04
  • No problem at all. Happy to help. :) – Onur Yıldırım Feb 12 '13 at 12:26
4

Add a mousemover method and call it in the init method:

mousemover: function() {
    var self = this;
    self.container.on('mousemove', function (e) {

        var offset = self.image.offset();

        self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
        self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;

        self.zoom(self.currentscale);
    });
},

Fiddle: http://jsfiddle.net/powtac/qGGwx/34/

powtac
  • 40,542
  • 28
  • 115
  • 170
  • Thanks powtac... This doesn't seem to work i'm afraid and neither does the Fiddle. The behaviour remains the same. However, tracking the mouse position changes inside the mousemove event rather than within the mousewheel event seems like it might be interesting route to take. Cheers. – gordyr Jan 23 '13 at 12:02
  • Ahh, you may have misunderstood the question, my apologies. The image shouldn't be moving when the mouse is moved. it should only be changing/zooming with the mousewheel.... Remaining stationary while the ouse position is changed, then resume zooming from the NEW mouse position if the user continues to zoom further in. Hope that clears it up... Thanks for trying though! – gordyr Jan 23 '13 at 12:06
3

Zoom point is not exactly right because of scaling of an image (0.9 in ratio). In fact mouse are pointing in particular point in container but we scale image. See this fiddle http://jsfiddle.net/qGGwx/99/ I add marker with position equal to transform-origin. As you can see if image size is equal to container size there is no issue. You need this scaling? Maybe you can add second container? In fiddle I also added condition in mousemove

if(self.running && self.currentscale>1 && self.currentscale != self.lastscale) return;

That is preventing from moving image during zooming but also create an issue. You can't change zooming point if zoom is still running.

abc667
  • 514
  • 4
  • 19
  • Thanks abc667. Unfortunately yes, the scaling is needed as the image size will never be known in advance and is scaled to fill the screen via css. Is there any way around this? – gordyr Feb 08 '13 at 16:49
  • I tested this a lot and I think in fact, there is no issue. In this fiddle http://jsfiddle.net/qGGwx/102/ I add second picture and set position of the green marker to transform-origin and it is ok evry time. If you zoom from `scale=1` to some point this point is not centered. The same thing is happening if you change zooming point and it looks like error but it is not. If you want to make it look "right" you have to center zooming point based on distance from center. But then if you zoom to some corner you get 3/4 of container black. – abc667 Feb 10 '13 at 13:14
3

Extending @jordancpaul's answer I have added a constant mouse_coord_weight which gets multiplied to delta of the mouse coordinates. This is aimed at making the zoom transition less responsive to the change in mouse coordinates. Check it out http://jsfiddle.net/7dWrw/

I have rewritten the onmousemove event hander as:

    self.container.on('mousemove', function (e) {
        var offset = self.image.offset();
        console.log(offset);

        var x = (e.pageX - offset.left) / self.currentscale,
            y = (e.pageY - offset.top) / self.currentscale;

        if(self.running) {
            self.mouseLocation.x += (x - self.mouseLocation.x) * self.mouse_coord_weight;
            self.mouseLocation.y += (y - self.mouseLocation.y) * self.mouse_coord_weight;
        } else {
            self.mouseLocation.x = x;
            self.mouseLocation.y = y;
        }

    });
Birla
  • 1,170
  • 11
  • 30