Display angle while rotating an object in fabric.js (introducing vectors)

Luiz Eduardo Zappa
11 min readDec 11, 2022

--

In this article we’ll enrich what we have developed in the last article (where we modified the cursor and the rotation icon). The purpose is to display the angle of the object as it’s rotated. For this, I will briefly present a review of vectors, which greatly facilitate development in fabric.js. Based on what we’ll learn in theory, we’ll develope this feature. In the end we will have:

Display angle while rotating

How to represent a point?

First of all we must understand how to represent a point in 2D space and what a Cartesian system is. I like the battleship analogy. How do we refer to the position of a ship?

Cartesian plane

We have two axes (I’ll call the horizontal as X an the vertical as Y) and we identify the ship’s position based on them. For example, there is part of a ship at point (2,4), the red dot. We can interpret this representation as starting from origin point (0,0), we move 2 squares in the direction of the X axis and 4 squares in the direction of the Y axis.

The Cartesian system is represented by two perpendicular lines (coordinate axis) and the point where they meet is the origin, point (0, 0).

What is a vector?

Simplistically, we can say that the vector is an arrow that starts from an origin (the tail) to a destinatin (the head). Using this definition, we can represent the position of the ship based on a vector:

Vector representation

The coordinates of a vector is a pair of numbers that “give instructions” for how to get from the origin of the vector to its head. The first number (2) represents how far to walk along the X-axis, and the second number (4) represents how far to walk along the Y-axis.

The convention for distinguishing vectors from points is to represent vectors vertically with square brackets and points horizontally with parentheses:

Point and vector representations

We represent the vectors with letters with an arrow on top:

The size of a vector

An important property of vectors is their size. Still in the previous example, how we calculate the distance from the origin of the Cartesian plane to the red point. The answer to this question is the size of the orange vector.

To calculate the distance, d, just apply the Pythagorean formula:

Substituting the values we have:

So:

The size of the vector

The size of the vector is also called the magnitude.

Operations with vectors: addition and subtraction

Let’s look at the basic operations with vectors. If we want to leave the red point and go to the green one, how would we represent this displacement with vectors?

An important feature of vectors is that they can be shifted to any part of our Cartesian plane. To represent this displacement (from red dot to the green one), we will consider the red point as our origin, and move four squares backwards on the X axis and one square upwards on the Y axis. That is, the representation of our vector would be:

And how would we represent the vector starting from the origin of the Cartesian system, point (0, 0), to our final point (the green one)? We would just move two squares backwards along the X axis and five squares up along the Y axis. The representation would be the yellow vector:

We have just visualized how to operate on a vector addition. The yellow vector is the sum of the orange and purple vectors:

We can generalize vector addition as:

Sum of vectors

Let’s see how this operation is in fabric.js:

const orangeVector = new fabric.Point(2, 4),
origin = new fabric.Point(2, 4),
destination = new fabric.Point(-2, 5),
purpleVector = fabric.util.createVector(origin, destination);

console.log(purpleVector);
// Point {x: -4, y: 1 }

// Addition
const yellowVector = orangeVector.add(purpleVector);

console.log(yellowVector);
// Point {x: -2, y: 5 }

The createVector function takes the tail (origin) and head (destination) of the vector as arguments. Note that for the orange vector this function was not used, because as the origin is the point(0, 0), the vector will be numerically equal to the destination point (head). So, for simplicity we already assign directly to the point value instead of doing fabric.util.createVector(new fabric.Point(0, 0), new fabric.Point(2, 4)) which result in the same value.

The cool thing about this operation is that we can locate our end point (the green one), just by making relative moviments between vectors (the purple vector has the red point as its origin. This vector does not know the origin of our Cartesian plane). We could perform various “relative moves” with vectors and still find our final position relative to the origin of the Cartesian plane:

Knowing the addition operation, the subtraction is straightforward. For example, if we wanted to subtract the purple vector [-4 1]from the orange one, it would be enough to add the inverse of the purple vector [4 -1]:

Now let’s look at the subtraction operation in fabric.js:

const orangeVector = new fabric.Point(2, 4),
origin = new fabric.Point(2, 4),
destination = new fabric.Point(-2, 5),
purpleVector = fabric.util.createVector(origin, destination);

