Chapter 2. Drawing on the Canvas

Using HTML5 Canvas effectively requires a strong foundation in drawing, coloring, and transforming basic two-dimensional shapes. While the selection of built-in shapes is relatively limited, we can draw any shape we desire using a series of line segments called paths, which we will discuss in the upcoming section Using Paths to Create Lines.

Note

The HTML5 Canvas API is well covered in many online forms. The W3C site has an exhaustive and constantly updated reference that details the features of the Canvas 2D Drawing API. It can be viewed at http://dev.w3.org/2006/canvas-api/canvas-2d-api.html.

However, this online reference lacks concrete examples on using the API. Rather than simply reprinting this entire specification, we will spend our time creating examples to explain and explore as many features as we have space to cover.

The Basic File Setup for This Chapter

As we proceed through the Drawing API, all the examples in this chapter will use the same basic file setup, shown below. Use this code as the basis for all of the examples we create. You will only have to change the contents of the drawScreen() function:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ch2BaseFile - Template For Chapter 2 Examples</title>
<script src="modernizr-1.6.min.js"></script>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
function eventWindowLoaded() {

   canvasApp();

}

function canvasSupport () {
     return Modernizr.canvas;
}

function canvasApp(){

if (!canvasSupport()) {
          return;
     }else{
      var theCanvas = document.getElementById("canvas");
      var context = theCanvas.getContext("2d");
   }

   drawScreen();

   function drawScreen() {
      //make changes here.
      context.fillStyle = '#aaaaaa';
      context.fillRect(0, 0, 200, 200);
      context.fillStyle  = '#000000';
      context.font = '20px _sans';
      context.textBaseline = 'top';
      context.fillText  ("Canvas!", 0, 0);

   }
}

</script>
</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">
<canvas id="canvas" width="500" height="500">
 Your browser does not support HTML5 Canvas.
</canvas>
</div>
</body>
</html>

The Basic Rectangle Shape

Let’s get our feet wet by looking at the single primitive, built-in geometric shape on Canvas—the rectangle. On Canvas, basic rectangle shapes can be drawn in three different ways: filling, stroking, or clearing. We can also build rectangles (or any other shape) by using paths, which we will cover in the next section.

First, let’s look at the API functions used for these three operations:

fillRect(x,y,width,height)

Draws a filled rectangle at position x,y for width and height.

strokeRect(x,y,width,height)

Draws a rectangular outline at position x,y for width and height. This makes use of the current strokeStyle, lineWidth, lineJoin, and miterLimit settings.

clearRect(x,y,width,height)

Clears the specified area and makes it fully transparent (using transparent black as the color) starting at position x,y for width and height.

Before we can use any of these functions, we will need to set up the fill or stroke style that will be used when drawing to the canvas.

The very basic way to set these styles is to use a color value represented by a 24-bit hex string. Here is an example from our first demonstration:

context.fillStyle = '#000000';
context.strokeStyle = '#ff00ff';

In Example 2-1, the fill style is simply set to be the RGB color black, while the stroke style is a classic purple color. The results are shown in Figure 2-1:

Example 2-1. Basic rectangles
function drawScreen() {
      context.fillStyle  = '#000000';
      context.strokeStyle = '#ff00ff';
      context.lineWidth  = 2;
      context.fillRect(10,10,40,40);
      context.strokeRect(0, 0,60,60);
      context.clearRect(20,20,20,20);

}
Basic rectangles
Figure 2-1. Basic rectangles

The Canvas State

When we draw on the Canvas context, we can make use of a stack of so-called drawing states. Each of these states stores data about the Canvas context at any one time. Here is a list of the data stored in the stack for each state:

  • Transformation matrix information such as rotations or translations using the context.rotate() and context.setTransform() methods

  • The current clipping region

  • The current values for canvas attributes, such as (but not limited to):

    • globalAlpha

    • globalCompositeOperation

    • strokeStyle

    • textAlign, textBaseline

    • lineCap, lineJoin, lineWidth, miterLimit

    • fillStyle

    • font

    • shadowBlur, shadowColor, shadowOffsetX, and shadowOffsetY

We will cover these states later in this chapter.

What’s Not Part of the State?

The current path (which we will explore later in this chapter) and current bitmap (see Chapter 4) being manipulated on the Canvas context are not part of the saved state. This very important feature will allow us to draw and animate individual objects on the canvas. The section Simple Canvas Transformations utilizes the Canvas state to apply transformations to only the current shape being constructed and drawn, leaving the rest of the canvas not transformed.

How Do We Save and Restore the Canvas State?

To save (push) the current state to the stack, call:

context.save()

To restore the canvas by “popping” the last state saved to the stack, use:

context.restore()

Using Paths to Create Lines

Paths are a method we can use to draw any shape on the canvas. A path is simply a list of points, and lines to be drawn between those points. A Canvas context can only have a single “current” path, which is not stored as part of the current drawing state when the context.save() method is called.

Context for paths is a critical concept to understand, because it will enable you to transform only the current path on the canvas.

Starting and Ending a Path

The beginPath() function call starts a path, and the closePath() function call ends the path. When you connect two points inside a path, it is referred to as a subpath. A subpath is considered “closed” if the final point connects to the first point.

Note

The current transformation matrix will affect everything drawn in this path. As we will see when we explore the upcoming section on transformations, we will always want to set the transformation matrix to the identity (or reset) if we do not want any transformation applied to a path.

The Actual Drawing

The most basic path is controlled by a series of moveTo() and lineTo() commands, as shown in Example 2-2.

Example 2-2. A simple line path
function drawScreen() {
   context.strokeStyle  = "black"; //need list of available colors
   context.lineWidth  = 10;
   context.lineCap  = 'square';
   context.beginPath();
   context.moveTo(20, 0);
   context.lineTo(100, 0);
   context.stroke();
   context.closePath();

}

Figure 2-2 shows an example of this output.

A simple line path
Figure 2-2. A simple line path

Example 2-2 simply draws a 10-pixel-wide horizontal line (or stroke) from position 20,0 to position 100,0.

We have also added the lineCap and strokeStyle attributes. Let’s take a brief look at the various attributes we can apply to a line before we move on to some more advanced drawing.

lineCap attributes

context.lineCap

The lineCap is the end of a line drawn on the context. It can be one of three values:

butt

The default; a flat edge that is perpendicular to the edge of the line.

