Zoom and pan, introduction to FabricJS part 5

We've covered so many topics in the previous series; from basic object manipulations to animations, events, filters, groups, and subclasses. But there's still couple of very interesting and useful things to discuss!

Zoom and panning

Let's see how we can implement a basic system of zoom and pan with the mouse interactions. We will use the mouse wheel to zoom up to 20X ( 2000% ) on a canvas and a alt + click action to drag around.
We start be hooking up the basic controls:

canvas.on('mouse:wheel', function(opt) {
  var delta = opt.e.deltaY;
  var zoom = canvas.getZoom();
  zoom = zoom + delta/200;
  if (zoom > 20) zoom = 20;
  if (zoom < 0.01) zoom = 0.01;
  canvas.setZoom(zoom);
  opt.e.preventDefault();
  opt.e.stopPropagation();
})
    

This is a basic zoom control, limited between 1% and 2000%. we want now to add dragging of the canvas. We will use ALT + DRAG, but you can change to another combination. The idea is that a mousedown with alt will set a boolean to true, so that a mouse move event can then understand that is time for dragging.

canvas.on('mouse:down', function(opt) {
  var evt = opt.e;
  if (evt.altKey === true) {
    this.isDragging = true;
    this.selection = false;
    this.lastPosX = evt.clientX;
    this.lastPosY = evt.clientY;
  }
});
canvas.on('mouse:move', function(opt) {
  if (this.isDragging) {
    var e = opt.e;
    this.viewportTransform[4] += e.clientX - this.lastPosX;
    this.viewportTransform[5] += e.clientY - this.lastPosY;
    this.requestRenderAll();
    this.lastPosX = e.clientX;
    this.lastPosY = e.clientY;
  }
});
canvas.on('mouse:up', function(opt) {
  this.isDragging = false;
  this.selection = true;
});
    

Ok, this is a basic setup that will allow you to control zoom and panning. There are still a couple of possible enhancement.
For example we can make the wheel-zoom to center the canvas around the point where the cursor is:

canvas.on('mouse:wheel', function(opt) {
  var delta = opt.e.deltaY;
  var pointer = canvas.getPointer(opt.e);
  var zoom = canvas.getZoom();
  zoom = zoom + delta/200;
  if (zoom > 20) zoom = 20;
  if (zoom < 0.01) zoom = 0.01;
  canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom);
  opt.e.preventDefault();
  opt.e.stopPropagation();
});
    

As a final touch we can limit the panning area to avoid view to go infinity in one direction. We stroke a rect of 1000x1000 pixels that will represent our panning area. And we add the code to limit the movements in that boundaries:

canvas.on('mouse:wheel', function(opt) {
var delta = opt.e.deltaY;
var zoom = canvas.getZoom();
zoom = zoom + delta/200;
if (zoom > 20) zoom = 20;
if (zoom < 0.01) zoom = 0.01;
canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom);
opt.e.preventDefault();
opt.e.stopPropagation();
var vpt = this.viewportTransform;
if (zoom < 400 / 1000) {
  this.viewportTransform[4] = 200 - 1000 * zoom / 2;
  this.viewportTransform[5] = 200 - 1000 * zoom / 2;
} else {
  if (vpt[4] >= 0) {
    this.viewportTransform[4] = 0;
  } else if (vpt[4] < canvas.getWidth() - 1000 * zoom) {
    this.viewportTransform[4] = canvas.getWidth() - 1000 * zoom;
  }
  if (vpt[5] >= 0) {
    this.viewportTransform[5] = 0;
  } else if (vpt[5] < canvas.getHeight() - 1000 * zoom) {
    this.viewportTransform[5] = canvas.getHeight() - 1000 * zoom;
  }
})