console.log(purpleVector);
// Point {x: -4, y: 1 }

// Subtraction
const yellowVector = orangeVector.subtract(purpleVector);

console.log(yellowVector);
// Point {x: 6, y: 3 }

Operations with vectors: multiplication and division

Multiplication and division operations are best understood by looking at the Cartesian plane. What would happen if we multiplied the vector v by two?

We will have a vector that will be twice the size of the original vector:

The logic for the division is the same. If we divided the vector by two, we would have a new vector that is half of the original vector:

That is, we can generalize these operations as:

Scaling a vector

These operations are commonly known as scaling.

Let’s look at the two operations in fabric.js:

const orangeVector = new fabric.Point(2, 4);

/* Multiplication */
const twoTimesOrange = orangeVector.scalarMultiply(2);
console.log(twoTimesOrange);
// Point {x: 4, y: 8}

/* Division */
const halfOrange = orangeVector.scalarDivide(2);
console.log(halfOrange);
// Point {x: 1, y: 2 }

Attention, in fabric.js there is another multiplication and division methods called multiply and divide. These types of multiplication and division are different, it’s a Hadamard product, that is, the element X will be multiplied (or divided) by the corresponding X, and the same goes for the Y. This operation does not have the same result (and interpretation) as what we saw above. Let’s see an example of it:

const v1 = new fabric.Point(2, 4),
v2 = new fabric.Point (4, 5);

/* Multiplication */
const v1Timesv2 = v1.multiply(v2);
console.log(v1Timesv2);
// Point {x: 8, y: 20}

/* Division */
const v1Dividev2 = v1.divide(v2);
console.log(v1Dividev2);
// Point {x: 0.5, y: 0.8}

Rotating vectors

The orange vector is rotated 63° degrees counterclockwise with respect to the X axis. And if we wanted to rotate it another 30° clockwise, how would we do it?

In fabric.js, counterclockwise rotations have angles with positive values and clockwise rotations have angles with negative values.

Mathematically, to find the rotation we should use the following formula:

If you are interested in the mathematical proof of this formula, checkout this article.

As you can see, the formula is not so friendly. But when we encapsulate it in a function, it becomes mucho more intelligible.

For example, let’s rotate this vector in fabric.js. As you can see from the code, it’s pretty easy to keep track of what’s going on.

const origin = new fabric.Point(0, 0),
destination = new fabric.Point(2, 4),
rotationAngle = fabric.util.degreesToRadians(-30);

const initialVector = fabric.util.createVector(origin, destination);
const rotatedVector = initialVector.rotate(rotationAngle);

console.log(rotatedVector);
// Point {x: 3.732050807568877, y: 2.464101615137755}

When working with angles, most functions use radians instead of degrees (which we are more used to). The same goes to fabric.js. Because of this, fabric.js has two help functions that convert between these two units of measurement: fabric.util.degreesToRadians and fabric.util.radiansToDegrees.

Implementing the feature

Okay, enought theory, let’s get to practice and see how to implement this new feature using vectors.

First of all let’s create the angle display and then we’ll use vectors to position it.

Let’s create two more properties on our state object to store the current angle and wheter we’re in the middle of a rotation:

const state = {
lastAngleRotation: null,
currentAngle: null,
isRotating: false,
}

To update these properties, we will extend the listener for when a rotation event is occurring. Also, when mouse up, we indicate that the rotating is over.

canvas.on('object:rotating', function (e) {
// ...
state.isRotating = true;
state.currentAngle = obj.angle;
});

canvas.on('mouse:up', function(opt) {
state.isRotating = false;
})

We could create the display using fabric.js objects (such as Text and the background with a Rect). However, this way the object’s controls will be in front of our display. Because of this, we will draw directly on the canvas:

canvas.on('after:render', function(opt) {
state.isRotating && renderRotateLabel(opt.ctx, canvas);
})

Every time we are in the middle of a rotation we will render the display on the canvas. Let’s create the function that draws the display. It receives as an argument the context of the canvas (which has the methods to draw on it) and the reference to the canvas itself.

