Pinch to zoom with CSS3

I'm trying to implement pinch-to-zoom gestures exactly as in Google Maps. I watched a talk by Stephen Woods - "Creating Responsive HTML5 Touch Interfaces” - about the issue and used the technique mentioned. The idea is to set the transform origin of the target element at (0, 0) and scale at the point of the transform. Then translate the image to keep it centered at the point of transform.

In my test code scaling works fine. The image zooms in and out fine between subsequent translations. The problem is I am not calculating the translation values properly. I am using jQuery and Hammer.js for touch events. How can I adjust my calculation in the transform callback so that the image stays centered at the point of transform?

The CoffeeScript (#test-resize is a div with a background image)

image = $('#test-resize')

hammer = image.hammer ->
  prevent_default: true
  scale_treshold: 0

width = image.width()
height = image.height()
toX = 0
toY = 0
translateX = 0
translateY = 0
prevScale = 1
scale = 1

hammer.bind 'transformstart', (event) ->

  toX = (event.touches[0].x + event.touches[0].x) / 2
  toY = (event.touches[1].y + event.touches[1].y) / 2

hammer.bind 'transform', (event) ->

  scale = prevScale * event.scale
  shiftX = toX * ((image.width() * scale) - width) / (image.width() * scale)
  shiftY = toY * ((image.height() * scale) - height) / (image.height() * scale)
  width = image.width() * scale
  height = image.height() * scale

  translateX -= shiftX
  translateY -= shiftY

  css = 'translateX(' + @translateX + 'px) translateY(' + @translateY + 'px) scale(' + scale + ')'
  image.css '-webkit-transform', css
  image.css '-webkit-transform-origin', '0 0'

hammer.bind 'transformend', () ->
  prevScale = scale


I managed to get it working.

jsFiddle demo

In the jsFiddle demo, clicking on the image represents a pinch gesture centred at the click point. Subsequent clicks increase the scale factor by a constant amount. To make this useful, you would want to make the scale and translate computations much more often on a transform event (hammer.js provides one).

The key to getting it to work was to correctly compute the point of scale coordinates relative to the image. I used event.clientX/Y to get the screen coordinates. The following lines convert from screen to image coordinates:

x -= offset.left + newX
y -= + newY

Then we compute a new size for the image and find the distances to translate by. The translation equation is taken from Stephen Woods' talk.

newWidth = image.width() * scale
newHeight = image.height() * scale

newX += -x * (newWidth - image.width) / newWidth
newY += -y * (newHeight - image.height) / newHeight

Finally, we scale and translate

image.css '-webkit-transform', "scale3d(#{scale}, #{scale}, 1)"         
wrap.css '-webkit-transform', "translate3d(#{newX}px, #{newY}px, 0)"

We do all our translations on a wrapper element to ensure that the translate-origin stays at the top left of our image.

I successfully used that snippet to resize images on phonegap, using hammer and jquery.

If it interests someone, i translated this to JS.

function attachPinch(wrapperID,imgID)
    var image = $(imgID);
    var wrap = $(wrapperID);

    var  width = image.width();
    var  height = image.height();
    var  newX = 0;
    var  newY = 0;
    var  offset = wrap.offset();

    $(imgID).hammer().on("pinch", function(event) {
        var photo = $(this);

        newWidth = photo.width() * event.gesture.scale;
        newHeight = photo.height() * event.gesture.scale;

        // Convert from screen to image coordinates
        var x;
        var y;
        x -= offset.left + newX;
        y -= + newY;

        newX += -x * (newWidth - width) / newWidth;
        newY += -y * (newHeight - height) / newHeight;

        photo.css('-webkit-transform', "scale3d("+event.gesture.scale+", "+event.gesture.scale+", 1)");      
        wrap.css('-webkit-transform', "translate3d("+newX+"px, "+newY+"px, 0)");

        width = newWidth;
        height = newHeight;


I looked all over the internet, and outernet whatever, until I came across the only working plugin/library -

var myScroll;
myScroll = new iScroll('wrapper');

where wrapper is your id as in id="wrapper"

<div id="wrapper">
    <img src="smth.jpg" />

This is something I wrote a few years back in Java and recently converted to JavaScript

function View()
 this.pos = {x:0,y:0};
 this.Z = 0;
 this.zoom = 1;
 this.scale = 1.1;
 this.Zoom = function(delta,x,y)
  var X = x-this.pos.x;
  var Y = y-this.pos.y;
  var scale = this.scale;
  if(delta>0) this.Z++;
   scale = 1/scale;
  this.zoom = Math.pow(this.scale, this.Z);

The this.Zoom = function(delta,x,y) takes:

  • delta = zoom in or out
  • x = x position of the zoom origin
  • y = y position of the zoom origin

A small example:

var view = new View();
var DivStyle = {x:-123,y:-423,w:300,h:200};
function OnMouseWheel(event)
 view.Zoom(event.wheelDelta,event.clientX,event.clientY); = (DivStyle.x*view.zoom+view.pos.x)+"px"; = (DivStyle.y*view.zoom+view.pos.y)+"px"; = (DivStyle.w*view.zoom)+"px"; = (DivStyle.h*view.zoom)+"px";
function OnMouseMove(event)
 view.pos = {x:event.clientX,y:event.clientY}; = (DivStyle.x*view.zoom+view.pos.x)+"px"; = (DivStyle.y*view.zoom+view.pos.y)+"px"; = (DivStyle.w*view.zoom)+"px"; = (DivStyle.h*view.zoom)+"px";
<body onmousewheel="OnMouseWheel(event)" onmousemove="OnMouseMove(event)">
<div id="div" style="position:absolute;left:-123px;top:-423px;width:300px;height:200px;border:1px solid;"></div>

This was made with the intention of being used with a canvas and graphics, but it should work perfectly for normal HTML layout

Not a real answer, but a link to a plug=in that does it all for you. Great work!

(Thanks 'Timmywil', who-ever you are)

Need Your Help

How to have vba execute every 10 minutes?

excel vba excel-vba

I need to have my macro executed every 10 minutes .

Does Razor syntax provide a compelling advantage in UI markup? syntax markup razor

I notice Scott Guthrie is starting to mention Razor a fair bit on his blog but I'm just not that sure that it's a good fit for my style.