Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Zooming on a point with CSS3 transform scale

Even if the following code snippet seems short, I struggled during days (shame on me!) to find a way to zoom on the point that is clicked using only CSS3 transform. It works now:

    var current = {x: 0, y: 0, zoom: 1}, c = document.getElementById('container');
    window.onclick = function(e) {
      wx = current.x + e.clientX / current.zoom;
      wy = current.y + e.clientY / current.zoom;
      var coef = e.ctrlKey ? 0.5 : 2;
      current.zoom *= coef;    
      current.x = wx - e.clientX / current.zoom; 
      current.y = wy - e.clientY / current.zoom; 
      c.style.transform = 'scale(' + current.zoom +') translate(' + (-current.x) + 'px,' + (-current.y) + 'px)';
    };
    html, body { margin: 0; padding: 0; overflow: hidden; min-height: 100%; }
    #container { position: absolute; transform-origin: 0 0; transition-duration: 3s;}
    #item { position: absolute; left:0px; top:0px; }
  <div id="container"><div id="item"><img src="http://fadili.users.greyc.fr/demos/WaveRestore/EMInpaint/parrot_original.png"></img></div></div>

The only problem is that the transition is weird, like if it first translates and then zooms ; it produces a weird zigzag effet. How to have a smooth CSS3 transition in this case?

See animated GIF here of the weird transition effect: http://gget.it/zf3fmwum/weirdtransition.gif

Note: the point which is clicked on is a fixed point of the scaling transform (example: click on the eye, the image is zoomed, and the cursor is still on the eye), like in GoogleMaps-doubleclick-zooming.

like image 632
Basj Avatar asked Dec 22 '14 13:12

Basj


People also ask

How do I zoom the same element size?

That is, using control-plus to increase text size and control-minus to reduce it.

How do I stop a CSS layout from distorting when zooming in out?

To fix the problem with zooming in, try adding the min-width attribute to your outer countainer ( #container or #navbar ?). Setting min-width prevents the webpage from trying to shrink down beyond the specified width (i.e. 300px).

How do you zoom in and out on CSS?

The non-standard zoom CSS property can be used to control the magnification level of an element. transform: scale() should be used instead of this property, if possible. However, unlike CSS Transforms, zoom affects the layout size of the element.


1 Answers

One thing to watch out for when using transforms is the order that you apply them. You'll find your example works rather differently if you switch the scale and the translate around.

Here is an interesting article on the matter:

https://staff.washington.edu/fmf/2011/07/15/css3-transform-attribute-order/

I wasn't able to repair your version, mainly because it misbehaves unexpectedly when you switch the order of the transforms. Basically it seems you are running into odd behaviour because the scale itself causes an automatic translation in position, and then you also translate... and it seems these different translations are occurring at a slightly different pace.

I did however re-implement a version that works, and allows you to translate before scaling. Keeping the transforms in this order seems to avoid the issue.

http://jsfiddle.net/fxpc5rao/32/

I've modified the version below to use translate3D just because it performs better for many systems.

var current = {x: 0, y: 0, zoom: 1},
    con = document.getElementById('container');
    
window.onclick = function(e) {
    var coef = e.shiftKey || e.ctrlKey ? 0.5 : 2,
        oz = current.zoom,
        nz = current.zoom * coef,
        /// offset of container
        ox = 20,
        oy = 20,
        /// mouse cords
        mx = e.clientX - ox,
        my = e.clientY - oy,
        /// calculate click at current zoom
        ix = (mx - current.x) / oz,
        iy = (my - current.y) / oz,
        /// calculate click at new zoom
        nx = ix * nz,
        ny = iy * nz,
        /// move to the difference
        /// make sure we take mouse pointer offset into account!
        cx = mx - nx,
        cy = my - ny
    ;
    // update current
    current.zoom = nz;
    current.x = cx;
    current.y = cy;
    /// make sure we translate before scale!
    con.style.transform
        = 'translate3D('+cx+'px, '+cy+'px,0) '
        + 'scale('+nz+')'
    ;
};
#container {
    position: absolute;
    left: 20px;
    top: 20px;
    width: 100%;
    height: 100%;
    transform-origin: 0 0 0;
    transition: transform 0.3s;
    transition-timing-function: ease-in-out;
    transform: translate3D(0,0,0) scale(1);
}

#item {
    position: absolute;
}
<div id="container">
    <div id="item">
        <img src="http://fadili.users.greyc.fr/demos/WaveRestore/EMInpaint/parrot_original.png" />
    </div>
</div>

update

I've updated my answer (and the snippet above) to take into account your additional requirement, you just need to modify the calculation to include the difference in mouse pointer offset.

http://jsfiddle.net/fxpc5rao/33/

Now with every click the difference between the calculated unscaled position and e.clientX, e.clientY is added. This gives you the offset you need to keep the zoomed translation occurring around the mouse pointer. The key change is here:

cx = (ix + (e.clientX - ix) - nx),
cy = (iy + (e.clientY - iy) - ny)

NOTE: Because you are relying on e.clientX and e.clientY you will find annoying offseting will occur if you move #container away from its current 0,0 coordinate. This can be done, but you will have to modify your calculations to localise the coordinates to whatever #container's location ends up being.

update 2

Good call @Basj, I wasn't aware that the transformations occurred in reverse order, I'll add the link in from your comment here:

CSS3 transform order matters: rightmost operation first

So as you say, you require the scale to occur before the translate in processing terms, but the translate to be written before the scale in the actual transform value — if that makes sense :) Still not exactly sure why doing one before the other results in the odd interpolation however.

Also, I've noticed there is a rather obvious optimisation — which I'm sure, as you are implementing this, you will have spotted — no point adding something only to subtract it later. I guess I'd just had too much festive cheer that day!

cx = e.clientX - nx,
cy = e.clientY - ny

update 3

No problem @jdavies, it is just a matter of converting your mouse coordinates so they are relative to the container's top left. How you calculate this offset will depend entirely on your project (it is much easier to get a layer's offset — cross browser — using something like jQuery.offset). However I've updated the code in this answer to take into account a hard-coded/fixed offset away from 0,0 using position absolute — just to illustrate. Here is an updated fiddle too:

http://jsfiddle.net/fxpc5rao/5/

As we are using clientX and clientY the mouse coordinates will always be calculated from the top left of the browser window, making them global to the page (disregarding scrolling). In order to localise them to the container, you just need to subtract the containers x and y position.

Container at 0,0                Container at 80,100

+------+------- screen x 0      +--------------- 0
|      |                        |      
|      |                        |  +------+
|   x  | <-- mouse click        |  |x     | <-- mouse click
+------+     at 100,120         |  |      |     at 100,120
|                               |  |      |     but relative
|                               |  +------+     20,20
|                               |               so we us 20,20

0 screen y                      0

The #container can also be contained within other elements, you just again have to take into account any positional offset these elements give to the #container. In the following fiddle there is a #page-setting element that is offsetting everything with margin, as long as the ox, oy variables are updated with the margin values everything should behave.

http://jsfiddle.net/fxpc5rao/34/

NOTE: If you place this system inside a scrollable page you will also need to add the viewport's scroll offsets to the mouse coordinates, I give an example here, but this is most likely not a full cross browser solution. You are better off looking at an established library like jQuery to calculate coordinates and offsets for you.

like image 152
Pebbl Avatar answered Oct 20 '22 01:10

Pebbl