A Better Line — Custom Fabric.js line with controls

OrderStack
6 min readMay 14, 2022

Introduction

Fabric.js — a powerful Javascript library that makes working with HTML5 canvas a breeze. Fabric provides a missing object model for canvas, as well as an SVG parser, layer of interactivity, and a whole suite of other indispensable tools. It is a fully open-source project, licensed under MIT, with many contributions. This Article is based on Fabric.js version 4.6.0

Drawbacks

If you have ever used the Fabric.js line you would know it acts like any other Fabric.js shape. This comes with one major drawback, i.e there’s no way to edit the line without skewing it which results in undesirable behavior.

To overcome this drawback we implemented line controls that helped us control the length of the line without skewing the line. This can also act as a building block for creating lines with arrowheads, where the skewing becomes more apparent.

The image below represents a basic line drawn using the kitchensink demo from the Fabric.js website

Normal Fabric.js Line

Now there’s no way to increase the length of the line without skewing the original line. Like in the image shown below.

Skewed Fabric.js Line

This issue becomes more apparent with an arrowhead line. As shown in the images below

Normal Arrowhead Line
Skewed Arrowhead Line

An Example of such arrowhead line implementation is given at https://www.thirdrocktechkno.com/blog/draw-an-arrow-using-html-5-canvas-and-fabricjs/

Solution

In this article, we will go over our solution to the aforementioned drawback. Before starting it’s assumed that you have a basic knowledge about Fabric.js and their existing line implementation.

The basic user experience we went with for line creation is clicking the “Draw Line” button to enter drawing mode. The user can then interact with the canvas to draw the element. The user has two options,

  • To leave the mouse(Mouse Up) at the very position the user clicked the mouse, which will result in the line being created with a default length(300px, set in code right now).
  • The other option for the user is to drag the mouse, with the help of the mouse-move event the attributes for the line are updated.

At the end of either of these interactions, two-line controls appear at both the ends of the line

We went for a class-based structure. As represented in the diagram below

LineShape

In this class drawShape, updateShape, and completeShape functions are an extension of similarly named functions in LineManager class. The most important function to note in this class is getLineCoords this function determines where the line controls will be placed

The groupedObject is nothing but a line object that has been grouped. The purpose of this is to write logic that would be similar irrespective of it being a normal line or an arrowhead line (To create an arrowhead we can use a fabric line and fabric triangle shape and then group them) as all the calculations will be for the grouped object. Using this function precise start and endpoints are calculated based on aCoords of the groupedObject. The sin and cos calculations are to determine the perfect midpoint based on the angle of the line.

Please note, setCoords() needs to be called every time the function is invoked because this function recalculates the aCoords and oCoords.

The events to note in this class are “moving” and ”rotating” as shown in the snipped below.

For either of the above-mentioned events, the position of the line control has to update along with the line. This is achieved by calling the updateLineControl function which in turn calls getLineCoords to get the coordinates for line controls here updateDotCoordinates is invoked, this function is defined on the prototype of the fabric Circle shape which is defined in LineControlManager class. Fabric.js Circle shape is what we have used to create the fabric controls. This function updates the position of the line controls.

This is defined by whatever shape is used as control (In our case circle)

LineControlManager

We create two fabric Circle elements namely startDot and endDot, these are the controls for our line. We have a variable called followerObject, which stores the fabric instance for the element that the control dots need to update. An important function to note here is the showControls function, the parameters for which contain the coordinates where the line controls need to be placed.

For both the controls “this.followerObject.strokeWidth / 2” is added to the top. This is to get the exact middle point of the thickness of the line. bringToFront() is called so that no other element is drawn over the control dots. When moving either of the controls the line (this.followerObject) needs to update. Let us have a look at the code snippet that gets executed when the startDot moves (Similar to endDot).

getCenterPoint() is used to get the x and y coordinates for the center point of the control dots, Furthermore this.followerObject.strokeWidth / 2 is subtracted from the Y values. These details are then used to call updateLineOnLineControlMove(). This function is attached to the prototype of the fabric Group object. Which is what is used to create our line. Let us have a look at updateLineOnLineControlMove() next as this function is what updates our line object.

updateLineOnLineControlMove()

The first thing we do is get the angle of the line based on the start and end x, and y coordinates. Then we calculate the angles where the object locks (Every 45-degree interval) using the logic that Fabric.js uses To calculate the actual distance between two points we use hypotenuse.

The image shown below is the way fabric manages angles

Fabric.js Angle Representation
  • “this” represents the instance, i.e the groupedObject from our LineShape class.
  • this._objects[0] is the actual Fabric.js line instance.

It has been given left of 0 — this.width / 2 because once an object is grouped the top left of that object becomes relative to the center of the parent group object. Thus half of the width of the group object needs to be offset for the line to start from the start of the group object.

If the control that’s being moved is the end dot then the values of the start dots are just assigned to the parent group object including the hypotenuse width that was calculated.

Because of the snapping logic, we started facing an issue, whenever the line entered the snapping angle threshold the start points of the lines would stop updating and the line would be weirdly aligned. To tackle this scenario whenever the snap angle is in the multiple of 45 degrees the start and end points need to be calculated.

In the case of the startDot move, we update the value based on the start dot move.

Wrapping Up For Now

In this article, we learned how to create a line that can be manipulated using control dots. Hopefully, Fabric.js will improve on the current line implementation to provide a more intuitive way to interact with lines.

You can interact with the codepen embedded below. Thanks for reading!

OrderStack — !(Just another IT company)

At OrderStack, we bring to you an augmented tech team that sells engineering as a service! We work with start-ups to build bespoke MVPs that are superior & result-driven.

If you or your team is facing engineering challenges and need some expert advice, feel free to reach out to us at contact@orderstack.xyz.

--

--

OrderStack

At OrderStack, we bring to you an augmented tech team that sells engineering as a service! We work with start-ups to build bespoke MVPs.