round

A semicircle that will have a diameter that is the length of the line.

square

A rectangle with the length of the line width and the width of half the line width placed flat perpendicular to the edge of the line.

lineJoin attributes

context.lineJoin

The lineJoin is the “corner” that is created when two lines meet. This is called a join. A filled triangle is created at the join, and we can set its basic properties with the lineJoin Canvas attribute.

miter

The default; an edge is drawn at the join. The miterLimit is the maximum allowed ratio of miter length to line width (the default is 10).

bevel

A diagonal edge is drawn at the join.

round

A round edge is drawn at the join.

lineWidth

The lineWidth (default = 1.0) depicts the thickness of the line.

strokeStyle

The strokeStyle defines the color or style that will be used for lines and around shapes (as we saw with the simple rectangles in Example 2-2).

Examples of More Advanced Line Drawing

Example 2-3 shows these attributes in action; the results are depicted in Figure 2-3. There are a few oddities when drawing lines on the canvas, which we will point out along the way.

Example 2-3. Line cap and join
function drawScreen() {

      // Sample 1: round end, bevel join, at top left of canvas
      context.strokeStyle  = "black"; //need list of available colors
      context.lineWidth  = 10;
      context.lineJoin  = 'bevel';
      context.lineCap  = 'round';
      context.beginPath();
      context.moveTo(0, 0);
      context.lineTo(25, 0);
      context.lineTo(25,25);
      context.stroke();
      context.closePath();

      // Sample 2: round end, bevel join, not at top or left of canvas
      context.beginPath();
      context.moveTo(10, 50);
      context.lineTo(35, 50);
      context.lineTo(35,75);
      context.stroke();
      context.closePath();

      // Sample 3: flat end, round join, not at top or left of canvas
      context.lineJoin  = 'round';
      context.lineCap = 'butt';
      context.beginPath();
      context.moveTo(10, 100);
      context.lineTo(35, 100);
      context.lineTo(35,125);
      context.stroke();
      context.closePath();

   }
Line cap and join
Figure 2-3. Line cap and join

These three line and join samples should help illustrate some of the combinations of attributes we can use to draw paths on the canvas.

The first sample attempts to draw starting at the top left of the canvas, resulting in a strange image. Canvas paths are drawn outward in both the x and y directions from the center of the pixel it begins on. For this reason, the top line in Sample 1 seems to be thinner than the 10 pixels we specified. In addition, the “round” end of the top-left horizontal line segment cannot be seen because both of these were drawn off the screen in the “negative” value areas of the screen coordinates. Furthermore, the diagonal “bevel” at the lineJoin is not drawn.

Sample 2 rectifies the problems in Sample 1 by offsetting the beginning of the drawing away from the top left. This allows the entire horizontal line to be drawn, as well as the “round” lineCap and the “bevel” lineJoin.

Sample 3 shows us eliminating the extra lineCap in favor of the default “butt,” and changing the lineJoin to “round.”

Advanced Path Methods

Let’s take a deeper look at some of the other methods we can use to draw paths on the canvas, including arcs and curves that can be combined to create complex images.

Arcs

There are four functions we can use to draw arcs and curves onto the canvas. An arc can be a complete circle or any part of a circle

context.arc()

context.arc(x, y, radius, startAngle, endAngle, anticlockwise)

The x and y values define the center of our circle, and the radius will be the radius of the circle upon which our arc will be drawn. startAngle and endAngle are in radians, not degrees. anticlockwise is a true or false value that defines the direction of the arc.

For example, if we want to draw a circle with a center point at position 100,100 and with a radius of 20, as shown in Figure 2-4, we could use the code below for the contents of drawScreen():

context.arc(100, 100, 20, (Math.PI/180)*0, (Math.PI/180)*360, false);

Example 2-4 illustrates the code necessary to create a simple circle.

Example 2-4. A circle arc
function drawScreen() {

      context.beginPath();
      context.strokeStyle = "black";
      context.lineWidth = 5;
      context.arc(100, 100, 20, (Math.PI/180)*0, (Math.PI/180)*360, false);

      //full circle
      context.stroke();
      context.closePath();

}
A basic circle arc
Figure 2-4. A basic circle arc

Notice that we have to convert our start angle (0) and our end angle (360) into radians by multiplying them by (Math.PI/180). By using 0 as the start angle and 360 as the end, we create a full circle.

We can also draw a segment of a circle by not specifying the entire 0 to 360 start and stop angles. This code for drawScreen() will create one-quarter of a circle drawn clockwise, as shown in Figure 2-5:

context.arc(100, 200, 20, (Math.PI/180)*0, (Math.PI/180)*90, false);
A one-quarter circle arc
Figure 2-5. A one-quarter circle arc

If we want to draw everything but the 0–90 angle, as shown in Figure 2-6, we can employ the anticlockwise argument and set it to true:

context.arc(100, 200, 20, (Math.PI/180)*0, (Math.PI/180)*90, true);
A three-fourths circle arc
Figure 2-6. A three-fourths circle arc

context.arcTo()

context.arcTo(x1, y1, x2, y2, radius)

The arcTo method has only been implemented in the latest browsers—perhaps because its capabilities can be replicated by the arc() function. It takes in a point (x1,y1) and draws a straight line from the current path position to this new position. Then it draws an arc from that point to the y1,y2 point using the given radius.

The context.arcTo method will work only if the current path has at least one subpath. So, let’s start with a line from position 0,0 to position 100,200. Then we will build our small arc. It will look a little like a bent wire coat hanger (for lack of a better description), as shown in Figure 2-7:

context.moveTo(0,0);
context.lineTo(100, 200);
context.arcTo(350,350,100,100,20);
An arcTo() example
Figure 2-7. An arcTo() example

Bezier Curves

Bezier curves, which are far more flexible than arcs, come in both the cubic and quadratic types:

The Bezier curve is defined in 2D space by a “start point,” an “end point,” and one or two “control” points, which determine how the curve will be constructed on the canvas. A normal cubic Bezier curve uses two points, while a quadric version uses a single point.

The quadratic version, shown in Figure 2-8, is the simplest, only needing the end point (last) and a single point in space to use as a control point (first):

context.moveTo(0,0);
context.quadraticCurveTo(100,25,0,50);
A simple quadratic Bezier curve
Figure 2-8. A simple quadratic Bezier curve

This curve starts at 0,0 and ends at 0,50. The point in space we use to create our arc is 100,25. This point is roughly the center of the arc vertically. The 100 value for the single control point pulls the arc out to make an elongated curve.

The cubic Bezier curve offers more options because we have two control points to work with. The result is that curves—such as the classic “S” curve shown in Figure 2-9—are easier to make:

context.moveTo(150,0);
context.bezierCurveTo(0,125,300,175,150,300);
A Bezier curve with two control points
Figure 2-9. A Bezier curve with two control points

The Canvas Clipping Region

Combining the save() and restore() functions with the Canvas clip region limits the drawing area for a path and its subpaths. We do this by first setting rect() to a rectangle that encompasses the region we would like to draw in, and then calling the clip() function. This will set the clip region to be the rectangle we defined with the rect() method call. Now, no matter what we draw onto the current context, it will only display the portion that is in this region. Think of this as a sort of mask that you can use for your drawing operations. Example 2-5 shows how this works, producing the clipped result shown in Figure 2-10.

Example 2-5. The Canvas clipping region
function drawScreen() {

      //draw a big box on the screen
      context.fillStyle = "black";
      context.fillRect(10, 10, 200, 200);
      context.save();
      context.beginPath();

      //clip the canvas to a 50×50 square starting at 0,0
      context.rect(0, 0, 50, 50);
      context.clip();

      //red circle
      context.beginPath();
      context.strokeStyle = "red"; //need list of available colors
      context.lineWidth = 5;
      context.arc(100, 100, 100, (Math.PI/180)*0, (Math.PI/180)*360, false);
      //full circle
      context.stroke();
      context.closePath();

      context.restore();

      //reclip to the entire canvas
      context.beginPath();
      context.rect(0, 0, 500, 500);
      context.clip();

      //draw a blue line that is not clipped
      context.beginPath();
      context.strokeStyle = "blue"; //need list of available colors
      context.lineWidth = 5;
      context.arc(100, 100, 50, (Math.PI/180)*0, (Math.PI/180)*360, false);
      //full circle
      context.stroke();
      context.closePath();


}
The Canvas clipping region
Figure 2-10. The Canvas clipping region

Example 2-5 first draws a large 200×200 black rectangle onto the canvas. Next, we set our Canvas clipping region to rect(0,0,50,50). The clip() call then clips the canvas to those specifications. When we draw our full red circle arc, we only see the portion inside this rectangle. Finally, we set the clipping region back to rect(0,0,500,500) and draw a new blue circle. This time, we can see the entire circle on the canvas.

Note

Other Canvas methods can be used with the clipping region. The most obvious is the arc() function:

arc(float x, float y, float radius, float startAngle,
float endAngle, boolean anticlockwise)

This can be used to create a circular clipping region instead of a rectangular one.

Compositing on the Canvas

Compositing refers to how finely we can control the transparency and layering effects of objects as we draw them to the canvas. There are two attributes we can use to control Canvas compositing operations: globalAlpha and globalCompositeOperation.

globalAlpha

The globalAlpha Canvas property defaults to 1.0 (completely opaque) and can be set from 0.0 (completely transparent) through 1.0. This Canvas property must be set before a shape is drawn to the canvas.

globalCompositeOperation

The globalCompositeOperation value controls how shapes are drawn into the current Canvas bitmap after both globalAlpha and any transformations have been applied (see the next section, Simple Canvas Transformations, for more information).

In the following list, the “source” is the shape we are about to draw to the canvas, and the “destination” refers to the current bitmap displayed on the canvas.

copy

Where they overlap, displays the source and not the destination.

destination-atop

Destination atop the source. Where the source and destination overlap and both are opaque, displays the destination image. Displays the source image wherever the source image is opaque but the destination image is transparent. Displays transparency elsewhere.

destination-in

Destination in the source. Displays the destination image wherever both the destination image and source image are opaque. Displays transparency elsewhere.

destination-out

Destination out source. Displays the destination image wherever the destination image is opaque and the source image is transparent. Displays transparency elsewhere.

destination-over

Destination over the source. Displays the destination image wherever the destination image is opaque. Displays the source image elsewhere.

lighter

Source plus destination. Displays the sum of the source image and destination image, with color values approaching 1.0 as a limit.

source-atop

Source atop the destination. Displays the source image wherever both images are opaque. Displays the destination image wherever the destination image is opaque but the source image is transparent. Displays transparency elsewhere.

source-in

Source in the destination. Displays the source image wherever both the source image and destination image are opaque. Displays transparency elsewhere.

source-out

Source out destination. Displays the source image wherever the source image is opaque and the destination image is transparent. Displays transparency elsewhere.

source-over

(Default.) Source over destination. Displays the source image wherever the source image is opaque. Displays the destination image elsewhere.

xor

Source xor destination. Exclusive OR of the source image and destination image.

Example 2-6 shows how some of these values can affect how shapes are drawn to the canvas, producing Figure 2-11.

Example 2-6. Canvas compositing example
function drawScreen() {

      //draw a big box on the screen
      context.fillStyle = "black"; //
      context.fillRect(10, 10, 200, 200);

      //leave globalCompositeOperation as is
      //now draw a red square
      context.fillStyle = "red";
      context.fillRect(1, 1, 50, 50);

      //now set it to source-over
      context.globalCompositeOperation = "source-over";
      //draw a red square next to the other one
      context.fillRect(60, 1, 50, 50);

      //now set to destination-atop
      context.globalCompositeOperation = "destination-atop";
      context.fillRect(1, 60, 50, 50);

      //now set globalAlpha
      context.globalAlpha = .5;

      //now set to source-atop
      context.globalCompositeOperation = "source-atop";
      context.fillRect(60, 60, 50, 50);

   }
Canvas compositing example
Figure 2-11. Canvas compositing example

As you can see in this example, we have toyed a little with both the globalCompositeOperation and the globalAlpha Canvas properties. When we assign the string source-over, we are essentially resetting the globalCompositeOperation back to the default. We then create some red squares to demonstrate a few of the various compositing options and combinations. Notice that destination-atop switches the newly drawn shapes under the current Canvas bitmap, and that the globalAlpha property only affects shapes that are drawn after it is set. This means we don’t have to save() and restore() the Canvas state to set the next drawn shape to a new transparency value.

