Drawing on canvas in JavaScript

1. SVG

This book will not go into SVG in detail, but I will briefly explain how it works. In “Choosing a Graphics Interface” on page 305, I’ll come back to the trade-offs that you must consider when deciding which drawing mechanism is appropriate for a given application.

This is an HTML document with a simple SVG picture in it:

<p>Normal HTML here.</p>

<svg xmlns=”http://www.w3.org/2000/svg”>

<circle r=”50″ cx=”50″ cy=”50″ fill=”red”/>

<rect x=”120″ y=”5″ width=”90″ height=”90″ stroke=”blue” fill=”none”/>

</svg>

The xmlns attribute changes an element (and its children) to a different XML namespace. This namespace, identified by a URL, specifies the dialect that we are currently speaking. The <circle> and <rect> tags, which do not exist in HTML, do have a meaning in SVG—they draw shapes using the style and position specified by their attributes.

The document is displayed like this:

Normal HTML here.

These tags create DOM elements, just like HTML tags, that scripts can interact with. For example, this changes the <circle> element to be colored cyan instead:

let circle = document.querySelector(“circle”);

circle.setAttribute(“fill”, “cyan”);

2. The Canvas Element

Canvas graphics can be drawn onto a <canvas> element. You can give such an element width and height attributes to determine its size in pixels.

A new canvas is empty, meaning it is entirely transparent and thus shows up as empty space in the document.

The <canvas> tag is intended to allow different styles of drawing. To get access to an actual drawing interface, we first need to create a context, an object whose methods provide the drawing interface. There are currently two widely supported drawing styles: “2d” for two-dimensional graphics and “webgl” for three-dimensional graphics through the OpenGL interface.

This book won’t discuss WebGL—we’ll stick to two dimensions. But if you are interested in three-dimensional graphics, I do encourage you to look into WebGL. It provides a direct interface to graphics hardware and allows you to render even complicated scenes efficiently, using JavaScript.

You create a context with the getContext method on the <canvas> DOM element.

<p>Before canvas.</p>

<canvas width=”120″ height=”60″></canvas> <p>After canvas.</p>

<script>

let canvas = document.querySelector(“canvas”);

let context = canvas.getContext(“2d”);

context.fillStyle = “red”;

context.fillRect(10, 10, 100, 50);

</script>

After creating the context object, the example draws a red rectangle 100 pixels wide and 50 pixels high, with its top-left corner at coordinates (10,10).

Before canvas.

Just like in HTML (and SVG), the coordinate system that the canvas uses puts (0,0) at the top-left corner, and the positive y-axis goes down from there. So (10,10) is 10 pixels below and to the right of the top-left corner.

3. Lines and Surfaces

In the canvas interface, a shape can be filled, meaning its area is given a cer­tain color or pattern, or it can be stroked, which means a line is drawn along its edge. The same terminology is used by SVG.

The fillRect method fills a rectangle. It takes first the x- and y- coordinates of the rectangle’s top-left corner, then its width, and then its height. A similar method, strokeRect, draws the outline of a rectangle.

Neither method takes any further parameters. The color of the fill, thickness of the stroke, and so on, are not determined by an argument to the method (as you might reasonably expect) but rather by properties of the context object.

The fillStyle property controls the way shapes are filled. It can be set to a string that specifies a color, using the color notation used by CSS.

The strokeStyle property works similarly but determines the color used for a stroked line. The width of that line is determined by the lineWidth prop­erty, which may contain any positive number.

<canvas></canvas>

<script>

let cx = document.querySelector(“canvas”).getContext(“2d”);

cx.strokeStyle = “blue”;

cx.strokeRect(5, 5, 50, 50);

cx.lineWidth = 5;

cx.strokeRect(135, 5, 50, 50);

</script>

This code draws two squares, using a thicker line for the second one.

When no width or height attribute is specified, as in the example, a can­vas element gets a default width of 300 pixels and height of 150 pixels.

4. Paths

A path is a sequence of lines. The 2D canvas interface takes a peculiar approach to describing such a path. It’s done entirely through side effects. Paths are not values that can be stored and passed around. Instead, if you want to do something with a path, you make a sequence of method calls to describe its shape.

<canvas></canvas>

<script>

let cx = document.querySelector(“canvas”).getContext(“2d”);

cx.beginPath();

for (let y = 10; y < 100; y += 10) {

cx.moveTo(10, y); cx.lineTo(90, y);

}

cx.stroke();

</script>

This example creates a path with a number of horizontal line segments and then strokes it using the stroke method. Each segment created with lineTo starts at the path’s current position. That position is usually the end of the last segment, unless moveTo was called. In that case, the next segment would start at the position passed to moveTo.

