INFO-I400 Topics in Informatics

Graphics, Animation, and Multimedia for the Web

Lesson 15: Canvas Transforms and Shadows

Prerequisites:

Reading Assignment

Summary

Transformations

Translation, scaling, and rotation are examples of affine transformations: transformations which preserve straight lines and the ratios of distances along straight lines. There are other affine transformations, such as reflection, shearing, and squeezing; but translation, rotation, and scale are generally the most useful.

From the reading assignment, you should know what translation, rotation, and scaling do, and how to save and restore the context state. You should also understand that translating and then rotating has a different effect than rotating and then translating.

You can apply these transformations to anything that can be rendered: images, text, and paths made by stroking and filling lines and curves.

Here is a summary of the commands:

ctx.translate(dx, dy);
ctx.rotate(radians);  // measured clockwise
ctx.scale(sx, sy);
ctx.save()
ctx.restore()

Shadows

Shadows are a bit like translation—they involve rendering a “copy” of an object at a fixed offset (Δ x, Δ y). You can have shadows drawn by controlling the context properties shadowOffsetX, shadowOffsetY, shadowColor (typically with some degree of transparency, i.e., α < 1), shadowBlur (0 being no blur, and 10 very blurry).

Examples

We will begin without any transformations at all, and then gradually add more and more transformations.

Example 15-01

Here is a function to draw a V-shaped arrowhead at a position (x, y).

// draw the arrowhead at (x, y)
function drawArrowHead(x, y) {
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x + 50, y + 12);
    ctx.lineTo(x, y + 25);
    ctx.lineTo(x + 25, y + 12);
    ctx.closePath();
    ctx.fillStyle = "gold";
    ctx.fill();
}

By changing the values of x through time, we can animate the arrowhead to “fly” across the canvas. We’re not using the translate method to do this; we’re computing all the coordinates ourselves, based on the parameters x and y:

Example 15-02

Alternatively, we can define a function to draw the arrowhead at the fixed position (0, 0):

// draw the arrowhead at (0, 0)
function drawArrowHead() {
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(50, 12);
    ctx.lineTo(0, 25);
    ctx.lineTo(25, 12);
    ctx.closePath();
    ctx.fillStyle = "gold";
    ctx.fill();
}

Then, when we want it to appear at (x, y), we can translate to that position before drawing:

ctx.save();
ctx.translate(x, y);
drawArrowHead();
ctx.restore();

This achieves the same effect, and saves us from having to take the position (x, y) into account in the drawArrowhead function.

Example 15-03

Let’s have our arrowhead spin and zoom in, instead of flying across the canvas. We’ll use rotate and scale, with variables theta for the angle and size. We’ll still translate, but now to to a fixed (x, y).

First, we’ll change our drawArrowhead function so that it draws at a “standard size” of 1 unit (1 pixel), which is the width of the arrowhead:

// draw the arrowhead at (0, 0) with size (width) 1.0
function drawArrowHead() {
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(1.0, 0.25);
    ctx.lineTo(0, 0.5);
    ctx.lineTo(0.5, 0.25);
    ctx.closePath();
    ctx.fillStyle = "gold";
    ctx.fill();
}

Then we’ll use scale and rotate before calling drawArrowhead:

ctx.save();
ctx.translate(x, y);
ctx.scale(size, size);
ctx.rotate(theta);
drawArrowHead();
ctx.restore();

Example 15-04

Did you notice that in the previous example, the arrowhead rotated around its top left corner, instead of around its center? That’s because in drawArrowhead, the point (0, 0) was the top left corner. To rotate it around its center, we need to revise drawArrowhead, changing to a coordinate system where (0, 0) is the center of the arrowhead:

// draw the arrowhead with its CENTER at (0, 0)
function drawArrowHead() {
    ctx.beginPath();
    ctx.moveTo(-0.5, -0.5);
    ctx.lineTo(0.5, 0);
    ctx.lineTo(-0.5, 0.5);
    ctx.lineTo(0, 0);
    ctx.closePath();
    ctx.fillStyle = "gold";
    ctx.fill();
}

