Canvas and Fabric.JS

 | 

Traditionally, we have utilized a normal file upload for user profile pictures, where we automatically center then crop after upload. This has a list of drawbacks.

  1. User can not edit the photo, and have no idea how it looks until image is uploaded.
  2. We don’t know how big the picture is until it is transferred to server-side.
  3. If image is rejected for being too big or malformed, feedback to user is very slow because we would have to get the file first.

During evaluation phase, Fabric.js emerged as the winning candidate not only because it solves the problems above, but also because it’s a comprehensive and easy to use wrapper around HTML5 Canvas, compared to the likes of Cropper.js, which, while popular, doesn’t use Canvas. Furthermore, Fabric.js has good community support.

Fabric.js achieves the above in the following ways. First, it uses Local Storage to store the image file as it is worked on. This means we can do everything client-side until user is ready to submit. Throughout the editing process, user can see the changes without network roundtrips. When user is ready to submit, we control the size and quality of the image. For our intent and purpose, something like 300×300 good quality jpg is more than enough, but when users upload a picture, it’s often much bigger, since no camera these days would shoot such a small picture by default. Fabric.js allows us to adjust the picture size first. In the end, instead of uploading a 1MB picture, we are uploading the finished product, and it’s roughly a 60KB picture, over an order of magnitude smaller. Finally, all submitted pictures will be uniform in size and quality.

Below is the code for setting up the canvas. We define the size of it, and hence the resulting image’s size. We also add a clipping mask to make it round for use as a profile image.

var canvas = new fabric.Canvas('canvas', {
preserveObjectStacking: true
});
var ctx = canvas.getContext("2d");
canvas.setHeight(280);
canvas.setWidth(280);
ctx.arc(140, 140, 140, 0, Math.PI * 2, true);
ctx.save();
ctx.clip();

canvas.renderAll();

Below is the code that wraps the image in local storage as an image object so we can work with it. As you can see there are multiple settings we can play with. There are many more that’s not shown below.

$('#uploadedImg').change(function (e) {
  var reader = new FileReader();
  reader.onload = function (event){
    var imgObj = new Image();
    imgObj.src = event.target.result;
    imgObj.onload = function () {
      var fabricImage = new fabric.Image(imgObj);
      fabricImage.set({
        top: 0,
        left: 0,
        angle: 0,
        hasBorders: false,
        cornerSize: 0,
        lockRotation: true
      });
      if (fabricImage.height < fabricImage.width) {
        fabricImage.scaleToHeight(canvas.getHeight()+10);
      } else {
        fabricImage.scaleToWidth(canvas.getWidth()+10);
      }
      canvas.clear();
      canvas.add(fabricImage);
      canvas.centerObject(fabricImage);
      canvas.setActiveObject(fabricImage);
      canvas.renderAll();
    }
  }

And the code below is for upload. Notice the simplicity of serializing the canvas to a jpg and just sending it out to the backend API.

var data = canvas.toDataURL({format: 'jpg'}); //png

var picture = {
  image: data,
  name: 'userUpload',
  size: data.length,
  type: 'img/jpg'
}

UserResource.uploadPictures(picture);

But wait, there is more! In addition to panning, zooming, and rotating, there are a lot more functionality available from Canvas and Fabric.js, such as vector graphics, animated objects, and 3D context. Fabric.js for image manipulation is like moment.js for time and time zone management, and underscore.js for data structure manipulation for Javascript.

Leave a Reply