The transformation matrix in fabric.js

Luiz Eduardo Zappa
14 min readJan 8, 2023

--

Recently I’m using the excellent fabric.js library for a side project. I came across the transformation matrix. The purpose of this article is to detail how the transformation matrix works and especially how did we arrive at the transformation matrix, why is it in the format we know? Although this is an explanation in the context of fabric.js, it’s extensible to other contexts (perhaps with slight implementation differences).

Since not everyone has a background in mathematics (it’s actually high school math, but some people might barely remember), I will start by explaining all the fundamentals so that any reader can follow this article. Feel free to skip some part if you find it too basic.

Base vectors of a Cartesian plane

We already saw what Cartesian planes are in the previous article (if you are not familiar whit this concept and with vectors, I strongly recommend check out this article before continuing reading).

In a Cartesian coordiante system there are standard unit vectors in the direction of the X and Y axis. The unit vector in the X axis is represented as î (hat i) and the unit vector in Y axis as ĵ (hat j).

Standard unit vectors

Unit vectors: vectores that have a length of 1. By convention every unit vector is represented with a hat. For instance: â, î,…

The unit vectors î and ĵ are the bases of our coordinate sytem, and we can represent any vector in terms of them.

Let’s look at an example. Here we will use the top left origin, that is, the positive diretion of the Y axis is downards, like the canvas in the fabric.js. Representing the point (2, 2) in this plane:

Plot point (2, 2)

Now let’s draw a vector from the origin point (0, 0) of our plane to the point (2, 2):

We can represent the vector [2, 2] in terms of the base of our plane:

Vector [2, 2] in terms of î and ĵ

That is, the vector [2, 2] is a linear combination of two î vectors and two ĵ vectors. Any vector in this plane can be represented as a linear combination of î and ĵ (the basis of our plane).

Linear transformation

A central concept for understading the transformation matrix is linear transformation:

Linear transformation alters our plane such that parallel lines stay parallel and equally spaced, and the origin doesn’t move.

Let’s see in practice a linear transformation applied to our vector [2, 2]. For this, I created an animation using fabric.js (you can play around here). When performing a rotation transformation with shear, we can see after transformation our vector remains equal to two steps in the direction of the transformed X axis (two î vectors) and two steps in the direction of the transformed Y (two ĵ vectors).

Linear transformation in action

The blue grids (dark and light) represent our transformed plane, and the light gray grids represent our untransformed plane. The values of î, ĵ, v vectors are calculated relative to the untransformed plane. Notice that the values of these vectors change relative to the untransformed plane. For example, the vector î before transformation was [1 0] and after transformation is [0.9397 -0.342]. But even with the transformation, the vector v remained equal to 2î + 2ĵ and the origin remained the same. That is, this is a linear transformation.

Linear transformation with matrices

As we have seen, to describe a linear transformation we only need to keep track where the vectors î and 2x2 transformation matrix go and calculate the coordinates with respect to the untransformed plane.

The matrix is nothing more than packing these two vectors together to make the calculation easier. It’s called a two-by-two matrix that describes a 2D linear transformation. The first column of this matrix describes the vector î after the transformation and the second column of the matrix describes the vector ĵ after the transformation:

2x2 transformation matrix

If we want to find out where a vector is located after the transformation, all we need to do is multiply our original vector by the 2x2 transformation matrix:

Applying transformation matrix to original vector to find final position

Note that we find the final position of the vector after the transformation without having to work with the Cartesian plane. This makes the calculation process much easier and faster. In an animated way, we can map the calculation from the Cartesian plane to matrices as follows:

Cartesian plane calculation vs matrix-wise

If you need a matrices review, I recommend these videos:

Let us now see some specific matrices and their respective transformations.

Transformation: scaling

When applying a scale on the X axis and a scale on the Y axis, the unit vectors î and ĵ after the transformation will be:

Scaling transformation

We can pack this transformation into a matrix that is called a scaling matrix:

Scaling matrix

For example, applying scaleX = 0.8 and scaleY = 0.8:

Scaling transformation in fabric.js

Transformation: flip

This operation is similar to scaling. We just need to invert one of the coordinates for horizontal or vertical inversion (or both) to reflect about the origin.