Example 15-05

Now let’s add a shadow to the arrowhead:

ctx.save();
ctx.translate(x, y);
ctx.scale(size, size);
ctx.rotate(theta);
// Shadow properties
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowColor="hsla(0, 0%, 50%, 0.25)";
ctx.shadowBlur = 5;
drawArrowHead();
ctx.restore();

This is a gray shadow which is 25% opaque and has some blur.

Example 15-06

We can also translate, scale, rotate, and add shadow to an image:

ctx.save();
ctx.translate(x, y);
ctx.rotate(theta);
ctx.scale(size, size);
// Shadow properties
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowColor="hsla(0, 25%, 50%, 0.5)";
ctx.shadowBlur = 3;
// Draw image
ctx.drawImage(im, 0, 0);
ctx.restore();

In this case, I’ve made the shadow a bit reddish, more opaque, and less blurry. Notice that the image itself has some transparency, and the shadow follows the opaque parts of the image, as it should:

Example 15-07

Finally, we can also transform and add shadows to text. This time I’ll make the shadow blue-green and a little sharper than even the image shadow. Also I’ll “align” the text at its horizontal and vertical centers, so that it will rotate around its center instead of around one of its ends.

ctx.save();
ctx.translate(x, y);
ctx.rotate(theta);
// Shadow properties
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowColor="hsla(180, 100%, 25%, 0.75)";
ctx.shadowBlur = 2.5;
// Text properties
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "32pt sans";
ctx.fillStyle = "blue";
// Draw text
ctx.fillText("Transformed!", 0, 0);
ctx.restore();

Notice how the shadow follows the shape of the text:

Suggestions and Questions

  1. Define a function that draws an object (or scene) with a a standard location of (0, 0), a standard size of 1 unit (or maybe 100 units, to work in percentages), and a standard orientation of 0 radians (which is also 0 degrees). Then apply translate, rotate, and scale transforms as needed. This is much easier than defining a function to draw an object where in the function you calculate the position, scale, and rotation effects yourself.

  2. When you rotate an object, it is important to realize what is the “center of rotation” for the object; it is not always the geometrical center of the object itself. Rectangles are rotated about their top left corner. Images, which are rectangular, are treated the same way. Arcs—I’m not sure about arcs; anyone want to check on this? Other paths are rotated about the first point in the path. At least, that’s what the Apple tutorial said; but I don’t think that’s what I observed in Example 15-04, where the first point in the path is ( − 0. 5,  − 0. 5), but the rotation seems to be about (0, 0). What do you think?

  3. Save and restore operate like a call stack in programming, where each time we call a function, a call frame is pushed onto the stack to store the function call’s state (local variables, return-to address), and popped off when the function call returns.

  4. After saving state and rendering, don’t forget to restore the state! If you find yourself saving and restoring state a lot, it might be helpful to use a higher-order function to make sure it is done consistently:

    function preservingState(action) {
        ctx.save();
        action();
        ctx.restore();
    }
    
    preservingState(function ()
                    {
                      ctx.translate(10, 50);
                      ctx.rotate(Math.PI / 2);
                      renderSomeObject();
                    });
  5. Is there any difference between scaling text and changing the font size, for example, drawing text with a 32-point font compared to scaling by 2 and drawing the text at a 16-point font?

  6. Is there any difference between scaling an image with the scale method and scaling an image with the width and height parameters of the drawImage method? For example, if the natural size of an image is 32 x 32, is there a difference between ctx.scale(2, 2); ctx.drawImage(im, x, y); and just ctx.drawImage(im, x, y, 64, 64)?

  7. The context state which is saved on the stack contains other data besides transformations (translate, rotate, scale). These include the color/style properties strokeStyle, fillStyle, globalAlpha, the line drawing properties lineWidth, lineCap, lineJoin, and the shadow properties shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor`, For a complete list of the context state data, see The Canvas State in the HTML5 developer specification.

Gallery

See the “Text” and “Animated image” examples (rotated text, rotating image) in Canvas Studies.

Reference