The path described by the previous program looks like this:

When filling a path (using the fill method), each shape is filled sepa­rately. A path can contain multiple shapes—each moveTo motion starts a new one. But the path needs to be closed (meaning its start and end are in the same position) before it can be filled. If the path is not already closed, a line is added from its end to its start, and the shape enclosed by the completed path is filled.

<canvas></canvas>

<script>

let cx = document.querySelector(“canvas”).getContext(“2d”);

cx.beginPath();

cx.moveTo(50, 10);

cx.lineTo(10, 70);

cx.lineTo(90, 70);

cx.fill();

</script>

This example draws a filled triangle. Note that only two of the triangle’s sides are explicitly drawn. The third, from the bottom-right corner back to the top, is implied and wouldn’t be there when you stroke the path.

You could also use the closePath method to explicitly close a path by adding an actual line segment back to the path’s start. This segment is drawn when stroking the path.

5. Curves

A path may also contain curved lines. These are unfortunately a bit more involved to draw.

The quadraticCurveTo method draws a curve to a given point. To deter­mine the curvature of the line, the method is given a control point as well as a destination point. Imagine this control point as attracting the line, giving it its curve. The line won’t go through the control point, but its direction at the start and end points will be such that a straight line in that direction would point toward the control point. The following example illustrates this: <canvas></canvas>

<script>

let cx = document.querySelector(“canvas”).getContext(“2d”);

cx.beginPath();

cx.moveTo(10, 90);

// control=(60,10) goal=(90,90) cx.quadraticCurveTo(60, 10, 90, 90);

cx.lineTo(60, 10);

cx.closePath();

cx.stroke();

</script>

It produces a path that looks like this:

We draw a quadratic curve from the left to the right, with (60,10) as control point, and then draw two line segments going through that control point and back to the start of the line. The result somewhat resembles a Star Trek insignia. You can see the effect of the control point: the lines leaving the lower corners start off in the direction of the control point and then curve toward their target.

The bezierCurveTo method draws a similar kind of curve. Instead of a single control point, this one has two—one for each of the line’s endpoints. Here is a similar sketch to illustrate the behavior of such a curve:

<canvas></canvas>

<script>

let cx = document.querySelector(“canvas”).getContext(“2d”);

cx.beginPath();

cx.moveTo(10, 90);

// control1=(10,10) control2=(90,10) goal=(50,90)
cx.bezierCurveTo(l0, 10, 90, 10, 50, 90);

cx.lineTo(90, 10);

cx.lineTo(10, 10);

cx.closePath();

cx.stroke();

</script>

The two control points specify the direction at both ends of the curve. The farther away they are from their corresponding point, the more the curve will “bulge” in that direction.

Such curves can be hard to work with—it’s not always clear how to find the control points that provide the shape you are looking for. Sometimes you can compute them, and sometimes you’lljust have to find a suitable value by trial and error.

The arc method is a way to draw a line that curves along the edge of a circle. It takes a pair of coordinates for the arc’s center, a radius, and then a start angle and end angle.

Those last two parameters make it possible to draw only part of the circle. The angles are measured in radians, not degrees. This means a full circle has an angle of 2n, or 2 * Math.PI, which is about 6.28. The angle starts counting at the point to the right of the circle’s center and goes clockwise from there. You can use a start of 0 and an end bigger than 2n (say, 7) to draw a full circle.

<canvas></canvas>

<script>

let cx = document.querySelector(“canvas”).getContext(“2d”);

cx.beginPath();

// center=(50,50) radius=40 angle=0 to 7 cx.arc(50, 50, 40, 0, 7);

// center=(150,50) radius=40 angle=0 to 1/2 pi cx.arc(150, 50, 40, 0, 0.5 * Math.PI);

cx.stroke();

</script>

The resulting picture contains a line from the right of the full circle (first call to arc) to the right of the quarter-circle (second call). Like other path-drawing methods, a line drawn with arc is connected to the previous path segment. You can call moveTo or start a new path to avoid this.

6. Drawing a Pie Chart

Imagine you’ve just taken ajob at EconomiCorp, Inc., and your first assign­ment is to draw a pie chart of its customer satisfaction survey results.

The results binding contains an array of objects that represent the sur­vey responses.

const results = [

{name: “Satisfied”, count: 1043, color: “lightblue”},

{name: “Neutral”, count: 563, color: “lightgreen”},

{name: “Unsatisfied”, count: 510, color: “pink”},

{name: “No comment”, count: 175, color: “silver”}

];

To draw a pie chart, we draw a number of pie slices, each made up of an arc and a pair of lines to the center of that arc. We can compute the angle taken up by each arc by dividing a full circle (2n) by the total number of responses and then multiplying that number (the angle per response) by the number of people who picked a given choice.