Flip transformation

The flip in the transformation matrix boils down to the inversion of the sign on the respective scale axis:

Flip matrix

For instance, applying flipY:

Flip transformation in fabric.js

Transformation: skewing

First, let’s understand how shear occurs. By performing a shear along the X axis (skewX) from an alpha angle at the point(x, y), our transformed point will be offset by a delta X value:

skewX of alpha

To calculate the value of delta X, just a little geometry:

Calculation of delta X

We know the tangent of alpha (= skewX) and the value of y, we can write delta X in terms of both:

That is, a shear transform ( skewX) on the X axis applied to a point(x, y) will transform it into:

(x, y) = (x + y * Tan(skewX), y)

The deduction of the shear formula along the Y axis is the same. In order to not get too long, I won’t disassemble it, but the result after a skewY transformation at the point(x, y) will be:

(x, y) = (x, y + x * Tan(skewY))

The more attentive reader may be wondering what happens if we apply skewX and skewY at the same time. We have to determine an order of operation, otherwise we will have a circular reference. In the case of the canvas (and fabric.js), first we apply the skewY and then the skewX:

So, we can write the shear transformation in the unit vectors as:

Skew transformation

We can pack this transformation into a matrix that is called shear matrix:

Shear matrix

For example, applying skewX = -20 and skewY = -10:

Shear transformation in fabric.js

Transformation: rotation

As expected, the transformation due to a rotation is a function of the angle of the rotation. The calculation of vectors values is found through trigonometry:

Rotation transformation

We can pack this transformation into a matrix that is called rotation matrix:

Rotation matrix

Transformation: translate

Translation moves our coordinate system to another location, i.e. changes the origin. And by definition, by changing the origin, it’s not a linear transformation, so we cannot describe this transformation in our linear transformation matrix.

To represent this transformation, we create a vector from the origin of the plane to the new origin of the translated plane:

Vector pointing to the translated plane

It may seem a little strange that the vectors î and ĵ continue with the values [1 0] and [0 1], but here we are talking about different planes.

And how do we represent translation in matrix format? We can combine our two-by-two linear transformation matrix with the translation into a single matrix:

Translation matrix

Notice that an extra line appeared at the end of our matrix, we have increased the matrix to make it compatible with translation operations.

Now if we are going to multiply a vector by this transformation matrix, we must also add one more line in the matrix that represents this vector to allow the matrix operations:

Composing multiple transformations

We can compose a serie of transformations in sequence. For example, a scaling, then a rotation and finally a shear: (note that we are now working with the augmented transformation matrix to accommodate the translation)

Transformation matrices

For this, we multiply each of the transformation matrices to arrive at the final transformation matrix:

Final transformation matrix

The matrix multiplication order makes difference in the final result. That is, matrix A times matrix B is different from matrix B times matrix A.

The order of multiplication is important, the first transformation start rightmost. For example, if we reversed the order of multiplication, the final result would be different:

Different results when we change the order of multiplication

But why when we do something like code below in fabric.js the end result is always the same?

const rect1= new fabric.Rect({ 
width: 100,
height: 100,
scaleX: 1,
scaleY: 1,
skewX: 1,
skewY: 1,
angle: 0
});

const rect2= new fabric.Rect({
width: 100,
height: 100,
scaleX: 1,
scaleY: 1,
skewX: 1,
skewY: 1,
angle: 0
});

canvas.add(rect1, rect2);

rect1.set({scaleX: 2});
rect1.set({angle: 30});
rect1.set({skewX: 45});

rect2.set({skewX: 45});
rect2.set({angle: 30});
rect2.set({scaleX: 2});

canvas.renderAll();

We are applying the transformations in different order, but the end result is the same. The reason is that there is a fixed transformation order for objects, regardless of whether we set one of the transformations before or after. The order used internally by fabric.js is as follows:

Fabric.js transformation order

First apply skew, then scaling, rotation and finally translation. We can change this order when working with groups, we’ll see later in this article.

Undoing transformations

We can undo certain transformations directly in the final transformation matrix. For this we use a property of matrices which is: the multiplication of matrix (M) by its inverse (M-¹) produces an identity matrix (I). Note that the order of multiplication in this case produces the same result:

Matrix times its inverse

The identity matrix does not change the result of a matrix multiplication. That is, matrix A multiplied by identity matrix (I) remains matrix A: (again the order of multiplication is indifferent)

Matrix times identity

So if we have the following transformation:

And we want to remove the scale transformation, just multiply by the inverse of the scale matrix:

Removing scale

One important thing is that we must multiply the inverse of the transformation matrix next to the original matrix. For example, if we wanted to remove skew, the first case is incorrect and the second correct:

Removing skew

Fabric.js has functions to assist with these operations:

  • fabric.util.invertTransform: calculates the inverse of a transformation matrix.
  • fabric.util.multiplyTransformMatrices: multiplies two transformation matrices.
  • fabric.util.transformPoint: apply transform to a point.

As we saw, for transformations in a single object fabric.js abstracts the entire transformation process. But when we work with different planes, knowing these operations is important.

The transformation matrix in fabric.js

The transformation matrix in fabric.js is represented in an array of length 6 as follows:

There are a few ways to calculate the transformation matrix of an object in fabric.js. It depends on which plane we are referring to:

const rect = new fabric.Rect({ 
width: 100,
height: 100,
scaleX: 1,
scaleY: 1,
skewX: 1,
skewY: 1,
angle: 0
});

// Transformation matrix in the object plane
rect.calcOwnMatrix();
// Transformation matrix from the canvas plane to the object plane
rect.calcTransformMatrix();

calcOwnMatrix: returns the transformation matrix in the object plane, without considering external transformations.

calcTransformMatrix: returns the transformation matrix from the canvas plane to the object plane. If the object is inside a group, it will consider these transformations as well.

Later on we will see in more details the multiple planes that exist on the canvas and these concepts will become clearer.

Let’s see the transformation matrix in action in fabric.js:

The different Cartesian planes in fabric.js

We will work with different Cartesian planes and see how the transformation matrix allows us to locate points on these different planes.

Let’s start with an example. Adding a rectangle to the canvas:

const rect = new fabric.Rect({
width: 50,
height: 50,
fill: 'red',
strokeWidth: 0,
originX: 'left',
originY: 'top',
left: 25,
top: 25
});
canvas.add(rect);

Our canvas is a Cartesian plane (origin is top left, so the Y axis grows downwards). When we add a rectangle, behind the scene we are defining a new plane and set of 4 points inside it that form our rectangle:

Canvas plane and rect plane

Now we have two planes:

  • The canvas plane (has gray arrows as axes)
  • The rectangle plane (orange arrows as axes)

Note that the plane of an object has its center as the origin, so it is located at the point (50, 50). This is a fabricj.js design choice. Each of these planes has a transformation matrix that allow us to apply the existing transformations in that plane to a point. If we calculate the transformation matrix of the object and the canvas:

// Canvas plane tranformation matrix
canvas.viewportTransform
// [1, 0, 0, 1, 0, 0]

// Object plane tranformation matrix
rect.calcTransformMatrix();
// [1, 0, 0, 1, 50, 50]

As we can see, in the plane of the object, translateX and translateY point to the center of the object which is the center of the plane.

In the previous image we are observing the points in relation to the canvas plane. What it we observed the same points but in relation to the plane of the rectangle? The representation would look like this:

The points relative to the plane of the rectangle

Let’s apply what we learn. I have two rectangles with two different transformations (one with scale and one with rotation):

const rect_1 = new fabric.Rect({
width: 50,
height: 50,
fill: 'red',
left: 10,
top: 10,
scaleX: 3
});

const rect_2 = new fabric.Rect({
width: 50,
height: 50,
fill: 'red',
left: 10,
top: 100,
angle: 30
});

canvas.add(rect_1, rect_2);

I want to create a line from the center to the top right corner. How could I do this using transformation matrix?

Our goal

Try to think a little before looking at the solution…

Let’s look at one of the ways to achieve this. We first look at the untransformed plane of the object. We want to draw a vector from the origin of this plane, point(0, 0), to the point(25, -25):

Object’s untransformed plane

When we are creating the line, we are on the canvas plane. That is, we need to find these points on the canvas plane. To do this, just multiply by the transformation matrix of each of the rectangles:

// Points of interest in the untransformed plane of objects
const centerPoint = new fabric.Point(0, 0),
cornerPoint = new fabric.Point(25, -25);

// The same points of interest now on the canvas plane
// As each rectangle has a different transformation,
// to map to the canvas plane we need to multiply by
// each of the transformation matrices.
const rect_1Center = centerPoint.transform(rect_1.calcTransformMatrix()),
rect_1CornerPoint = cornerPoint.transform(rect_1.calcTransformMatrix());

const rect_2Center = centerPoint.transform(rect_2.calcTransformMatrix()),
rect_2CornerPoint = cornerPoint.transform(rect_2.calcTransformMatrix());

// Then we create the lines using the points on the canvas plane
const line_1 = new fabric.Line(
[
rect_1Center.x, // line origin.x
rect_1Center.y, // line origin.y
rect_1CornerPoint.x, // line destination.x
rect_1CornerPoint.y // line destination.y
],
{ stroke: 'blue', strokeWidth: 1, originX: 'center', originY: 'center'}
);

const line_2 = new fabric.Line(
[
rect_2Center.x,
rect_2Center.y,
rect_2CornerPoint.x,
rect_2CornerPoint.y
],
{ stroke: 'blue', strokeWidth: 1, originX: 'center', originY: 'center'}
);

canvas.add(line_1, line_2);

We also work with different planes when an object is inside a group. Each group represents a new plane and the transformation existing in the group is applied to all objects (or other groups) contained within it.

For example, let’s add two rectangles to a group and apply a scaleY equal to 2 to the group.

const rect_1 = new fabric.Rect({
width: 50,
height: 50,
fill: 'red',
left: 10,
top: 10,
scaleX: 3
});

const rect_2 = new fabric.Rect({
width: 50,
height: 50,
fill: 'red',
left: 10,
top: 100,
angle: 30
});

const group = new fabric.Group(
[rect_1, rect_2],
{
left: 30,
top: 100,
scaleY: 2
}
)

canvas.add(group);

Each of the rectangles and the group will have their respective transformation matrices ( calcOwnMatrix ), which not consider any external transformation (from another plane):

console.log(rect_1.calcOwnMatrix()); 
// [3, 0, 0, 1, 12.75, -54.333647796503186]
console.log(rect_2.calcOwnMatrix());
// [0.8660254037844387, 0.49999999999999994, -0.49999999999999994, 0.8660254037844387, -54.416352203496814, 45]
console.log(group.calcOwnMatrix());
// [1, 0, 0, 2, 119.25, 259.6672955930064]

To find the total existing transformation in each of the rectangles, we must multiply all the transformation matrices that affect them. That is, we must multiply the transformation matrix of the group and that of the rectangle itself:

console.log(fabric.util.multiplyTransformMatrices(
group.calcOwnMatrix(),
rect_1.calcOwnMatrix()
));
// [3, 0, 0, 2, 132, 151]

console.log(fabric.util.multiplyTransformMatrices(
group.calcOwnMatrix(),
rect_2.calcOwnMatrix()
));
// [0.8660254037844387, 0.9999999999999999, -0.49999999999999994, 1.7320508075688774, 64.83364779650319, 349.6672955930064]

A point of attention when multiplying the transformation matrices is in the order of multiplication. The first transformation must be rightmost, so the last argument is the rectangle transformation matrix since it is the first transformation. This process of multiplying transformation matrices would get tedious if we had a lot of nested groups. Fabric.js has a method that calculate this for us:

console.log(rect_1.calcTransformMatrix());
// [3, 0, 0, 2, 132, 151]

console.log(rect_2.calcTransformMatrix());
// [0.8660254037844387, 0.9999999999999999, -0.49999999999999994, 1.7320508075688774, 64.83364779650319, 349.6672955930064]

Altough fabric.js provides us with many out-of-the-box resources, knowing how to work with transformation matrices allows us to develop interesting things.

Below are some video references on the subject that present the topic in a very visual way:

Matrices and Transformations — Math for Gamedev

Linear transformation and matrices

Matrix multiplication as compostion

--

--

Luiz Eduardo Zappa

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