In the next section, we will look at some transformations that affect the entire canvas. As a result, if we want to transform only the newly drawn shape, we will have to use the save() and restore() functions.

Simple Canvas Transformations

Transformations on the canvas refer to the mathematical adjustment of physical properties of drawn shapes. The two most commonly used shape transformations are scale and rotate, which we will focus on in this section.

Under the hood, a mathematical matrix operation applies to all transformations. Luckily, you do not need to understand this to use simple Canvas transformations. We will discuss how to apply rotation, translation, and scale transformations by changing simple Canvas properties.

Rotation and Translation Transformations

An object on the canvas is said to be at the 0 angle rotation when it is facing to the left (this is important if an object has a facing; otherwise, we will use this as a guide). Consequently, if we draw an equilateral box (all four sides are the same length), it doesn’t have an initial facing other than one of the flat sides facing to the left. Let’s draw that box for reference:

//now draw a red square
context.fillStyle = "red";
context.fillRect(100,100,50,50);

Now, if we want to rotate the entire canvas 45 degrees, we need to do a couple simple steps. First, we always set the current Canvas transformation to the “identity” (or “reset”) matrix:

context.setTransform(1,0,0,1,0,0);

Because Canvas uses radians, not degrees, to specify its transformations, we need to convert our 45-degree angle into radians:

var angleInRadians = 45 * Math.PI / 180;
context.rotate(angleInRadians);

Lesson 1: Transformations are applied to shapes and paths drawn after the setTransform() or other transformation function is called

If you use this code verbatim, you will see a funny result…nothing! This is because the setTransform() function call only affects shapes drawn to the canvas after it is applied. We drew our square first, then set the transformation properties. This resulted in no change (or transform) to the drawn square. Example 2-7 gives the code in the correct order to produce the expected result, as illustrated in Figure 2-12.

Example 2-7. Simple rotation transformation
function drawScreen() {

      //now draw a red square
      context.setTransform(1,0,0,1,0,0);
      var angleInRadians = 45 * Math.PI / 180;
      context.rotate(angleInRadians);
      context.fillStyle = "red";
      context.fillRect(100,100,50,50);

   }
Simple rotation transformation
Figure 2-12. Simple rotation transformation

We get a result this time, but it will probably differ from what you expect. The red box is rotated, but it looks like the canvas was rotated with it. The entire canvas did not rotate, only the portion drawn after the context.rotate() function was called. So, why did our square both rotate and move off to the left of the screen? The origin of the rotation was set at the “nontranslated” 0,0 position, resulting in the square rotating from the top left of the entire canvas.

Example 2-8 offers a slightly different scenario: draw a black box first, then set the rotation transform, and finally draw the red box again. See the results in Figure 2-13.

Example 2-8. Rotation and the Canvas state
function drawScreen() {

      //draw black square
      context.fillStyle = "black";
      context.fillRect(20,20,25,25);

      //now draw a red square
      context.setTransform(1,0,0,1,0,0);
      var angleInRadians = 45 * Math.PI / 180;
      context.rotate(angleInRadians);
      context.fillStyle = "red";
      context.fillRect(100,100,50,50);

   }
Rotation and the Canvas state
Figure 2-13. Rotation and the Canvas state

The small black square was unaffected by the rotation, so you can see that only the shapes drawn after the context.rotate() function was called were affected.

Again, the red box was moved far off to the left. To reiterate, this occurred because the canvas did not know what origin to use for the rotation. In the absence of an actual translated origin, the 0,0 position setting is applied, resulting in the context.rotate() function rotating “around” the 0,0 point, which brings us to our next lesson.

Lesson 2: We must “translate” the point of origin to the center of our shape to rotate it around its own center

Let’s change Example 2-8 to rotate the red square 45 degrees while keeping it in its current location.

First, we take the numbers we applied to the fillRect() function call to create a few variables to hold the red square’s attributes. This is not necessary, but it will make the code much easier to read and change later:

var x = 100;
var y = 100;
var width = 50;
var height = 50;

Next, using the context.translate() function call, we must change the origin of the canvas to be the center of the red square we want to rotate and draw. This function moves the origin of the canvas to the accepted x and y locations. The center of our red square will now be the desired top-left corner x location for our object (100), plus half the width of our object. Using the variables we created to hold attributes of the red square, this would look like:

x+0.5*width

Next, we must find the y location for the origin translation. This time, we use the y value of the top-left corner of our shape and the height of the shape:

y+.05*height

The translate() function call looks like this:

context.translate(x+.05*width, y+.05*height)

Now that we have translated the canvas to the correct point, we can do our rotation. The code has not changed:

context.rotate(angleInRadians);

Finally, we need to draw our shape. We cannot simply reuse the same values from Example 2-8 because the canvas origin point has moved to the center of the location where we want to draw our object. You can now consider 125,125 as the starting point for all draw operations. We get 125 for x by taking the upper-left corner of the square (100) and adding half its width (25). We do the same for the y origin position. The translate() method call accomplishes this.

We will need to draw the object starting with the correct upper-left coordinates for x and y. We do this by subtracting half the width of our object from the origin x, and half the height of our object from the origin y:

context.fillRect(-0.5*width,-0.5*height, width, height);

Why do we do this? Figure 2-14 illustrates the situation.

Consider that we want to draw our square starting at the top-left corner. If our origin point is at 125,125, the top left is actually 100,100. However, we have translated our origin so the canvas now considers 125,125 to be 0,0. To start our box drawing at the nontranslated canvas, we have to start at –25,–25 on the “translated” canvas.

This forces us to draw our box as though the origin is at 0,0, not 125,125. Therefore, when we do the actual drawing of the box, we must use these coordinates, as shown in Figure 2-15.

The newly translated point
Figure 2-14. The newly translated point
Drawing with a translated point
Figure 2-15. Drawing with a translated point

In summary, we needed to change the point of origin to the center of our square so it would rotate around that point. But when we draw the square, we need our code to act as though the (125,125) point is actually (0,0). If we had not translated the origin, we could have used the (125,125) point as the center of our square (as in Figure 2-14). Example 2-9 demonstrates how this works, creating the result shown in Figure 2-16.

