Canvas

The <canvas> element was introduced with HTML5, and are supported by all major browser. It is used to draw 2D and 3D graphics. The element required an opening and closing tag.

You use the canvas element in it's simplest form by writing:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Canvas</title>
  </head>
  <body>

    <canvas id="canvas"></canvas>

  </body>
</html>

The element is placed inside the body tag in the HTML document.

This will then create an element you can fill with graphics using JavaScript. The id attribute is used to reference the element with JavaScript and CSS, just like how other element are referenced. Usually you will also set the height and width of the canvas. This can be done inline or with CSS.

Note that if you make the canvas smaller than the content, the content outside the element will not be shown. If you don't set a height or width, the canvas will use it's default values of 300px wide and 150px high.

The background color of your canvas can also be set using the CSS property background-color, equal to all other HTML elements. Other CSS properties used on the canvas element will only affect the element, e.g. border and shadow, and not the shapes drawn inside the canvas.

The content of this chapter is based on Mozilla Developer Network's presentation on Canvas.

Drawing a rectangle

If you want to create a red rectangle inside the canvas element, you would write:

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d')
context.fillStyle = 'red';
context.fillRect(10, 20, 100, 150);

The first thing that's happening in the example above is that we reference the element in the document, which we gave the id "canvas". On the second line we use the method getContext() which returns a drawing context, i.e. the type of canvas. The different types of canvases are:

  • "2d" creates a two-dimensional rendering context.

  • "webgl" creates a three-dimensional rendering context. This context is only available on browsers that implements WebGL version 1.

  • "webgl2" creates a three-dimensional rendering context. This context is only available on browsers that implements WebGL version 2.

  • "bitmaprenderer" will create a context which only provides functionality to replace the content of the canvas with a given ImageBitmap.

Note that the argument is a string, and may only be one of those four. If you give an invalid argument, you will get null in return.

The two following lines of code will draw a red rectangle that has it's first pixel drawn at the browser's coordinates 10, 20. That is 10 pixels from the left on the x-axis and 20 pixels from the top on the y-axis.

You should know that the getContext method takes another argument in the shape of a JavaScript object, after the context type. This is the context attributes. For the 2d context type, which is the one relevant for this course, the only available context attribute is alpha. If alpha is set to true, the canvas element contains alpha channels (opacity). If this is set to false, the browser know that the backdrop always will be opaque, which can speed up drawing of transparent content and images.

If you want to set the context attribute, you would write:

canvas.getContext('2d', {alpha : false})

Okay, so that's a thorough introduction. Now let's learn how to draw stuff!

Drawing on a 2D canvas

When you're drawing on a canvas, the order of drawing is important. The latter drawings, e.g. if you create two rectangles, the latter will be on top of the first one, should they overlap.

In the example below we draw two rectangles that will overlap, because the one starts within the drawn area of the first one.

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d')
context.fillStyle = 'red';
context.fillRect(10, 10, 100, 100);
context.fillStyle = 'blue';
context.fillRect(30, 30, 100, 100);

Actually, <canvas> only support one primitive shape: rectangles. All other shapes must be created by combining one or more paths, a list of points connected by lines. More on that later!

Rectangles

Earlier you saw that we wrote context.fillRect(x, y, width, height). The fillRect() function is used to fill the rectangle with color. We don't have do use it. There are two other functions you should know, strokeRect(x, y, width, height) and clearRect(x, y, width, height). strokeRect draws the outline of a rectangle. clearRect() clears the specified rectangle, making it fully transparent. The parameters are all the same for these three functions.

In the example below, we utilise these three functions.

context.fillRect(25, 25, 100, 100);
context.clearRect(45, 45, 60, 60);
context.strokeRect(50, 50, 50, 50);

Lines and triangles

The other primitive shape in canvas are paths. This is a list of points, connected by lines that can be curved or not, and be of different widths and colors.

When drawing shapes with paths, you have to:

  1. Create the path

  2. Use drawing functions to draw into the path

  3. Close the path

  4. Stroke or fill the path to render it

The functions you use to do this are in order:

  • beginPath() - creates a new path. Once it is created, future drawing commands are directed into the path and used to build the path

  • Path methods (this can be used as necessary)

    • moveTo\(\) - moves the starting point of a new sub-path to the \(x,y\) coordinates

    • lineTo\(\) - connects the last point in the subpath to the \(x,y\) coordinates with a straight line

    • arc\(\) - Adds an arc to the path which is centered at \(x, y\) position with radius r starting at startAngle and ending at endAngle going in the given direction by anticlockwise

    • arcTo\(\) - Adds an arc to the path with the given control points and radius, connected to the previous point by a straight line.

    • rect\(\) - Creates a path for a rectangle at position \(x, y\) with a size that is determined by width and height

  • closePath() - closes the path so that future drawing commands are directed to the context

  • stroke() - Draws the shape by stroking the outline

  • fill() Draws a solid shape by filling the path's content area

