Table of Contents

Fix, Fork, Contribute

How to implement zoom from mouse in 2D WebGL


I'm currently building a 2D drawing app in WebGL. I want to implement zoom to point under mouse cursor similar to example in here. But I can't figure out how to apply the solution from that answer in my case.

I have done basic zoom by scaling camera matrix. But it zooms to the top-left corner of the canvas, due to that being the origin (0,0) set by the projection (as far as I understand).

Basic pan & zoom implemented: img

My draw function (including matrix computations) looks like this:

var projection = null;
var view = null;
var viewProjection = null;

function draw(gl, camera, sceneTree){
  // projection matrix
  projection = new Float32Array(9);
  mat3.projection(projection, gl.canvas.clientWidth, gl.canvas.clientHeight);

  // camera matrix
  view = new Float32Array(9);
  mat3.fromTranslation(view, camera.translation);
  mat3.rotate(view, view, toRadians(camera.rotation));
  mat3.scale(view, view, camera.scale);
  // view matrix
  mat3.invert(view, view)

  // VP matrix
  viewProjection = new Float32Array(9);
  mat3.multiply(viewProjection, projection, view);

  // go through scene tree:
  //  - build final matrix for each object
  //      e.g: u_matrix = VP x Model (translate x rotate x scale) 

  // draw each object in scene tree
  // ... 

Vertex shader:

attribute vec2 a_position;

uniform mat3 u_matrix;

void main() {
  gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);

Zoom function:

function screenToWorld(screenPos){
  // normalized screen position 
  let nsp = [
     2.0 * screenPos[0] / - 1,
     - 2.0 * screenPos[1] / + 1

  let inverseVP = new Float32Array(9);
  mat3.invert(inverseVP, viewProjection);

  let worldPos = [0, 0];
  return vec2.transformMat3(worldPos, nsp, inverseVP);

var zoomRange = [0.01, 2];

canvas.addEventListener('wheel', (e) => {
  let oldZoom = camera.scale[0];
  let zoom = Math.min(Math.max(oldZoom + e.deltaX / 100, zoomRange[0]), zoomRange[1]);

  camera.scale = [zoom, zoom];

  let zoomPoint = screenToWorld([e.clientX, e.clientY]);
  // totally breaks if enable this line 
  //vec2.copy(camera.translation, zoomPoint);

  // call draw function again

}, false);

If I apply zoomPoint to camera translation, the values of zoomPoint (and the camera position accordingly) start to raise up uncontrollably with every zoom event (no mater if I zoom in or out) and the objects drawn in the scene go immediately out of view.

Would greatly appreciate any insights or suggestions about what am I doing wrong here. Thanks.


Since you didn't post a minimal reproducible example in the question itself I couldn't test with your math library. Using my own though I was able to zoom like this

  const [clipX, clipY] = getClipSpaceMousePosition(e);

  // position before zooming
  const [preZoomX, preZoomY] = m3.transformPoint(
      [clipX, clipY]);

  // multiply the wheel movement by the current zoom level
  // so we zoom less when zoomed in and more when zoomed out
  const newZoom = camera.zoom * Math.pow(2, e.deltaY * -0.01);
  camera.zoom = Math.max(0.02, Math.min(100, newZoom));


  // position after zooming
  const [postZoomX, postZoomY] = m3.transformPoint(
      [clipX, clipY]);

  // camera needs to be moved the difference of before and after
  camera.x += preZoomX - postZoomX;
  camera.y += preZoomY - postZoomY;

Note that zoom is the opposite of scale. If zoom = 2 then I want everything to appear 2x larger. To do that requires shrinking the camera space so we scale that space by 1 / zoom


note that I included camera.rotation just to make sure things worked if rotated. They seem to. Here's one with zoom, pan, and rotate

The question and quoted portions thereof are CC BY-SA 4.0 by nicktgn from here
Questions? Ask on stackoverflow.
Issue/Bug? Create an issue on github.
Use <pre><code>code goes here</code></pre> for code blocks
comments powered by Disqus