Example 2-9. Rotation around the center point
function drawScreen() {

      //draw black square
      context.fillStyle = "black";
      context.fillRect(20,20 ,25,25);

      //now draw a red square
      context.setTransform(1,0,0,1,0,0);
      var angleInRadians = 45 * Math.PI / 180;
      var x = 100;
      var y = 100;
      var width = 50;
      var height = 50;
      context.translate(x+.5*width, y+.5*height);
      context.rotate(angleInRadians);
      context.fillStyle = "red";
      context.fillRect(-.5*width,-.5*height , width, height);

   }
Rotation around the center point
Figure 2-16. Rotation around the center point

Let’s look at one final rotation example. Example 2-10 takes Example 2-9 and simply adds four separate 40×40 squares to the canvas, rotating each one slightly. The result is shown in Figure 2-17.

Example 2-10. Multiple rotated squares
function drawScreen() {

      //now draw a red square
      context.setTransform(1,0,0,1,0,0);
      var angleInRadians = 45 * Math.PI / 180;
      var x = 50;
      var y = 100;
      var width = 40;
      var height = 40;
      context.translate(x+.5*width, y+.5*height);
      context.rotate(angleInRadians);
      context.fillStyle = "red";
      context.fillRect(-.5*width,-.5*height , width, height);

      context.setTransform(1,0,0,1,0,0);
      var angleInRadians = 75 * Math.PI / 180;
      var x = 100;
      var y = 100;
      var width = 40;
      var height = 40;
      context.translate(x+.5*width, y+.5*height);
      context.rotate(angleInRadians);
      context.fillStyle = "red";
      context.fillRect(-.5*width,-.5*height , width, height);

      context.setTransform(1,0,0,1,0,0);
      var angleInRadians = 90 * Math.PI / 180;
      var x = 150;
      var y = 100;
      var width = 40;
      var height = 40;
      context.translate(x+.5*width, y+.5*height);
      context.rotate(angleInRadians);
      context.fillStyle = "red";
      context.fillRect(-.5*width,-.5*height , width, height);
      context.setTransform(1,0,0,1,0,0);
      var angleInRadians = 120 * Math.PI / 180;
      var x = 200;
      var y = 100;
      var width = 40;
      var height = 40;
      context.translate(x+.5*width, y+.5*height);
      context.rotate(angleInRadians);
      context.fillStyle = "red";
      context.fillRect(-.5*width,-.5*height , width, height);

}
Multiple rotated squares
Figure 2-17. Multiple rotated squares

Next, we will examine scale transformations.

Scale Transformations

The context.scale() function takes in two parameters: the first is the scale attribute for the x-axis, and the second is the scale attribute for the y-axis. The value 1 is the normal scale for an object. Therefore, if we want to double an object’s size, we can set both values to 2. Using the code below in drawScreen() produces the red square shown in Figure 2-18:

context.setTransform(1,0,0,1,0,0);
context.scale(2,2);
context.fillStyle = "red";
context.fillRect(100,100 ,50,50);
A simple scaled square
Figure 2-18. A simple scaled square

If you test this code, you will find that scale works in a similar manner as rotation. We did not translate the origin of the scale point to double the size of the square; rather, we used the top-left corner of the canvas as the origin point. The result is that the red square appears to move farther down and to the left. What we would like is for the red square to remain in place and to scale from its center. We do this by translating to the center of the square before we scale, and by drawing the square around this center point (just as we did in Example 2-9). Example 2-11 produces the result shown in Figure 2-19.

Example 2-11. Scale from the center point
function drawScreen() {

      //now draw a red square
      context.setTransform(1,0,0,1,0,0);
      var x = 100;
      var y = 100;
      var width = 50;
      var height = 50;
      context.translate(x+.5*width, y+.5*height);
      context.scale(2,2);
      context.fillStyle = "red";
      context.fillRect(-.5*width,-.5*height , width, height);

   }
Scale from the center point
Figure 2-19. Scale from the center point

Combining Scale and Rotation Transformations

If we want to both scale and rotate an object, Canvas transformations can easily be combined to achieve the desired results (as shown in Figure 2-20). Let’s look in Example 2-12 at how we might combine them by using scale(2,2) and rotate(angleInRadians) from our previous examples.

Example 2-12. Scale and rotation combined
function drawScreen() {
      context.setTransform(1,0,0,1,0,0);
      var angleInRadians = 45 * Math.PI / 180;
      var x = 100;
      var y = 100;
      var width = 50;
      var height = 50;
      context.translate(x+.5*width, y+.5*height);
      context.scale(2,2);
      context.rotate(angleInRadians);
      context.fillStyle = "red";
      context.fillRect(-.5*width,-.5*height , width, height);

   }
Scale and rotation combined
Figure 2-20. Scale and rotation combined

Example 2-13 also combines rotation and scale, this time using a rectangle. Figure 2-21 reveals what it creates.

Example 2-13. Scale and rotate a nonsquare object
function drawScreen() {

      //now draw a red rectangle
      context.setTransform(1,0,0,1,0,0);
      var angleInRadians = 90 * Math.PI / 180;
      var x = 100;
      var y = 100;
      var width = 100;
      var height = 50;
      context.translate(x+.5*width, y+.5*height);
      context.rotate(angleInRadians);
      context.scale(2,2);

      context.fillStyle = "red";
      context.fillRect(-.5*width,-.5*height , width, height);

   }
Scale and rotate a nonsquare object
Figure 2-21. Scale and rotate a nonsquare object
The bounding box of a complex shape
Figure 2-22. The bounding box of a complex shape

Filling Objects with Colors and Gradients

In this chapter, we have quickly looked at color and fill styles as we proceeded through the discussions of basic and complex shape construction. In this section, we will take a deeper look at coloring and filling shapes we draw on the canvas. In addition to these simple colors and fills, there are a number of different gradient styles that we can employ. Furthermore, Canvas also has a method to fill shapes with bitmap images (see Chapter 4).

Setting Basic Fill Colors

The Canvas fillStyle property is used to set a basic color for filling shapes on the canvas. We saw this earlier in the chapter when we used simple color names for our fillStyle. An example is:

context.fillStyle = "red";

Below is a list of the usable color string values from the HTML4 specification. As of this writing, the HTML5 color specification has not been set. In the absence of any additional HTML5-specific colors, the HTML4 colors will work properly in HTML5:

Black = #000000
Green = #008000
Silver = #C0C0C0
Lime = #00FF00
Gray = #808080
Olive = #808000
White = #FFFFFF
Yellow = #FFFF00
Maroon = #800000
Navy = #000080
Red = #FF0000
Blue = #0000FF
Purple = #800080
Teal = #008080
Fuchsia = #FF00FF
Aqua = #00FFFF

