Canvas 2D Context

Part 10. Transformations

See a canvas? Transform it beyond recognition!
Self-taught painter

Any CanvasRenderingContext2D instance has a current transformation matrix which is applied to the coordinate system of the canvas. A transformation matrix defines the relations between the initial device-independent canvas coordinate system and a new coordinate system used to display the contents of the canvas on the screen. In essence, transformations do not alter the intrinsic geometry of a graphical object: instead, they change coordinate systems. Transformations can be expressed in terms of simple transforms (translation, rotation and scaling), or follow the entries of an affine transformation matrix. A 3x3 transformation matrix has the following entries:

A transformation matrix can also be expressed as a vector [ a b c d e f ].

Translation

The e and f components of the matrix are used to define translation in the x and y directions, respectively. In this case the transformation matrix is equal to

To apply a transformation effect, the transform method with matrix entries as its parameters is invoked on the rendering context. In the example below the first text snippet is drawn in the original coordinate system. Then transform is called, matrix multiplication is performed and a new current matrix is established. This time the coordinate values (20, 20) of the text in the new coordinate system are mapped to the coordinate values (120, 120) in the original system: coordinates are said to be translated by 100 units in the x direction and 100 units in the y direction:

context.fillText("demo", 20, 20);
context.transform(1, 0, 0, 1, 100, 100);
context.fillText("demo", 20, 20);

The same effect can be achieved by invoking the translate(100, 100) convenience method:

context.fillText("demo", 20, 20);
context.translate(100, 100); // x and y
context.fillText("demo", 20, 20);

Scaling

To scale the coordinate system, the a and d components of the matrix are employed:

In the example below the transformation specified by means of the matrix scales the coordinates so that units in the new coordinate system is the same size as the sx and sy units in the previous one. Practically, it means that one unit in the x direction in the transformed system is equal to 0.2 in the original coordinate system, so the yellow rectangle is scaled down along the x axis. A unit in the new vertical dimension is equal to 1.2 in the original system, so the yellow rectangle is scaled up:

context.save();
context.fillStyle="crimson";
context.fillRect(10, 10, 200, 200);
context.restore();

context.save();
context.fillStyle="yellow";
// scale factor in the horizontal direction is 0.2; scale factor in the vertical direction is 1.2:
context.transform(0.2, 0, 0, 1.2, 0, 0);
context.fillRect(10, 10, 200, 200);
context.restore();

A simpler approach is to call the scale(x, y) method and pass scaling factors as its arguments:

context.scale(0.2, 1.2);

Rotation

Rotation is slightly more complex: it uses a, b, c and d matrix components as well as a couple of trigonometric functions:

The a parameter is a clockwise rotation angle expressed in radians. To demonstrate a rotation effect, we'll use an external image resource and call the drawImage method twice - first before the transformation, then after matrix multiplication. The rotation of the coordinate system is performed by the angle of 0.78 radians. There are 2π radians (approximately 6.28) in a full circle (360 degrees), so 180 degrees are equal to π (3.14) radians, 90 degrees are approximately 1.57 radians, and 45 degrees are equivalent to 0.785 radians.

var img=document.getElementById("imageSource");

context.save();
context.drawImage(img, 100, 10);
context.restore();

context.save();
context.transform(Math.cos(0.78), Math.sin(0.78), -Math.sin(0.78), Math.cos(0.78), 0, 0);
context.drawImage(img, 100, 10);
context.restore()

There's a more straightaway method which can be used to perform rotation: this is the rotate(angle) method accepting an angle in radians as its only argument:

var img=document.getElementById("imageSource");

context.save();
context.drawImage(img, 100, 10);
context.restore();

context.save();
context.rotate(0.78);
context.drawImage(img, 100, 10);
context.restore();

Nested Transformations

Successive commands form the current transformation matrix as the concatenation of a number of transformations. A coordinate system below is transformed according to the I(T1(R(T2g))) pattern: to calculate the initial coordinates of a graphic object g, its coordinate values are first multiplied by the T2 translation matrix, then the resulting T2g is multiplied by the R rotation matrix, after that RT2g is multiplied by the T1 translation matrix. I is the identity matrix.

context.transform(1, 0, 0, 1, 200, 200); // translation - T1
context.transform(Math.cos(-0.78), Math.sin(-0.78), -Math.sin(-0.78), Math.cos(-0.78), 0, 0); // rotation - R
context.transform(1, 0, 0, 1, 100, 100); // translation - T2

Sometimes it is necessary to reset the current matrix to the identity matrix before applying a new effect. To prevent the accumulation of successive transformations, the setTransform(a, b, c, d, e, f) method is used: it resets the current transform to the identity matrix and then apply matrix entries similar to the transform(a, b, c, d, e, f) method:

context.transform(1, 0, 0, 1, 200, 200); // translation - T1
context.transform(Math.cos(-0.78), Math.sin(-0.78), -Math.sin(-0.78), Math.cos(-0.78), 0, 0); // rotation - R
context.setTransform(1, 0, 0, 1, 100, 100); // identity matrix is loaded, then translation is performed

The current transformation matrix (or CTM) is part of the graphics state, so it can be pushed onto the stack. A stack of matrices are useful in construction of complex 2D scenes. Like any other stack element, the topmost matrix is popped off the stack when the restore() method is invoked on the rendering context.

Summary: Transformations

transformation matrix
context.transform(a, b, c, d, e, f);

context.setTransform(a, b, c, d, e, f);

simple transformation operations
context.translate(x, y);

context.scale(x, y);

context.rotate(angle);