title: CSS3 zooming on mouse cursor
body: <p>My goal is to create a plugin that enables zooming &amp; panning operations on a page area, just like how Google Maps currently works (meaning: scrolling with the mouse = zooming in/out of the area, click &amp; hold &amp; move &amp; release = panning).</p>
  <p>When scrolling, I wish to have a zoom operation centered on the mouse cursor.</p>
  <p>For this, I use on-the-fly CSS3 matrix transformations. The only, yet mandatory, constraint is that I cannot use anything else than CSS3 translate &amp; scale transformations, with a transform origin of 0px 0px.</p>
  <p>Panning is out of the scope of my question, since I have it working already.
  When it comes to zooming, I am struggling to figure out where the glitch is in my Javascript code.</p>
  <p>The problem must be somewhere in the <code>MouseZoom.prototype.zoom</code> function, in the calculation of the translation on the x axis and y axis.</p>
  <p>First, here is my HTML code:</p>
  <pre><code>&lt;!DOCTYPE html&gt;
  &lt;html&gt;
  &lt;head&gt;
      &lt;meta name=&quot;viewport&quot; content=&quot;width = device-width, initial-scale = 1.0, user-scalable = no&quot; /&gt;
      &lt;meta name=&quot;apple-mobile-web-app-capable&quot; content=&quot;yes&quot;&gt;
      &lt;meta name=&quot;apple-mobile-web-app-status-bar-style&quot; content=&quot;black&quot; /&gt;
      &lt;script src=&quot;https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js&quot;&gt;&lt;/script&gt;
      &lt;script src=&quot;jquery.mousewheel.min.js&quot;&gt;&lt;/script&gt;
      &lt;script src=&quot;StackOverflow.js&quot;&gt;&lt;/script&gt;
      &lt;style type=&quot;text/css&quot; media=&quot;all&quot;&gt;
          #drawing {
              position: absolute;
              top: 0px; 
              left: 0px; 
              right:0; 
              bottom:0;
              z-index: 0;
              background: url(http://catmacros.files.wordpress.com/2009/09/cats_banzai.jpg) no-repeat;
              background-position: 50% 50%;
          }
      &lt;/style&gt;
      &lt;title&gt;Test&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
      &lt;div id=&quot;drawing&quot;&gt;&lt;/div&gt;
      &lt;script&gt;
          var renderer = new ZoomPanRenderer(&quot;drawing&quot;);
      &lt;/script&gt;
  &lt;/body&gt;
  &lt;/html&gt;
  </code></pre>
  <p>As you can see, I am using Jquery and the jquery mouse wheel plugin from Brandon Aaron, which can be found here:
  <a href="https://github.com/brandonaaron/jquery-mousewheel/" rel="nofollow noreferrer">https://github.com/brandonaaron/jquery-mousewheel/</a></p>
  <p>Here is the content of the StackOverflow.js file:</p>
  <pre><code>/***************************************************** 
   * Transformations
   ****************************************************/
  function Transformations(translateX, translateY, scale){
      this.translateX = translateX;
      this.translateY = translateY;
      this.scale = scale;
  }
  
  /* Getters */
  Transformations.prototype.getScale = function(){ return this.scale; }
  Transformations.prototype.getTranslateX = function(){ return this.translateX; }
  Transformations.prototype.getTranslateY = function(){ return this.translateY; }
  
  /***************************************************** 
   * Zoom Pan Renderer
   ****************************************************/
  function ZoomPanRenderer(elementId){
      this.zooming = undefined;
      this.elementId = elementId;
      this.current = new Transformations(0, 0, 1);
      this.last = new Transformations(0, 0, 1);
      new ZoomPanEventHandlers(this);
  }
  
  /* setters */
  ZoomPanRenderer.prototype.setCurrentTransformations = function(t){ this.current = t; }
  ZoomPanRenderer.prototype.setZooming = function(z){ this.zooming = z; }
  
  /* getters */
  ZoomPanRenderer.prototype.getCurrentTransformations = function(){ return this.current; }
  ZoomPanRenderer.prototype.getZooming = function(){ return this.zooming; }
  ZoomPanRenderer.prototype.getLastTransformations = function(){ return this.last; }
  ZoomPanRenderer.prototype.getElementId = function(){ return this.elementId; }
  
  /* Rendering */
  ZoomPanRenderer.prototype.getTransform3d = function(t){
      var transform3d = &quot;matrix3d(&quot;;
      transform3d+= t.getScale().toFixed(10) + &quot;,0,0,0,&quot;;
      transform3d+= &quot;0,&quot; + t.getScale().toFixed(10) + &quot;,0,0,&quot;;
      transform3d+= &quot;0,0,1,0,&quot;;
      transform3d+= t.getTranslateX().toFixed(10) + &quot;,&quot; + t.getTranslateY().toFixed(10)  + &quot;,0,1)&quot;;
      return transform3d;
  }
  
  ZoomPanRenderer.prototype.getTransform2d = function(t){
      var transform3d = &quot;matrix(&quot;;
      transform3d+= t.getScale().toFixed(10) + &quot;,0,0,&quot; + t.getScale().toFixed(10) + &quot;,&quot; + t.getTranslateX().toFixed(10) + &quot;,&quot; + t.getTranslateY().toFixed(10) + &quot;)&quot;;
      return transform3d;
  }
  
  ZoomPanRenderer.prototype.applyTransformations = function(t){
      var elem = $(&quot;#&quot; + this.getElementId());
      elem.css(&quot;transform-origin&quot;, &quot;0px 0px&quot;);
      elem.css(&quot;-ms-transform-origin&quot;, &quot;0px 0px&quot;);
      elem.css(&quot;-o-transform-origin&quot;, &quot;0px 0px&quot;);
      elem.css(&quot;-moz-transform-origin&quot;, &quot;0px 0px&quot;);
      elem.css(&quot;-webkit-transform-origin&quot;, &quot;0px 0px&quot;);
      var transform2d = this.getTransform2d(t);
      elem.css(&quot;transform&quot;, transform2d);
      elem.css(&quot;-ms-transform&quot;, transform2d);
      elem.css(&quot;-o-transform&quot;, transform2d);
      elem.css(&quot;-moz-transform&quot;, transform2d);
      elem.css(&quot;-webkit-transform&quot;, this.getTransform3d(t));
  }
  
  /***************************************************** 
   * Event handler
   ****************************************************/
  function ZoomPanEventHandlers(renderer){
      this.renderer = renderer;
      
      /* Disable scroll overflow - safari */
      document.addEventListener('touchmove', function(e) { e.preventDefault(); }, false);
      
      /* Disable default drag opeartions on the element (FF makes it ready for save)*/
      $(&quot;#&quot; + renderer.getElementId()).bind('dragstart', function(e) { e.preventDefault(); });
      
      /* Add mouse wheel handler */
      $(&quot;#&quot; + renderer.getElementId()).bind(&quot;mousewheel&quot;, function(event, delta) {
          if(renderer.getZooming()==undefined){
              var offsetLeft = $(&quot;#&quot; + renderer.getElementId()).offset().left;
              var offsetTop = $(&quot;#&quot; + renderer.getElementId()).offset().top;
              var zooming = new MouseZoom(renderer.getCurrentTransformations(), event.pageX, event.pageY, offsetLeft, offsetTop, delta);
              renderer.setZooming(zooming);
              
              var newTransformation = zooming.zoom();
              renderer.applyTransformations(newTransformation);
              renderer.setCurrentTransformations(newTransformation);
              renderer.setZooming(undefined);
          }
          return false;
      });
  }
  
  /***************************************************** 
   * Mouse zoom
   ****************************************************/
  function MouseZoom(t, mouseX, mouseY, offsetLeft, offsetTop, delta){
      this.current = t;
      this.offsetLeft = offsetLeft;
      this.offsetTop = offsetTop;
      this.mouseX = mouseX;
      this.mouseY = mouseY;
      this.delta = delta;
  }
  
  MouseZoom.prototype.zoom = function(){
      var previousScale = this.current.getScale();
      var newScale = previousScale + this.delta/5;
      if(newScale&lt;1){
          newScale = 1;
      }
      var ratio = newScale / previousScale;
      
      var imageX = this.mouseX - this.offsetLeft;
      var imageY = this.mouseY - this.offsetTop;
      
      var previousTx = - this.current.getTranslateX() * previousScale;
      var previousTy = - this.current.getTranslateY() * previousScale;
      var previousDx = imageX * previousScale;
      var previousDy = imageY * previousScale;
  
      var newTx = (previousTx * ratio + previousDx * (ratio - 1)) / newScale;
      var newTy = (previousTy * ratio + previousDy * (ratio - 1)) / newScale;
      
      return new Transformations(-newTx, -newTy, newScale);
  }
  </code></pre>