Note

All these color values will work with the strokeStyle as well as the fillStyle.

Of course, using a string for the color name is not the only available method of specifying a solid color fill. The list below includes a few other methods:

Setting the fill color with the rgb() method

The rgb() method lets us use the 24-bit RGB value when specifying our fill colors:

context.fillStyle = rgb(255,0,0);

This will result in the same red color as the string value above.

Setting the fill color with a hex number string

We can also set the fillStyle color with a hex number in a string:

context.fillStyle = "#ff0000";
Setting the fill color with the rgba() method

The rgba() method allows us to specify a 32-bit color value with the final 8 bits representing the alpha value of the fill color:

context.fillStyle = rgba(255,0,0,1);

The alpha value can be from 1 (opaque) to 0 (transparent).

Filling Shapes with Gradients

There are two basic options for creating gradient fills on the canvas: linear and radial. A linear gradient creates a horizontal, vertical, or diagonal fill pattern; the radial variety creates a fill that “radiates” from a central point in a circular fashion. Let’s look at some examples of each.

Linear gradients

Linear gradients come in three basic styles: horizontal, vertical, and diagonal. We control where colors change in our gradient by setting color stops at points along the length of the object we wish to fill.

Linear horizontal gradients

Example 2-14 creates a simple horizontal gradient, as shown in Figure 2-23.

Example 2-14. A linear horizontal gradient
function drawScreen() {

      // horizontal gradient values must remain 0
      var gr = context.createLinearGradient(0, 0, 100, 0);

      // Add the color stops.
      gr.addColorStop(0,'rgb(255,0,0)');
      gr.addColorStop(.5,'rgb(0,255,0)');
      gr.addColorStop(1,'rgb(255,0,0)');

      // Use the gradient for the fillStyle.
      context.fillStyle = gr;
      context.fillRect(0, 0,100,100);

   }
A linear horizontal gradient
Figure 2-23. A linear horizontal gradient

To create the horizontal gradient, we must first create a variable (gr) to reference the new gradient. Here’s how we set it:

var gr = context.createLinearGradient(0,0,100,0);

The four parameter values in the createLinearGradient method call are the top-left x and y coordinates to start the gradient, as well as the two bottom-right points to end the gradient. Our example starts at 0,0 and goes to 100,0. Notice that the y values are both 0 when we create a horizontal gradient; the opposite will be true when we create a vertical gradient.

Once we have defined the size of our gradient, we then add in color stops that take two parameter values. The first is a relative position origin point along the gradient to start with color, and the second is the color to use. The relative position must be a value from 0.0 to 1.0:

gr.addColorStop(0,'rgb(255,0,0)');
gr.addColorStop(.5,'rgb(0,255,0)');
gr.addColorStop(1,'rgb(255,0,0)');

Therefore, in Example 2-14, we have set a red color at 0, a green color at .5 (the center), and another red color at 1. This will fill our shape with a relatively even red to green to red gradient.

Next, we need to get the context.fillStyle to be the gradient we just created:

context.fillStyle = gr;

Finally, we create a rectangle on the canvas:

context.fillRect(0, 0, 100, 100);

Notice that we created a rectangle that was the exact size of our gradient. We can change the size of the output rectangle like this:

context.fillRect(0, 100, 50, 100);
context.fillRect(0, 200, 200, 100);

Example 2-15 adds these two new filled rectangles to Example 2-14 to create Figure 2-24. Notice that the gradient fills up the available space, with the final color filling out the area larger than the defined gradient size.

Example 2-15. Multiple gradient-filled objects
function drawScreen() {

   var gr = context.createLinearGradient(0, 0, 100, 0);

   // Add the color stops.
   gr.addColorStop(0,'rgb(255,0,0)');
   gr.addColorStop(.5,'rgb(0,255,0)');
   gr.addColorStop(1,'rgb(255,0,0)');

   // Use the gradient for the fillStyle.
   context.fillStyle = gr;
   context.fillRect(0, 0, 100, 100);
   context.fillRect(0, 100, 50, 100);
   context.fillRect(0, 200, 200, 100);

    }
Linear horizontal gradient on multiple objects
Figure 2-24. Linear horizontal gradient on multiple objects
Applying a horizontal gradient to a stroke

Gradients can be applied to any shape—even the stroke around a shape. Example 2-16 takes the filled rectangles from Example 2-15 and creates a strokeRect shape instead of a filled rectangle. Figure 2-25 shows the very different result.

Example 2-16. A horizontal stroke gradient
function drawScreen() {

      var gr = context.createLinearGradient(0, 0, 100, 0);

      // Add the color stops.
      gr.addColorStop(0,'rgb(255,0,0)');
      gr.addColorStop(.5,'rgb(0,255,0)');
      gr.addColorStop(1,'rgb(255,0,0)');

      // Use the gradient for the fillStyle.
      context.strokeStyle = gr;
      context.strokeRect(0, 0, 100, 100);
      context.strokeRect(0, 100, 50, 100);
      context.strokeRect(0, 200, 200, 100);

   }
Horizontal stroke gradients
Figure 2-25. Horizontal stroke gradients
Applying a horizontal gradient to a complex shape

We can also apply a linear gradient to a “closed” shape made up of points, as shown in Example 2-17. A shape is considered closed when the final point is the same as the starting point.

Example 2-17. Horizontal gradient on a complex shape
function drawScreen() {

      var gr = context.createLinearGradient(0, 0, 100, 0);

      // Add the color stops.
      gr.addColorStop(0,'rgb(255,0,0)');
      gr.addColorStop(.5,'rgb(0,255,0)');
      gr.addColorStop(1,'rgb(255,0,0)');

      // Use the gradient for the fillStyle.
      context.fillStyle = gr;
      context.beginPath();
      context.moveTo(0,0);
      context.lineTo(50,0);
      context.lineTo(100,50);
      context.lineTo(50,100);
      context.lineTo(0,100);
      context.lineTo(0,0);
      context.stroke();
      context.fill();
      context.closePath();

   }

In this example, we use the context.fill() command to fill in our shape with the current fillStyle, creating the output shown in Figure 2-26.

A horizontal gradient on a complex shape
Figure 2-26. A horizontal gradient on a complex shape

