title: Set canvas zoom/scale origin
body: <p>I'm trying to create zoom effect on canvas, and I've managed to do that, but there's a small problem. Zooming (scaling) origin is top left of the canvas. How can I specify a zoom/scale origin?</p>
  
  <p>I suppose I need to use <code>translate</code> but I don't know how and where I should implement it.</p>
  
  <p>What I want to use as zoom origin is the mouse position, but for simplicity, center of canvas will do.</p>
  
  <p><a href="https://jsfiddle.net/akinuri/byhnb65j/" rel="noreferrer">JSFiddle</a></p>
  
  <p><div class="snippet" data-lang="js" data-hide="true" data-console="true" data-babel="false">
  <div class="snippet-code snippet-currently-hidden">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>var canvas = document.getElementById("canvas");
  var context = canvas.getContext("2d");
  canvas.width = 600;
  canvas.height = 400;
  
  var global = {
    zoom: {
      origin: {
        x: null,
        y: null,
      },
      scale: 1,
    },
  };
  
  function zoomed(number) {
    return Math.floor(number * global.zoom.scale);
  }
  
  function draw() {
    context.beginPath();
    context.rect(zoomed(50), zoomed(50), zoomed(100), zoomed(100));
    context.fillStyle = 'skyblue';
    context.fill();
  
    context.beginPath();
    context.arc(zoomed(350), zoomed(250), zoomed(50), 0, 2 * Math.PI, false);
    context.fillStyle = 'green';
    context.fill();
  }
  
  draw();
  
  canvas.addEventListener("wheel", trackWheel);
  canvas.addEventListener("wheel", zoom);
  
  function zoom() {
    context.setTransform(1, 0, 0, 1, 0, 0);
    context.clearRect(0, 0, canvas.width, canvas.height);
    draw();
  }
  
  function trackWheel(e) {
    if (e.deltaY &lt; 0) {
      if (global.zoom.scale &lt; 5) {
        global.zoom.scale *= 1.1;
      }
    } else {
      if (global.zoom.scale &gt; 0.1) {
        global.zoom.scale *= 0.9;
      }
    }
    global.zoom.scale = parseFloat(global.zoom.scale.toFixed(2));
  }</code></pre>
  <pre class="snippet-code-css lang-css prettyprint-override"><code>body {
    background: gainsboro;
    margin: 0;
  }
  canvas {
    background: white;
    box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
  }</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;canvas id="canvas"&gt;&lt;/canvas&gt;</code></pre>
  </div>
  </div>
  </p>
  
  <p><hr>
  <strong>Update 1</strong></p>
  
  <p>It seems there are few other question related to this subject on SO, but none that I can directly implement in my code. </p>
  
  <p>I've tried to examine the <a href="http://phrogz.net/tmp/canvas_zoom_to_cursor.html" rel="noreferrer">demo</a> Phrogz provided in <a href="https://stackoverflow.com/questions/5189968/zoom-canvas-to-mouse-cursor">Zoom Canvas to Mouse Cursor</a>, but it's far too complex (for me, at least). Tried to implement his solution:</p>
  
  <pre><code>ctx.translate(pt.x,pt.y);
  ctx.scale(factor,factor);
  ctx.translate(-pt.x,-pt.y);
  </code></pre>
  
  <p><a href="https://jsfiddle.net/akinuri/byhnb65j/1/" rel="noreferrer">JSFiddle</a></p>
  
  <p><div class="snippet" data-lang="js" data-hide="true" data-console="true" data-babel="false">
  <div class="snippet-code snippet-currently-hidden">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>var canvas = document.getElementById("canvas");
  var context = canvas.getContext("2d");
  canvas.width = 600;
  canvas.height = 400;
  
  var global = {
    zoom: {
      origin: {
        x: null,
        y: null,
      },
      scale: 1,
    },
  };
  
  function draw() {
    context.beginPath();
    context.rect(50, 50, 100, 100);
    context.fillStyle = 'skyblue';
    context.fill();
  
    context.beginPath();
    context.arc(350, 250, 50, 0, 2 * Math.PI, false);
    context.fillStyle = 'green';
    context.fill();
  }
  
  draw();
  
  canvas.addEventListener("wheel", trackWheel);
  canvas.addEventListener("wheel", trackMouse);
  canvas.addEventListener("wheel", zoom);
  
  function zoom() {
    context.setTransform(1, 0, 0, 1, 0, 0);
    context.clearRect(0, 0, canvas.width, canvas.height);
  
    context.translate(global.zoom.origin.x, global.zoom.origin.y);
    context.scale(global.zoom.scale, global.zoom.scale);
    context.translate(-global.zoom.origin.x, -global.zoom.origin.y);
  
    draw();
  }
  
  function trackWheel(e) {
    if (e.deltaY &gt; 0) {
      if (global.zoom.scale &gt; 0.1) {
        global.zoom.scale *= 0.9;
      }
    } else {
      if (global.zoom.scale &lt; 5) {
        global.zoom.scale *= 1.1;
      }
    }
    global.zoom.scale = parseFloat(global.zoom.scale.toFixed(2));
  }
  
  function trackMouse(e) {
    global.zoom.origin.x = e.clientX;
    global.zoom.origin.y = e.clientY;
  }</code></pre>
  <pre class="snippet-code-css lang-css prettyprint-override"><code>body {
    background: gainsboro;
    margin: 0;
  }
  canvas {
    background: white;
    box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
  }</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;canvas id="canvas"&gt;&lt;/canvas&gt;</code></pre>
  </div>
  </div>
  </p>
  
  <p>but it didn't really help. It seems to use the mouse position as the zoom origin but there are "jumps" when I zoom in.</p>
  
  <hr>
  
  <p><strong>Update 2</strong></p>
  
  <p>I've managed to isolate and simplify the zoom effect from Blindman67's example to understand how it works better. I gotta admit, I still don't fully understand it :) I'm gonna share it here. Future visitors might benefit.</p>
  
  <p><a href="https://jsfiddle.net/akinuri/cbx7pmra/" rel="noreferrer">JSFiddle</a></p>
  
  <p><div class="snippet" data-lang="js" data-hide="true" data-console="true" data-babel="false">
  <div class="snippet-code snippet-currently-hidden">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>var canvas    = document.getElementById("canvas");
  var context   = canvas.getContext("2d");
  canvas.width  = 600;
  canvas.height = 400;
  
  var zoom = {
    scale : 1,
    screen : {
      x : 0,
      y : 0,
    },
    world : {
      x : 0,
      y : 0,
    },
  };
  
  var mouse = {
    screen : {
      x : 0,
      y : 0,
    },
    world : {
      x : 0,
      y : 0,
    },
  };
  
  var scale = {
    length : function(number) {
      return Math.floor(number * zoom.scale);
    },
    x : function(number) {
      return Math.floor((number - zoom.world.x) * zoom.scale + zoom.screen.x);
    },
    y : function(number) {
      return Math.floor((number - zoom.world.y) * zoom.scale + zoom.screen.y);
    },
    x_INV : function(number) {
      return Math.floor((number - zoom.screen.x) * (1 / zoom.scale) + zoom.world.x);
    },
    y_INV : function(number) {
      return Math.floor((number - zoom.screen.y) * (1 / zoom.scale) + zoom.world.y);
    },
  };
  
  function draw() {
    context.clearRect(0, 0, canvas.width, canvas.height);
  
    context.beginPath();
    context.rect(scale.x(50), scale.y(50), scale.length(100), scale.length(100));
    context.fillStyle = 'skyblue';
    context.fill();
  
    context.beginPath();
    context.arc(scale.x(350), scale.y(250), scale.length(50), 0, 2 * Math.PI, false);
    context.fillStyle = 'green';
    context.fill();
  }
  
  canvas.addEventListener("wheel", zoomUsingCustomScale);
  
  function zoomUsingCustomScale(e) {
    trackMouse(e);
    trackWheel(e);
    scaleShapes();
  }
  
  function trackMouse(e) {
    mouse.screen.x = e.clientX;
    mouse.screen.y = e.clientY;
    mouse.world.x  = scale.x_INV(mouse.screen.x);
    mouse.world.y  = scale.y_INV(mouse.screen.y);
  }
  
  function trackWheel(e) {
    if (e.deltaY &lt; 0) {
      zoom.scale = Math.min(5, zoom.scale * 1.1);
    } else {
      zoom.scale = Math.max(0.1, zoom.scale * (1/1.1));
    }
  }
  
  function scaleShapes() {
    zoom.screen.x = mouse.screen.x;
    zoom.screen.y = mouse.screen.y;
    zoom.world.x = mouse.world.x;
    zoom.world.y = mouse.world.y;
    mouse.world.x = scale.x_INV(mouse.screen.x);
    mouse.world.y = scale.y_INV(mouse.screen.y);
    draw();
  }
  
  draw();</code></pre>
  <pre class="snippet-code-css lang-css prettyprint-override"><code>body {
    background: gainsboro;
    margin: 0;
  }
  
  canvas {
    background: white;
    box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
  }</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;canvas id="canvas"&gt;&lt;/canvas&gt;</code></pre>
  </div>
  </div>
  </p>
  
  <p>Since this is a simplified version, I suggest you check out the Blindman67's example first. Also, even though I've accepted Blindman67's answer, you can still post an answer. I find this subject interesting. So I'd like to know more about it.</p>