Now we're going to put all this information to life by drawing a triangle.

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
context.beginPath();
context.moveTo(75, 50);
context.lineTo(100, 70);
context.lineTo(100, 25);
context.fill();

The function moveTo() specify where the coordinates where the first pixel should be drawn. This can be viewed as lifting the pen from one spot to another. moveTo() can be used to draw unconnected paths.

lineTo() specifies the paths do be drawn. Lastly fill() fills the shape with a solid color. If you change fill() with stroke() you'll see that only two lines are drawn. Should you want a third line to complete the triangle, e.g. if it only should have a stroke, you would add context.closePath() before stroke() or fill(). This function tries to draw a straight line from the current coordinates (100, 25) to the starting coordinates (75, 50).

Circles

To draw arcs and circles, we use the arc() function.

  • arc(x, y, radius, startAngle, endAngle, anticlockwise)

Draws an arc which is centred at the coordinates (x, y) with radius r starting at startAngle and ending at endAngle, going in the given direction indicated by the boolean value anticlockwise (defaulting to clockwise).

The example below will give a complete circle:

context.beginPath();
context.arc(100, 100, 40, 0, 2*Math.PI, true)
context.stroke();
context.beginPath();
context.arc(100, 100, 40, 0, Math.PI, true)
context.stroke();
context.beginPath();
context.arc(100, 100, 40, 0, Math.PI, false)
context.stroke();

bezierCurveTo() and quadraticCurveTo() is also worth to read about if you want to create complex shapes. You can do that at Mozilla Developer Network's canvas tutorial.

Text

With canvas you have two functions to render text:

  • fillText(text, x, y)

    Fills a given text at the coordinates (x, y).

  • strokeText(text, x, y)

    Strokes a given text at the coordinates (x, y).

An example of fillText() could be:

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
context.fillText('Hello Canvas!', 10, 10);

An example of strokeText() could be:

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
context.font = '48px serif';
context.strokeText('Hello Canvas!', 10, 48);

There are four properties you can use to style the text.

  • font

    Takes a value which is the font size e.g. 48px and font-family e.g. arial or font type e.g. 'serif'.

    context.font = '48px serif';

  • textAlign

    This is the text alignment setting, which takes one of the values start, end, left, right or center.

    context.textAlign = 'left';

  • textBaseline

    This is the baseline alignment setting, which takes on of the values top, hanging, middle, alphabetic, ideographic or bottom. The default value is alphabetic.

    context.textBaseline = 'top';

  • direction

    This is the text's direction setting, which takes one of the values ltr, rtl or inherit. The default value is inherit.

    context.direction = 'ltr';

These four properties are similar to their CSS counterparts.

Styling the canvas

We have already mentioned the property fillStyle, which applies a color to your shape. You can also apply a style to your strokes using the property strokeStyle. The color can, similar to CSS, be expressed with a keyword (e.g. red, blue, green) or as a hex value (e.g. FF0000, 00FF00, 0000FF). You can also use rgb and rgba, should you want the alpha channel also.

With lines, you have the properties:

  • lineWidth

    Sets the width of lines drawn in the future. The value can be any integer.

    context.lineWidth = 15;

  • lineCap

    Sets the appearance of the ends of lines. The types can be butt, round or square.

    context.lineCap = 'square';

  • lineJoin

    Sets the appearance of the corners where lines meet. The types can be round, bevel or miter.

    context.lineJoin = 'round';

  • lineDashOffset

    Specifies where to start a dash array on a line. The value can be any integer.

    context.lineDashOffset = 2;

And the function:

  • setLineDash(segment)

    Sets the line dash pattern. The argument is an array of numbers that specifies the distances to alternately draw a line and a gap. If the number of elements in the array is odd, the elements of the array get copied and concatenated. E.g. [5, 15, 25] will become [5, 15, 25, 5, 15, 25].

    context.setLineDash([5, 15]);

Placing images in your canvas

Importing images into a canvas is basically a two step process:

  1. Get a reference to an image

  2. Draw the image on the canvas using the drawImage() function

To do this, you first need to create a new image object using the Image() constructor:

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const img = new Image();
img.src = 'path/to/image.png';

In this case the image is stored locally in your folder structure. You can also use an url, if the image is publicly available.

We have now taken care of step 1. In step 2 we use the function drawImage(img, x, y). The following code build upon the previous example:

context.drawImage(img, 10, 10);

It can happen that the image is loaded after the code is executed, which happens because the image must be loaded from the server. To handle this you can use the image's onload property:

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const img = new Image();

img.onload = function() {
    context.drawImage(img, 10, 10)
}

img.src = 'path/to/image.png';

In some cases you need to scale the image. To lower the file's size, this should be done beforehand. But you can use the function drawImage(image, x, y, width, height) to accomplish this:

drawImage(img, 10, 10, 150, 200);

You can also crop an image. This is done using the function drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight). The parameters sx, sy, sWidth and sHeight represents the image's x and y coordinates, and the image's width and height. The parameters dx, dy, dWidth and dHeight represents the rectangle into which to draw the image on the destination canvas.

Animate on a 2D canvas

Now let's dive into animations. One limitation of canvas is that once a shape is drawn, it stays that way. If you want to move it, you have to redraw the shape. This can take a lot of time, and the performance depends on the speed of the speed of the computer that is running the animation.

If you want to animate a canvas, that is to redraw it, there are some steps you need to follow:

  1. Clear the canvas

    Unless the shapes you will be drawing fills the complete canvas (e.g. background image), you need to clear any shapes that have been drawn previously. This can be done using the function clearRect().

    context.clearRect(x, y, width, height);

  2. Save the canvas' state

    If you're changing any settings (e.g. styles, transformations) which affect the canvas' state and you want to make sure the original state is used each time a frame is drawn, you need to save that original state.

    context.save();

  3. Draw animated shapes

    This is where you're actually rendering the new frame.

  4. Restore the canvas' state

    If you saved the state, restore it before you drawing a new frame.

    context.restore();

Shapes are drawn to the canvas using the canvas' functions directly, or by calling custom functions. We only see these results appear on the canvas when the script finishes executing. Thus, it is not possible to do an animation from within a for loop. This means that you have to execute the drawing functions over a period of time. This can be done using the functions:

  • setInterval(function, delay)

    Starts repeatedly executing the function specified every *delay* milliseconds.

  • setTimeout(function, delay)

    Executes the function specified in *delay* milliseconds.

If you don't want any user interaction you can use the setInterval() function. On the other hand, should you want to create e.g. a game with user interaction, you can use setTimeout(). By setting EventListeners, you can listen for the user's interaction and execute animations based on them.

The following example is taken from Mozilla Developer Network's canvas basic animation tutorial.

const sun = new Image();
const moon = new Image();
const earth = new Image();
function init() {
  sun.src = 'https://mdn.mozillademos.org/files/1456/Canvas_sun.png';
  moon.src = 'https://mdn.mozillademos.org/files/1443/Canvas_moon.png';
  earth.src = 'https://mdn.mozillademos.org/files/1429/Canvas_earth.png';
  window.requestAnimationFrame(draw);
}

function draw() {
  const canvas = document.getElementById('canvas')
  const context = canvas.getContext('2d');

  context.globalCompositeOperation = 'destination-over';
  context.clearRect(0, 0, 300, 300); // clear canvas

  context.fillStyle = 'rgba(0, 0, 0, 0.4)';
  context.strokeStyle = 'rgba(0, 153, 255, 0.4)';
  context.save();
  context.translate(150, 150);

  // Earth
  var time = new Date();
  context.rotate(((2 * Math.PI) / 60) * time.getSeconds() + ((2 * Math.PI) / 60000) * time.getMilliseconds());
  context.translate(105, 0);
  context.fillRect(0, -12, 35, 24); // Shadow
  context.drawImage(earth, -12, -12);

  // Moon
  context.save();
  context.rotate(((2 * Math.PI) / 6) * time.getSeconds() + ((2 * Math.PI) / 6000) * time.getMilliseconds());
  context.translate(0, 28.5);
  context.drawImage(moon, -3.5, -3.5);
  context.restore();

  context.restore();

  context.beginPath();
  context.arc(150, 150, 105, 0, Math.PI * 2, false); // Earth orbit
  context.stroke();

  context.drawImage(sun, 0, 0, 300, 300);

  window.requestAnimationFrame(draw);
}

init();

Note that most tutorials use the constant or variable name ctx instead of context. I use context to make it clear what this constant represent. This could would give the animation of the earth circling the sun, with the moon orbiting around the earth. I recommend that you try implementing this yourself, and play with the arguments to see how it affects the final result.

In this example, you also see some new properties and functions.

  • context.globalCompositeOperation This sets the type of compositing operation to apply when drawing new shapes. This property takes a type that can be one of source-over, source-in, source-out, source-atop, destination-over, destination-in, destination-out, destination-atop, lighter, copy, xor, multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, hue, saturation, color and luminosity. Don't worry though, you don't need to remember any of these. You can see their effect at MDN's globalCompositeOperation.

  • context.translate(x, y) This functions takes an x and an y coordinate as parameters. When called, the canvas' (0, 0) coordinates are set to the translate function's coordinates.

  • context.rotate(degree) This functions rotates a shape or image with the degree specified as the parameter.

  • window.requestAnimationFrame(callback) This tells the browser that you wish to perform an animation, and requests that the browser call a specified function to update an animation before the next repaint. The parameter callback should be the function you want to call.

You can see all the functions and properties available for the canvas' context at w3school.

Last updated