Figure 2-26 shows the new shape we have created with points. As long as the points are closed, the fill will work as we expect.

Vertical gradients

Vertical gradients are created in a very similar manner as the horizontal variety. The difference is that we must specify a y value that is not 0, and the x values must both be 0. Example 2-18 shows the shape from Example 2-17 created with a vertical rather than a horizontal gradient to produce the output in Figure 2-27.

Example 2-18. Vertical gradients
function drawScreen() {

      var gr = context.createLinearGradient(0, 0, 0, 100);

      // Add the color stops.
      gr.addColorStop(0,'rgb(255,0,0)');
      gr.addColorStop(.5,'rgb(0,255,0)');
      gr.addColorStop(1,'rgb(255,0,0)');

      // Use the gradient for the fillStyle.
      context.fillStyle = gr;
      context.beginPath();
      context.moveTo(0,0);
      context.lineTo(50,0);
      context.lineTo(100,50);
      context.lineTo(50,100);
      context.lineTo(0,100);
      context.lineTo(0,0);
      //context.stroke();
      context.fill();
      context.closePath();

   }
A vertical gradient example
Figure 2-27. A vertical gradient example

The only difference between Example 2-18 and Example 2-17 is the line creating the linear gradient.

The horizontal version (Example 2-17):

var gr = context.createLinearGradient(0, 0, 100, 0);

The new vertical version (Example 2-18):

var gr = context.createLinearGradient(0, 0, 0, 100);

All of the same rules for strokes on horizontal gradients apply to vertical ones. Example 2-19 takes the shape from Example 2-18, stroking it with the gradient instead of filling it, producing the outline shown in Figure 2-28.

Example 2-19. A vertical gradient stroke
function drawScreen() {

      var gr = context.createLinearGradient(0, 0, 0, 100);

      // Add the color stops.
      gr.addColorStop(0,'rgb(255,0,0)');
      gr.addColorStop(.5,'rgb(0,255,0)');
      gr.addColorStop(1,'rgb(255,0,0)');

      // Use the gradient for the fillStyle.
      context.strokeStyle = gr;
      context.beginPath();
      context.moveTo(0,0);
      context.lineTo(50,0);
      context.lineTo(100,50);
      context.lineTo(50,100);
      context.lineTo(0,100);
      context.lineTo(0,0);
      context.stroke();
      context.closePath();

   }
A vertical gradient stroke
Figure 2-28. A vertical gradient stroke
Diagonal gradients

You can easily create a diagonal gradient by varying both the second x and second y parameters of the createLinearGradient() function:

var gr= context.createLinearGradient(0, 0, 100, 100);

To create a perfect diagonal gradient, as shown in Figure 2-29, fill a square that is the same size as the diagonal gradient. The code is provided in Example 2-20.

Example 2-20. A diagonal gradient
function drawScreen() {

      var gr = context.createLinearGradient(0, 0, 100, 100);

      // Add the color stops.
      gr.addColorStop(0,'rgb(255,0,0)');
      gr.addColorStop(.5,'rgb(0,255,0)');
      gr.addColorStop(1,'rgb(255,0,0)');

      // Use the gradient for the fillStyle.
      context.fillStyle = gr;
      context.beginPath();
      context.moveTo(0,0);
      context.fillRect(0,0,100,100)
      context.closePath();

   }
A diagonal gradient example
Figure 2-29. A diagonal gradient example

Radial gradients

The definition process for radial and linear gradients is very similar. Although a radial gradient takes six parameters to initialize rather than the four needed for a linear gradient, it uses the same color stop idea to create the color changes.

The six parameters are used to define the center point and the radii of two circles. The first circle is the “start” circle, and the second circle is the “end” circle. Let’s look at an example:

var gr = context.createRadialGradient(50,50,25,50,50,100);

The first circle has a center point of 50,50 and a radius of 25; the second has a center point of 50,50 and a radius of 100. This will effectively create two concentric circles.

We set color stops the same way we did with the linear gradients:

gr.addColorStop(0,'rgb(255,0,0)');
gr.addColorStop(.5,'rgb(0,255,0)');
gr.addColorStop(1,'rgb(255,0,0)');

Example 2-21 puts this together to create the result shown in Figure 2-30.

Example 2-21. A simple radial gradient
function drawScreen() {

      var gr = context.createRadialGradient(50,50,25,50,50,100);

      // Add the color stops.
      gr.addColorStop(0,'rgb(255,0,0)');
      gr.addColorStop(.5,'rgb(0,255,0)');
      gr.addColorStop(1,'rgb(255,0,0)');

      // Use the gradient for the fillStyle.
      context.fillStyle = gr;
      context.fillRect(0, 0, 200, 200);

   }
A simple radial gradient
Figure 2-30. A simple radial gradient

Example 2-22 offsets the second circle from the first to create the effects shown in Figure 2-31.

Example 2-22. A complex radial gradient
function drawScreen() {

      var gr = context.createRadialGradient(50,50,25,100,100,100);

      // Add the color stops.
      gr.addColorStop(0,'rgb(255,0,0)');
      gr.addColorStop(.5,'rgb(0,255,0)');
      gr.addColorStop(1,'rgb(255,0,0)');

      // Use the gradient for the fillStyle.
      context.fillStyle = gr;
      context.fillRect(0, 0, 200, 200);

}
A complex radial gradient
Figure 2-31. A complex radial gradient

As with the linear gradients, we can also apply the radial gradients to complex shapes. Example 2-23 takes an arc example from earlier in this chapter, but applies a radial gradient to create Figure 2-32.

Example 2-23. A radial gradient applied to a circle
function drawScreen() {

      var gr = context.createRadialGradient(50,50,25,100,100,100);

      // Add the color stops.
      gr.addColorStop(0,'rgb(255,0,0)');
      gr.addColorStop(.5,'rgb(0,255,0)');
      gr.addColorStop(1,'rgb(255,0,0)');

      // Use the gradient for the fillStyle.
      context.fillStyle = gr;
      context.arc(100, 100, 100, (Math.PI/180)*0, (Math.PI/180)*360, false);
      context.fill();

   }
A radial gradient applied to a circle
Figure 2-32. A radial gradient applied to a circle

Example 2-23 takes the radial gradient from Example 2-22 and applies it to a circle shape rather than a rectangle shape. This removes the red square from the background of the shape.