function renderRotateLabel(ctx, canvas) {
const angleText = `${state.currentAngle.toFixed(0)}°`,
borderRadius = 5,
rectWidth = 32,
rectHeight = 19,
textWidth = 6.01 * angleText.length - 2.317;

const pos = new fabric.Point(20, 20);

ctx.save();
ctx.translate(pos.x, pos.y);
ctx.beginPath();
ctx.fillStyle = "rgba(37,38,39,0.9)";
ctx.roundRect(0, 0, rectWidth, rectHeight, borderRadius);
ctx.fill();
ctx.font = "400 13px serif"
ctx.fillStyle = "hsla(0,0%, 100%, 0.9)";
ctx.fillText(angleText, rectWidth/2 - textWidth/2, rectHeight/2 + 4)
ctx.restore();
}

Let’s understand this function. First we use a template literal to create the text that will be displayed. We also define the dimensions of the rectangle that will be the background of our display, the radius of its border and finally the size of the text that will be rendered.

In the pos constant we define the position that will be rendered.

The ctx.save() function stores the drawing settings on the canvas so we can restore them later.

The ctx.translate function moves the position to the one defined in the pos constant.

Then we start our drawing with ctx.beginPath(), define the color of the fill property and draw a rectangle with rounded edges at position (0, 0) which is the position defined in pos, since we translated to this position.

The ctx.fill() paints the rectangle.

Then we define the properties of the text that will be rendered. The text position is centered on the rectangle with rectWidth/2 — textWidth/2 and rectHeight/2 + 4.

Finally, we restore the previous properties with ctx.restore().

We get the following:

Now let’s position this display according to the mouse position. I want it to be 40 pixels away from the mouse cursor at a 30 degrees clockwise angle.

We need to store the cursor position, for that we will create a new property in the state object. And inside rotation listener, we’ll update this property:

const state = {
// ...
cursorPos: new fabric.Point()
}

canvas.on('object:rotating', function (e) {
// ...
state.cursorPos.x = e.pointer.x;
state.cursorPos.y = e.pointer.y;
});

Let’s create a function just to render the placement. Then we apply the same logic to our display.

function renderVector(ctx, canvas) {
const pos = state.cursorPos.add(
new fabric.Point(40, 0)
);
ctx.save();
ctx.beginPath();
ctx.moveTo(state.cursorPos.x, state.cursorPos.y);
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
ctx.restore();
}

Let’s understand what’s going on. The canvas has the top left origin, so we can represent our cartesian plan as:

Starting from the origin of our canvas, the vector stored in the state.cursorPos property points to the mouse cursor. Then we add a vector that shifts 40 pixels to the right. This piece of code that does this:

//...
const pos = state.cursorPos.add(
new fabric.Point(40, 0)
);
//..

Let’s render this function we defined:

canvas.on("after:render", function (opt) {
state.isRotating && renderRotateLabel(opt.ctx, canvas);
state.isRotating && renderVector(opt.ctx, canvas);
});

The result will be this:

Now we need to rotate it 30 degrees clockwise. For this we do:

// ...
const pos = state.cursorPos.add(
new fabric.Point(40, 0)
.rotate(fabric.util.degreesToRadians(30))
);
// ...

The result will be this:

Let’s now use this placement for our display. For this, in the renderRotateLabel function we change the constant pos by:

// ...
const pos = state.cursorPos.add(
new fabric.Point(40, 0)
.rotate(fabric.util.degreesToRadians(30))
);
// ...

Okay, now our display is in the desired position:

But it’s still not cool. When the mouse leaves the canvas, the display also disappears. Let’s make it always visible on the screen even when the mouse leaves. For that, inside the renderRotateLabel function we change the ctx.translate to be limited to the viewport.

const { tl, br } = canvas.vptCoords;

// ...
ctx.translate(
Math.min(
Math.max(pos.x, tl.x),
br.x - rectWidth
),
Math.min(
Math.max(pos.y, tl.y),
br.y - rectHeight
)
);
// ...

Done, now the display does not leave the viewport:

--

--

Luiz Eduardo Zappa

Engineer breaking into the world of information technology 👨‍💻 I comment on what I'm developing on https://twitter.com/imluizzappa