<canvas width=”200″ height=”200″></canvas>

<script>

let cx = document.querySelector(“canvas”).getContext(“2d”);

let total = results

.reduce((sum, {count}) => sum + count, 0);

// Start at the top

let currentAngle = -0.5 * Math.PI;

for (let result of results) {

let sliceAngle = (result.count / total) * 2 * Math.PI;

cx.beginPath();

// center=100,100, radius=100

// from current angle, clockwise by slice’s angle

cx.arc(100, 100, 100,

currentAngle, currentAngle + sliceAngle);

currentAngle += sliceAngle; cx.lineTo(100, 100);

cx.fillStyle = result.color; cx.fill();

}

</script>

This draws the following chart:

But a chart that doesn’t tell us what the slices mean isn’t very helpful. We need a way to draw text to the canvas.

7. Text

A 2D canvas drawing context provides the methods fillText and strokeText. The latter can be useful for outlining letters, but usually fillText is what you need. It will fill the outline of the given text with the current fillStyle.

<canvas></canvas>

<script>

let cx = document.querySelector(“canvas”).getContext(“2d”);

cx.font = “28px Georgia”;

cx.fillStyle = “fuchsia”;

cx.fillText(“I can draw text, too!”, 10, 50);

</script>

You can specify the size, style, and font of the text with the font property. This example just gives a font size and family name. It is also possible to add italic or bold to the start of the string to select a style.

The last two arguments to fillText and strokeText provide the position at which the font is drawn. By default, they indicate the position of the start of the text’s alphabetic baseline, which is the line that letters “stand” on, not counting hanging parts in letters such as j or p. You can change the hor­izontal position by setting the textAlign property to “end” or “center” and the vertical position by setting textBaseline to “top”, “middle”, or “bottom”.

We’ll come back to our pie chart, and the problem of labeling the slices, in the exercises at the end of the chapter.

8. Images

In computer graphics, a distinction is often made between vector graph­ics and bitmap graphics. The first is what we have been doing so far in this chapter—specifying a picture by giving a logical description of shapes. Bitmap graphics, on the other hand, don’t specify actual shapes but rather work with pixel data (rasters of colored dots).

The drawImage method allows us to draw pixel data onto a canvas. This pixel data can originate from an <img> element or from another canvas. The following example creates a detached <img> element and loads an image file into it. But it cannot immediately start drawing from this picture because the browser may not have loaded it yet. To deal with this, we register a “load” event handler and do the drawing after the image has loaded.

<canvas></canvas>

<script>

let cx = document.querySelector(“canvas”).getContext(“2d”);

let img = document.createElement(“img”);

img.src = “img/hat.png”; img.addEventListener(“load”, () => {

for (let x = 10; x < 200; x += 30) {

cx.drawImage(img, x, 10);

}

});

</script>

By default, drawImage will draw the image at its original size. You can also give it two additional arguments to set a different width and height.

When drawImage is given nine arguments, it can be used to draw only a fragment of an image. The second through fifth arguments indicate the rectangle (x, y, width, and height) in the source image that should be copied, and the sixth to ninth arguments give the rectangle (on the canvas) into which it should be copied.

This can be used to pack multiple sprites (image elements) into a single image file and then draw only the part you need. For example, we have this picture containing a game character in multiple poses:

By alternating which pose we draw, we can show an animation that looks like a walking character.

To animate a picture on a canvas, the clearRect method is useful. It resembles fillRect, but instead of coloring the rectangle, it makes it trans­parent, removing the previously drawn pixels.

We know that each sprite, each subpicture, is 24 pixels wide and 30 pix­els high. The following code loads the image and then sets up an interval (repeated timer) to draw the next frame:

<canvas></canvas>

<script>

let cx = document.querySelector(“canvas”).getContext(“2d”);

let img = document.createElement(“img”);

img.src = “img/player.png”;

let spriteW = 24, spriteH = 30;

img.addEventListener(“load”, () => {

let cycle = 0; setInterval(() => {

cx.clearRect(0, 0, spriteW, spriteH);

cx.drawImage(img,

// source rectangle cycle * spriteW, 0, spriteW, spriteH,

// destination rectangle 0,  0, spriteW, spriteH);

cycle = (cycle + 1) % 8;

}, 120);

});

</script>

The cycle binding tracks our position in the animation. For each frame, it is incremented and then clipped back to the 0 to 7 range by using the remainder operator. This binding is then used to compute the x-coordinate that the sprite for the current pose has in the picture.

Source: Haverbeke Marijn (2018), Eloquent JavaScript: A Modern Introduction to Programming, No Starch Press; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *