Drawing with Fabric.js
By Tim Pollock, OCI Principal Software Engineer
June 2014
Introduction
Fabric.js is a Javascript library that simplifies drawing graphics to the HTML5 canvas element. This article shows some simple Fabric.js examples and addresses some issues that are commonly encountered with its use. The goal here is to show how to draw objects, keep them within a window when they are dragged, and provide zooming and panning. This might be used as a starting point for an application to let users create objects and define their positions.
The examples shown in this article are available for download here.
Browser Support
Fabric.js only works on modern browsers. The list below is from the Fabric.js GitHub page and shows which browsers it will work on:
- Firefox 2+
- Safari 3+
- Opera 9.64+
- Chrome (all versions should work)
- IE9, IE10, IE11
Demos
You can view several demos at the Fabric.js Demo page. In this article, however, we'll focus on drawing rectangles and manipulation them in different ways.
Creating a Canvas
We'll begin by creating an HTML5 canvas object. Older browsers may not handle HTML5 elements properly, so we'll display a message on those browsers instead. If you're using a modern browser that supports HTML5, then this example won't be very interesting, but if you're using an older browser you should see a message indicating that canvas isn't supported.
Creating a canvas is as simple as creating a Fabric.js object and setting its attributes.
- var canvas = new fabric.Canvas('myCanvas');
- canvas.setHeight(580);
- canvas.setWidth(865);
That results in an empty canvas:
[CodePen functionality removed since original publication]
References used in this example:
- Fabric.js canvas Documentation - http://fabricjs.com/docs/fabric.Canvas.html
- Mozilla canvas Documentation - https://developer.mozilla.org/en-US/docs/HTML/Canvas
- Detecting canvas Support - http://stackoverflow.com/questions/2745432/best-way-to-detect-that-html5-canvas-is-not-supported
Drawing a Rectangle
Next, we'll draw a rectangle on the canvas by using the Fabric.js Rect
method, then set its position, size and some other attributes.
- var rect = new fabric.Rect({
- left: 150,
- top: 100,
- fill: rectFill,
- stroke: rectStroke,
- width: 150,
- height: 250
- });
-
- canvas.add(rect);
The canvas.add
method adds the rectangle to the canvas. If you click on the rectangle you'll see that you can easily drag, rotate and resize it. This is true of all of the 2D canvas objects that Fabric allows you to create. Those include lines, circles, triangles, ellipses, rectangles, polylines, polygons, text, images and paths. We'll only focus on rectangles in this article, but what we cover here applies to all of the Fabric.js object types.
[CodePen functionality removed since original publication]
See the Pen Fabric_002 by OCI (@OCI) on CodePen.
References used in this example:
- Fabric.js Rect Documentation - http://fabricjs.com/docs/fabric.Rect.html
- Mozilla Rectangle Documentation - https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Canvas_tutorial/Drawing_shapes#Rectangles
- Fabric.js Rect Example - http://fabricjs.com/fabric-intro-part-1/
Bounding the Rectangle
One nice thing about the Fabric.js rectangle object is that it can be repositioned, but we'd like to keep it within the bounds of the canvas, so we'll handle the object:modified
event and restore a rectangle to its original position if it has moved too far.
- canvas.on('object:modified', function (e) {
- var obj = e.target;
-
- if (obj.getLeft() < 0
- || obj.getTop() < 0
- || obj.getLeft() + obj.getWidth() > canvas.getWidth()
- || obj.getTop() + obj.getHeight() > canvas.getHeight()) {
- if (obj.getAngle() != obj.originalState.angle) {
- obj.setAngle(obj.originalState.angle);
- }
- else {
- obj.setTop(obj.originalState.top);
- obj.setLeft(obj.originalState.left);
- obj.setScaleX(obj.originalState.scaleX);
- obj.setScaleY(obj.originalState.scaleY);
- }
- obj.setCoords();
- }
- });
Now if you move the rectangle out of the canvas area the move will be rejected, resulting it the rectangle going back to its original position.
[CodePen functionality removed since original publication]
See the Pen Fabric_003 by OCI (@OCI) on CodePen.
References used in this example:
- Keeping elements within the canvas - https://groups.google.com/forum/#!topic/fabricjs/DHvNmjJfaYM
The Bounding Box
The previous example isn't perfect, since if you rotate the rectangle you might notice that you can place it outside the canvas bounds, or it might not allow you to move it to particular locations within the canvas. This is because we're using the top-left corner of the rectangle, and our calculations are not correct once it has been rotated. To fix this we should bound the rectangle by using its bounding box. Let's show that bounding box by drawing it in the after:render
handler, just to show where it is located.
- canvas.on('after:render', function () {
- canvas.contextContainer.strokeStyle = '#555';
-
- canvas.forEachObject(function (obj) {
- var bound = obj.getBoundingRect();
-
- canvas.getContext().strokeRect(
- bound.left + 0.5,
- bound.top + 0.5,
- bound.width,
- bound.height
- );
- });
- });
Now when you rotate the rectangle you will see it within a bounding box. In our next example we'll bound the bounding box of the rectangle within the canvas.
[CodePen functionality removed since original publication]
See the Pen Fabric_004 Show Bounding Box by OCI (@OCI) on CodePen.
References used in this example:
- Bounding rectangle example - http://fabricjs.com/bounding-rectangle/
Bounding with the Bounding Box
Now that we can determine the position of the bounding box of a rectangle, we can do a better job of keeping a rectangle within the bounds of the canvas. We'll get the bounding box and check that it stays within the canvas when the rectangle is moved or resized.
- canvas.on('object:modified', function (e) {
- var obj = e.target;
- var rect = obj.getBoundingRect();
-
- if (rect.left < 0
- || rect.top < 0
- || rect.left + rect.width > canvas.getWidth()
- || rect.top + rect.height > canvas.getHeight()) {
- if (obj.getAngle() != obj.originalState.angle) {
- obj.setAngle(obj.originalState.angle);
- }
- else {
- obj.setTop(obj.originalState.top);
- obj.setLeft(obj.originalState.left);
- obj.setScaleX(obj.originalState.scaleX);
- obj.setScaleY(obj.originalState.scaleY);
- }
- obj.setCoords();
- }
- });
Now our rectangle is properly bound within the canvas.
[CodePen functionality removed since original publication]
See the Pen Fabric_005 Bound By Bounding Box by OCI (@OCI) on CodePen.
References used in this example:
- Fabric.js event handling - https://github.com/kangax/fabric.js/wiki/Working-with-events
Adding Text
You might use rectangles in an application that displays businesses, where each rectangle shows the name of the business, along with its address and phone number. To do this we'll create a Fabric.js Text
object and add it, along with a rectangle, to a Fabric.js Group
. By doing so we can manipulate the Group
and see both the rectangle and the text move as a unit.
- var name = "Bob's Burgers";
- var address = "2343 Main";
- var phone = "111-222-3333";
-
- var text = new fabric.Text(name + '\n' + address + '\n' + phone, {
- fontFamily: fontFamily,
- fontSize: fontSize,
- shadow: textShadow
- });
-
- var rect = new fabric.Rect({
- fill: rectFill,
- stroke: rectStroke,
- width: 150,
- height: 250
- });
-
- var group = new fabric.Group([rect, text], {
- left: 150,
- top: 100
- });
-
- canvas.add(group);
- }
This results in the object shown below. Notice, however, that the text doesn't scale well when you change the rectangle width. We'll fix that in the next example.
[CodePen functionality removed since original publication]
See the Pen Fabric_006 Rectangle with Text by OCI (@OCI) on CodePen.
References used in this example:
- Example of adding text - http://fabricjs.com/fabric-intro-part-3/
Uniscaling
To keep the text within the rectangle from getting distorted when its width or height are changed, we'll specify that the rectangle only be resized by the corners to keep the aspect ratio from changing. To do this we'll set the Fabric.js Group
lockUniScaling
attribute to true.
- var group = new fabric.Group([rect, text], {
- left: 150,
- top: 100,
- lockUniScaling: true
- });
Now you can change the size of the rectangle, but the ratio of width to height remain constant.
[CodePen functionality removed since original publication]
See the Pen Fabric_007 Uniscaling by OCI (@OCI) on CodePen.
References used in this example:
- Fabric.js lockUniScaling documentation - http://fabricjs.com/docs/fabric.Object.html
Custom Corners
The rectangle corners and border can easily be changed by setting the appropriate attributes of the Fabric.js Group
that contains the rectangle and the text.
- var group = new fabric.Group([rect, text], {
- left: 150,
- top: 100,
- lockUniScaling: true,
- borderColor: '9DD290',
- cornerColor: '1A7206',
- cornerSize: 6,
- transparentCorners: false
- });
This was a simple change but it improves the look of the rectangle.
[CodePen functionality removed since original publication]
See the Pen Fabric_008 Custom Corners by OCI (@OCI) on CodePen.
References used in this example:
- Fabric.js customizations - http://fabricjs.com/customization/
Zooming
Our next step is to respond to the mouse wheel by zooming our canvas in and out. In order to better see the results of zooming we'll add a background image consisting of a rectangle that is filled with a pattern. That rectangle will be the same size as the canvas, so initially it will fill the entire canvas surface.
- var background = new fabric.Rect({
- left: 0,
- top: 0,
- stroke: 'White',
- width: canvasWidth,
- height: canvasHeight,
- scaleX: 1,
- scaleY: 1,
- selectable: false,
- zoomScale: 1,
- isBackground: true
- });
- canvas.add(background);
- fabric.util.loadImage('background.png', function (img) {
- background.setPatternFill({
- source: img,
- repeat: 'repeat'
- });
- canvas.renderAll();
- });
A second rectangle object will be added to demonstrate how to properly scale and position objects during zooming. Then we'll add a handler that responds to the mouse wheel event. In that function we'll verify that the mouse is within the bounds of the background and that the background is not zoomed out too far. Finally, the mousewheel handler will change the scale of all of the objects and reposition them in order to keep their relative positions correct.
- $("body").mousewheel(function (event, delta) {
- var mousePageX = event.pageX;
- var mousePageY = event.pageY;
-
- var offset = $("#myCanvas").offset();
- var canvasX = offset.left;
- var canvasY = offset.top;
-
- // Ignore if mouse is not on canvas
- if (mousePageX < canvasX || mousePageY < canvasY || mousePageX > (canvasX + canvas.width)
- || mousePageY > (canvasY + canvas.height)) {
- return;
- }
-
- // Ignore if mouse is not on background
- var background = getBackground();
-
- var mouseOffsetX = event.offsetX;
- var mouseOffsetY = event.offsetY;
-
- if (mouseOffsetX < background.left || mouseOffsetX > background.left + background.currentWidth
- || mouseOffsetY < background.top || mouseOffsetY > background.top + background.currentHeight) {
- return;
- }
-
- var scaleFactor = 1.1;
- var change = (delta > 0) ? scaleFactor : (1 / scaleFactor);
-
- // Limit zooming out
- var newZoomScale = zoomScale * change;
- if (newZoomScale < 1) {
- return;
- }
- zoomScale = newZoomScale;
-
- var backgroundWidthOrig = (background.width * background.scaleX);
- var backgroundHeightOrig = (background.height * background.scaleY);
-
- // Scale objects
- var objects = canvas.getObjects();
- for (var i in objects) {
- var obj = objects[i];
-
- if (obj.zoomScale != undefined) {
- obj.scaleX = obj.zoomScale * zoomScale;
- obj.scaleY = obj.zoomScale * zoomScale;
- }
-
- obj.left = obj.left * change;
- obj.top = obj.top * change;
- obj.setCoords();
- }
-
- var backgroundWidthNew = (background.width * background.scaleX);
- var backgroundHeightNew = (background.height * background.scaleY);
-
- var backgroundWidthDelta = backgroundWidthNew - backgroundWidthOrig;
- var backgroundHeightDelta = backgroundHeightNew - backgroundHeightOrig;
-
- // Shift objects
- for (var i in objects) {
- var obj = objects[i];
- obj.left -= (backgroundWidthDelta / 2);
- obj.top -= (backgroundHeightDelta / 2);
- obj.setCoords();
- }
-
- canvas.renderAll();
-
- event.stopPropagation();
- event.preventDefault();
- });
Now you can zoom the objects in and out. Click the HTML tab to
see the full example code.
[CodePen functionality removed since original publication]
References used in this example:
- Example of zooming - http://jsfiddle.net/w5NjC/1/
- Mouse wheel support - https://github.com/brandonaaron/jquery-mousewheel
Panning
Our last example will add panning so we can drag all of the objects across the canvas. We'll add mouse down, mouse move and mouse up handlers to do this.
The mouse down handler first checks that the mouse is within the bounds of the background, then verifies that the mouse down is on the background by checking if any objects have been selected. When we created the background we set selectable
to false, so clicking the background will not make it a selected object.
If we've clicked a rectangle we want to exit the mouse down handler, since we want to retain the ability to drag rectangles without panning the entire canvas. We'll then set a flag to indicate the mouse down event occurred outside of an object in order to properly handle a mouse move event, then we'll capture the position of each object so we can reposition them later.
- $('canvas').mousedown(function (e) {
- mouseXOrig = e.offsetX;
- mouseYOrig = e.offsetY;
-
- // Ignore if mouse is not on background
- var background = getBackground();
- if (mouseXOrig < background.left
- || mouseXOrig > background.left + (background.width * zoomScale)
- || mouseYOrig < background.top
- || mouseYOrig > background.top + (background.height * zoomScale)) {
- return;
- }
-
- var activeObject = canvas.getActiveObject();
- if (activeObject != undefined) {
- canvas.bringToFront(activeObject);
- return;
- }
-
- mouseDownOutsideObject = true;
-
- var objects = canvas.getObjects();
-
- for (var i = 0; i < objects.length; i++) {
- mouseXStart[i] = objects[i].left;
- mouseYStart[i] = objects[i].top;
- }
- });
The mouse move handler will do nothing if the mouse was clicked on an object. Otherwise, it will set the background object to selectable so it can be panned along with the other objects. It then calculates the distance the mouse moved and check that it has not moved too far (we don't want to drag our objects off of the canvas). Finally, it sets the position of each object and calls renderAll
in order to display them. Note the call to setCoords
, which is often necessary when repositioning objects in order to get Fabric.js to properly render them in their new positions.
- $('canvas').mousemove(function (e) {
- if (mouseDownOutsideObject) {
- var background = getBackground();
- background.selectable = true;
-
- var mouseXNew = e.pageX - $(this).offset().left;
- var mouseYNew = e.pageY - $(this).offset().top;
-
- var mouseXDelta = (mouseXNew - mouseXOrig);
- var mouseYDelta = (mouseYNew - mouseYOrig);
-
- var borderWidth = 200;
-
- if (background.getLeft() + mouseXDelta <= borderWidth
- && background.getTop() + mouseYDelta <= borderWidth
- && background.getLeft() + background.getWidth() + mouseXDelta >= canvas.getWidth() - borderWidth
- && background.getTop() + background.getHeight() + mouseYDelta >= canvas.getHeight() - borderWidth) {
- var objects = canvas.getObjects();
-
- for (var i = 0; i < objects.length; i++) {
- objects[i].setLeft(mouseXDelta + mouseXStart[i]);
- objects[i].setTop(mouseYDelta + mouseYStart[i]);
- objects[i].setCoords();
- }
-
- canvas.renderAll();
- }
-
- background.selectable = false;
- }
- });
The last bit of code we'll show will be the mouse up handler, which simple sets the background selectable
attribute back to false.
- canvas.on('mouse:up', function (e) {
- mouseDownOutsideObject = false;
- getBackground().selectable = false;
- });
Now we can click on the background and pan the view in order to move it around on the canvas.
[CodePen functionality removed since original publication]
References used in this example:
- Example of panning - http://jsbin.com/uzipec/9/edit
Summary
This article has shown how to draw objects on the HTML canvas using Fabric.js, and has provided some details on how to customize the objects you draw. The Customization page shows several additional examples. With Fabric.js you can easily write Javascript code to draw on a web page.
References
Fabric.js links are shown below:
- Fabric.js - http://fabricjs.com/
- Fabric.js GitHub - https://github.com/kangax/fabric.js/
- Fabric.js demos - http://fabricjs.com/demos/">Fabric.js Demo
- Fabric.js canvas Documentation - http://fabricjs.com/docs/fabric.Canvas.html
- Mozilla canvas Documentation - https://developer.mozilla.org/en-US/docs/HTML/Canvas
- Detecting canvas Support - http://stackoverflow.com/questions/2745432/best-way-to-detect-that-html5-canvas-is-not-supported
- Fabric.js Rect Documentation - http://fabricjs.com/docs/fabric.Rect.html
- Mozilla Rectangle Documentation - https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Canvas_tutorial/Drawing_shapes#Rectangles
- Fabric.js Rect Example - http://fabricjs.com/fabric-intro-part-1/
- Keeping elements within the canvas - https://groups.google.com/forum/#!topic/fabricjs/DHvNmjJfaYM
- Bounding rectangle example - http://fabricjs.com/bounding-rectangle/
- Fabric.js event handling - https://github.com/kangax/fabric.js/wiki/Working-with-events
- Example of adding text - http://fabricjs.com/fabric-intro-part-3/
- Fabric.js lockUniScaling documentation - http://fabricjs.com/docs/fabric.Object.html
- Fabric.js customizations - http://fabricjs.com/customization/
- Example of zooming - http://jsfiddle.net/w5NjC/1/
- Mouse wheel support - https://github.com/brandonaaron/jquery-mousewheel
- Example of panning - http://jsbin.com/uzipec/9/edit
Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.