Drawing with Fabric.js

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:

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.

  1. var canvas = new fabric.Canvas('myCanvas');
  2. canvas.setHeight(580);
  3. canvas.setWidth(865);

That results in an empty canvas:
[CodePen functionality removed since original publication]

See the Pen JdOpEB by OCI (@OCI) on CodePen.

References used in this example:

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.

  1. var rect = new fabric.Rect({
  2. left: 150,
  3. top: 100,
  4. fill: rectFill,
  5. stroke: rectStroke,
  6. width: 150,
  7. height: 250
  8. });
  9.  
  10. 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:

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.

  1. canvas.on('object:modified', function (e) {
  2. var obj = e.target;
  3.  
  4. if (obj.getLeft() < 0
  5. || obj.getTop() < 0
  6. || obj.getLeft() + obj.getWidth() > canvas.getWidth()
  7. || obj.getTop() + obj.getHeight() > canvas.getHeight()) {
  8. if (obj.getAngle() != obj.originalState.angle) {
  9. obj.setAngle(obj.originalState.angle);
  10. }
  11. else {
  12. obj.setTop(obj.originalState.top);
  13. obj.setLeft(obj.originalState.left);
  14. obj.setScaleX(obj.originalState.scaleX);
  15. obj.setScaleY(obj.originalState.scaleY);
  16. }
  17. obj.setCoords();
  18. }
  19. });

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:

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.

  1. canvas.on('after:render', function () {
  2. canvas.contextContainer.strokeStyle = '#555';
  3.  
  4. canvas.forEachObject(function (obj) {
  5. var bound = obj.getBoundingRect();
  6.  
  7. canvas.getContext().strokeRect(
  8. bound.left + 0.5,
  9. bound.top + 0.5,
  10. bound.width,
  11. bound.height
  12. );
  13. });
  14. });

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 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.

  1. canvas.on('object:modified', function (e) {
  2. var obj = e.target;
  3. var rect = obj.getBoundingRect();
  4.  
  5. if (rect.left < 0
  6. || rect.top < 0
  7. || rect.left + rect.width > canvas.getWidth()
  8. || rect.top + rect.height > canvas.getHeight()) {
  9. if (obj.getAngle() != obj.originalState.angle) {
  10. obj.setAngle(obj.originalState.angle);
  11. }
  12. else {
  13. obj.setTop(obj.originalState.top);
  14. obj.setLeft(obj.originalState.left);
  15. obj.setScaleX(obj.originalState.scaleX);
  16. obj.setScaleY(obj.originalState.scaleY);
  17. }
  18. obj.setCoords();
  19. }
  20. });

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:

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.

  1. var name = "Bob's Burgers";
  2. var address = "2343 Main";
  3. var phone = "111-222-3333";
  4.  
  5. var text = new fabric.Text(name + '\n' + address + '\n' + phone, {
  6. fontFamily: fontFamily,
  7. fontSize: fontSize,
  8. shadow: textShadow
  9. });
  10.  
  11. var rect = new fabric.Rect({
  12. fill: rectFill,
  13. stroke: rectStroke,
  14. width: 150,
  15. height: 250
  16. });
  17.  
  18. var group = new fabric.Group([rect, text], {
  19. left: 150,
  20. top: 100
  21. });
  22.  
  23. canvas.add(group);
  24. }

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:

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.

  1. var group = new fabric.Group([rect, text], {
  2. left: 150,
  3. top: 100,
  4. lockUniScaling: true
  5. });

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:

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.

  1. var group = new fabric.Group([rect, text], {
  2. left: 150,
  3. top: 100,
  4. lockUniScaling: true,
  5. borderColor: '9DD290',
  6. cornerColor: '1A7206',
  7. cornerSize: 6,
  8. transparentCorners: false
  9. });

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:

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.

  1. var background = new fabric.Rect({
  2. left: 0,
  3. top: 0,
  4. stroke: 'White',
  5. width: canvasWidth,
  6. height: canvasHeight,
  7. scaleX: 1,
  8. scaleY: 1,
  9. selectable: false,
  10. zoomScale: 1,
  11. isBackground: true
  12. });
  13. canvas.add(background);
  14. fabric.util.loadImage('background.png', function (img) {
  15. background.setPatternFill({
  16. source: img,
  17. repeat: 'repeat'
  18. });
  19. canvas.renderAll();
  20. });

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.

  1. $("body").mousewheel(function (event, delta) {
  2. var mousePageX = event.pageX;
  3. var mousePageY = event.pageY;
  4.  
  5. var offset = $("#myCanvas").offset();
  6. var canvasX = offset.left;
  7. var canvasY = offset.top;
  8.  
  9. // Ignore if mouse is not on canvas
  10. if (mousePageX < canvasX || mousePageY < canvasY || mousePageX > (canvasX + canvas.width)
  11. || mousePageY > (canvasY + canvas.height)) {
  12. return;
  13. }
  14.  
  15. // Ignore if mouse is not on background
  16. var background = getBackground();
  17.  
  18. var mouseOffsetX = event.offsetX;
  19. var mouseOffsetY = event.offsetY;
  20.  
  21. if (mouseOffsetX < background.left || mouseOffsetX > background.left + background.currentWidth
  22. || mouseOffsetY < background.top || mouseOffsetY > background.top + background.currentHeight) {
  23. return;
  24. }
  25.  
  26. var scaleFactor = 1.1;
  27. var change = (delta > 0) ? scaleFactor : (1 / scaleFactor);
  28.  
  29. // Limit zooming out
  30. var newZoomScale = zoomScale * change;
  31. if (newZoomScale < 1) {
  32. return;
  33. }
  34. zoomScale = newZoomScale;
  35.  
  36. var backgroundWidthOrig = (background.width * background.scaleX);
  37. var backgroundHeightOrig = (background.height * background.scaleY);
  38.  
  39. // Scale objects
  40. var objects = canvas.getObjects();
  41. for (var i in objects) {
  42. var obj = objects[i];
  43.  
  44. if (obj.zoomScale != undefined) {
  45. obj.scaleX = obj.zoomScale * zoomScale;
  46. obj.scaleY = obj.zoomScale * zoomScale;
  47. }
  48.  
  49. obj.left = obj.left * change;
  50. obj.top = obj.top * change;
  51. obj.setCoords();
  52. }
  53.  
  54. var backgroundWidthNew = (background.width * background.scaleX);
  55. var backgroundHeightNew = (background.height * background.scaleY);
  56.  
  57. var backgroundWidthDelta = backgroundWidthNew - backgroundWidthOrig;
  58. var backgroundHeightDelta = backgroundHeightNew - backgroundHeightOrig;
  59.  
  60. // Shift objects
  61. for (var i in objects) {
  62. var obj = objects[i];
  63. obj.left -= (backgroundWidthDelta / 2);
  64. obj.top -= (backgroundHeightDelta / 2);
  65. obj.setCoords();
  66. }
  67.  
  68. canvas.renderAll();
  69.  
  70. event.stopPropagation();
  71. event.preventDefault();
  72. });

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:

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.

  1. $('canvas').mousedown(function (e) {
  2. mouseXOrig = e.offsetX;
  3. mouseYOrig = e.offsetY;
  4.  
  5. // Ignore if mouse is not on background
  6. var background = getBackground();
  7. if (mouseXOrig < background.left
  8. || mouseXOrig > background.left + (background.width * zoomScale)
  9. || mouseYOrig < background.top
  10. || mouseYOrig > background.top + (background.height * zoomScale)) {
  11. return;
  12. }
  13.  
  14. var activeObject = canvas.getActiveObject();
  15. if (activeObject != undefined) {
  16. canvas.bringToFront(activeObject);
  17. return;
  18. }
  19.  
  20. mouseDownOutsideObject = true;
  21.  
  22. var objects = canvas.getObjects();
  23.  
  24. for (var i = 0; i < objects.length; i++) {
  25. mouseXStart[i] = objects[i].left;
  26. mouseYStart[i] = objects[i].top;
  27. }
  28. });

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.

  1. $('canvas').mousemove(function (e) {
  2. if (mouseDownOutsideObject) {
  3. var background = getBackground();
  4. background.selectable = true;
  5.  
  6. var mouseXNew = e.pageX - $(this).offset().left;
  7. var mouseYNew = e.pageY - $(this).offset().top;
  8.  
  9. var mouseXDelta = (mouseXNew - mouseXOrig);
  10. var mouseYDelta = (mouseYNew - mouseYOrig);
  11.  
  12. var borderWidth = 200;
  13.  
  14. if (background.getLeft() + mouseXDelta <= borderWidth
  15. && background.getTop() + mouseYDelta <= borderWidth
  16. && background.getLeft() + background.getWidth() + mouseXDelta >= canvas.getWidth() - borderWidth
  17. && background.getTop() + background.getHeight() + mouseYDelta >= canvas.getHeight() - borderWidth) {
  18. var objects = canvas.getObjects();
  19.  
  20. for (var i = 0; i < objects.length; i++) {
  21. objects[i].setLeft(mouseXDelta + mouseXStart[i]);
  22. objects[i].setTop(mouseYDelta + mouseYStart[i]);
  23. objects[i].setCoords();
  24. }
  25.  
  26. canvas.renderAll();
  27. }
  28.  
  29. background.selectable = false;
  30. }
  31. });

The last bit of code we'll show will be the mouse up handler, which simple sets the background selectable attribute back to false.

  1. canvas.on('mouse:up', function (e) {
  2. mouseDownOutsideObject = false;
  3. getBackground().selectable = false;
  4. });

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:

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:

 

Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.