We can also apply our radial gradient to the stroke of our arc rather than the fill, as shown in Example 2-24 and Figure 2-33.

Example 2-24. An arc stroke gradient
function drawScreen() {

      var gr = context.createRadialGradient(50,50,25,100,100,100);

      // Add the color stops.
      gr.addColorStop(0,'rgb(255,0,0)');
      gr.addColorStop(.5,'rgb(0,255,0)');
      gr.addColorStop(1,'rgb(255,0,0)');

      // Use the gradient for the fillStyle.
      context.strokeStyle = gr;
      context.arc(100, 100, 50, (Math.PI/180)*0, (Math.PI/180)*360, false)
      context.stroke();

   }
An arc stroke gradient
Figure 2-33. An arc stroke gradient

Example 2-24 created a circle that is smaller than the version in Example 2-23, so the radial gradient would show up on the stroke of the arc. If we left it the same size as Example 2-23, we would have a solid red fill because the radial gradient is solid red at the diameter edge of the circle.

Filling Shapes with Patterns

We will cover using bitmap images on the canvas in Chapter 4, but for now, let’s take a quick look at how images can be used as fill patterns for shapes we draw.

Fill patterns are initialized with the createPattern() function, which takes two parameters. The first is an Image object instance, and the second is a String representing how to display the repeat pattern inside the shape. We can use a loaded image file or an entire other canvas as a fill pattern for a drawn shape.

There are currently four types of image fills:

  • repeat

  • repeat-x

  • repeat-y

  • no-repeat

Modern browsers have implemented these four types to various degrees, but standard repeat seems to be the most common. Let’s look at it now and then we will take a brief look at the other three.

Figure 2-34 shows a simple bitmap fill pattern that we can use to test this functionality. It is a 20×20 green circle on a transparent background, saved as a .gif file named fill_20x20.gif.

The fill_20x20.gif image for our fill
Figure 2-34. The fill_20x20.gif image for our fill

Example 2-25 tests this first with the repeat string to create a box full of little green circles, as shown in Figure 2-35.

Example 2-25. Filling with an image file using repeat
function drawScreen() {

      var fillImg = new Image();
      fillImg.src = 'fill_20x20.gif';
      fillImg.onload = function(){

         var fillPattern = context.createPattern(fillImg,'repeat');
         context.fillStyle = fillPattern;
         context.fillRect(0,0,200,200);

      }

   }
repeat fill example
Figure 2-35. repeat fill example

It is best not to use Image instances until they have loaded completely. We will cover this in detail in Chapter 4, but for now, we simply create an inline onload event handler function that will be called when Image is ready to be used. The repeat pattern string does a good job of completely filling the 200×200 square. Let’s see the code for how the other repeat strings perform (in Example 2-26), and view the results in Figures 2-36 through 2-38.

Example 2-26. Using the no-repeat, repeat-x, and repeat-y strings
function drawScreen() {


      var fillImg = new Image();
      fillImg.src = 'fill_20x20.gif';

      fillImg.onload = function(){

         var fillPattern1 = context.createPattern(fillImg,'no-repeat');
         var fillPattern2 = context.createPattern(fillImg,'repeat-x');
         var fillPattern3 = context.createPattern(fillImg,'repeat-y');

         context.fillStyle = fillPattern1;
         context.fillRect(0,0,100,100);

         context.fillStyle = fillPattern2;
         context.fillRect(0,110,100,100);

         context.fillStyle = fillPattern3;
         context.fillRect(0,220,100,100);

      }

   }

Note

Each browser will show these patterns in a different manner.

no-repeat, repeat-x, and repeat-y in Safari
Figure 2-36. no-repeat, repeat-x, and repeat-y in Safari
no-repeat, repeat-x, and repeat-y in Firefox
Figure 2-37. no-repeat, repeat-x, and repeat-y in Firefox
no-repeat, repeat-x, and repeat-y in Chrome
Figure 2-38. no-repeat, repeat-x, and repeat-y in Chrome

Only Firefox seems to show anything of significance when the repeat-x and repeat-y strings are used in the repeat parameter. We will cover more examples of filling, as well as many other uses for bitmap images, in Chapter 4.

Creating Shadows on Canvas Shapes

We can add shadows to shapes we draw on the canvas using four parameters. As with the tiled fill patterns in the previous section, this feature has not been fully implemented on all HTML5-compliant browsers.

We add a shadow by setting four Canvas properties:

  • shadowOffsetX

  • shadowOffsetY

  • shadowBlur

  • shadowColor

The shadowOffsetX and shadowOffsetY values can be positive or negative. Negative values will create shadows to the left and top rather than to the bottom and right. The shadowBlur property sets the size of the blurring effect on the shadow. None of these three parameters is affected by the current Canvas transformation matrix. The shadowColor can be any color set via HTML4 color constant string—rgb() or rgba()—or with a string containing a hex value.

Example 2-27 and Figure 2-39 show a few different boxes drawn with various shadow settings.

Adding shadows to drawn objects
Figure 2-39. Adding shadows to drawn objects
Example 2-27. Adding shadows to drawn objects
function drawScreen() {

     context.fillStyle = 'red';

     context.shadowOffsetX = 4;
     context.shadowOffsetY = 4;
     context.shadowColor = 'black';
     context.shadowBlur = 4;
     context.fillRect(10,10,100,100);

     context.shadowOffsetX = -4;
     context.shadowOffsetY = -4;
     context.shadowColor = 'black';
     context.shadowBlur = 4;
     context.fillRect(150,10,100,100);

     context.shadowOffsetX = 10;
     context.shadowOffsetY = 10;
     context.shadowColor = 'rgb(100,100,100)';
     context.shadowBlur = 8;
     context.arc(200, 300, 100, (Math.PI/180)*0, (Math.PI/180)*360, false)
     context.fill();
}

As you can see, if we adjust the shadowOffset values along with the shadowBlur value, we create various shadows. We can also create shadows for complex shapes drawn with paths and arcs.

What’s Next

We covered a lot of ground in this chapter, introducing the ways to construct primitive and complex shapes, and how we can draw and transform them on the canvas. We also discussed how to composite, rotate, scale, translate, fill, and create shadows on these shapes. But we’ve only just begun exploring HTML5 Canvas. In the next chapter, we will look at how to create and manipulate text objects on the canvas.

Get HTML5 Canvas now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.