Drawing Lines, Paths, and Curves - HTML5 and the Canvas - HTML5, 20 Lessons to Successful Web Development (2015)

HTML5, 20 Lessons to Successful Web Development (2015)

PART II HTML5 and the Canvas

LESSON 12 Drawing Lines, Paths, and Curves

Images

To view the accompanying video for this lesson, please visit mhprofessional.com/nixonhtml5/.

In this lesson I’ll be showing you how you can customize all the functions that draw using lines, such as strokeRect() and strokeText() (which you’ve already seen), as well as the line-drawing functions, and how to use paths to create complicated patterns and curves. All example files used in this (and every other) lesson can be downloaded from 20lessons.com.

Drawing Lines

The HTML5 canvas supports line drawing using many different styles because you can precisely specify the width of lines with the lineWidth property (as you have previously seen), and can also set properties such as lineCap, lineJoin, and miterLimit.

The lineWidth Property

You previously encountered the lineWidth property in Lesson 11 where it was used to thicken the line width used in a call to the strokeText() function. What this property does is change the line width for future operations that use line drawing, including strokeText() and stroke() (detailed a little later).

For example, the following command sets the lineWidth property to 10 pixels, as seen in the horizontal and angled lines in Figure 12-1 (for comparison the thin, vertical lines in the figure are one pixel wide):

context.lineWidth = 10

Images

FIGURE 12-1 A selection of line types using different line caps and joins

The lineCap Property

Using the lineCap property, you can choose the way the starts and ends of lines are displayed. This is known as their line cap, and hence the lineCap property name, which can be any value out of butt, round, and square, as shown in the left half of Figure 12-1, and used like these examples:

Images

The top line on the left of the figure uses the lineCap value of butt in which the ends butt up exactly against the vertical lines I have drawn (for comparing the line cap types), indicating the start and end points of each line. If no value is given to the lineCap property, it assumes a default value of butt.

The middle line uses a lineCap property of round and, as you can see, it therefore extends past the left and right edges, with the center point of the rounded cap being the end points of the line.

The bottom line uses the value square for the lineCap property, which is almost the same as round, in that the centers of the squares are the end points of the line.

The lineJoin Property

The lineJoin property is similar to the lineCap property, but it applies only at the points at which lines are joined. It supports the values of round, bevel, and miter, as you can see by looking at the joins of the three right-hand pairs of lines in Figure 12-1, in which the end caps are the same as the lines on the left. Following are examples of setting this property:

Images

However, the top pair of lines uses the value round for the lineJoin property, the middle pair uses the value bevel, and the bottom one uses the value miter.

The miterLimit Property

In order to achieve the sharp miter in the bottom-right pair of lines in Figure 12-1, it was necessary to use the miterLimit property, giving it a value of 12, in order to allow the quite sharp angle to extend far enough. Here is how you would set this property:

context.miterLimit = 12

Images

If miterLimit is not set to a sufficiently large enough value for a miter, then mitered joins will simply use the bevel value instead, so if you are having trouble with your miters, simply increase the value you supply for miterLimit until the miter displays.

Drawing with Paths

Figure 12-1 was created using a combination of line properties (as described in the previous section), along with a combination of path-handling functions. Using them, it is easy to move an imaginary pen to a start location, define a path it must follow, and then tell it to draw along that path using the properties already set up for it such as width, caps, joins, and color, as described next.

The beginPath() and closePath() Functions

Every path created for an HTML5 canvas must start with a call to beginPath(), and end with a call to closePath(), like this:

Images

Think of them as being like opening and closing HTML tags. Once a path is created, you can make it display, but first let’s look at how to create one.

The moveTo() and lineTo() Functions

The first step in a path is generally to move to a location on the canvas so, for example, to move to the location 20,20, you would issue this command:

context.moveTo(20, 20)

To then specify that a line should be drawn (once the path is completed), you can then issue a command such as the following, which will specify that the next part of the path is to draw a line from the current location 20,20 to 390,20:

context.lineTo(390, 20)

Let’s look at the path used to draw a rectangle, including the opening and closing path function calls:

Images

The stroke() Function

Once you have a path created, you can draw it on the canvas using the stroke() function like this, which in the case of the current example displays as Figure 12-2:

context.stroke()

Images

FIGURE 12-2 A rectangle drawn using a path

And there you have it. The path has been processed by the stroke() function and all the parts in the path are now drawn.

The rect() Function

If all you wanted to draw in the first instance was a rectangle, then there’s a quicker way to do this than defining an entire path. Instead you can use the rect() function with a path, like this:

Images

The top left-hand corner of the rectangle is specified by the first two arguments in the function call, and the second two contain the width and height of the rectangle. The end result is shown in Figure 12-3, in which both rectangles (from the previous and current examples) have been combined and created within the same path, as follows:

Images

Images

FIGURE 12-3 The two rectangles are created from a single path.

The fill() Function

Using the fill() function, you can fill in any area bounded by a path. For example, the following code creates a four-pointed star, which is then filled in, as shown in Figure 12-4:

Images

Images

FIGURE 12-4 Filling in a four-pointed star

If you don’t fully enclose the shape by drawing a line back to the start point, the function still does a very good attempt at filling only the shape by making that final link for you before performing the fill.

The clip() Function

When creating a path, you can choose to constrain the drawing area using the clip() function to select any area of the canvas, so that any part that would be drawn outside of this area will be ignored, and only parts of the path that fall inside the clipped area will be used.

The clip() function works on a path in the same way as the stroke() or fill() functions. For example, the following code creates a diamond-shaped path, which is then revealed with a simple call to stroke(), as shown in Figure 12-5, in which the diamond has been drawn over the star shape:

Images

Images

FIGURE 12-5 The diamond path is drawn on top of the star pattern.

However, by placing the diamond path before the star shape is drawn, and using the clip() function on it after the stroke() function, this path becomes a bounded area, outside of which future path-related functions cannot draw, as with the following code (the result of which is shown inFigure 12-6):

Images

Images

Images

FIGURE 12-6 The diamond is both drawn and used in a clip() call.

If you want to use a path for constraint only, and not actually draw it, simply omit the call to stroke() from the previous example, and the result is Figure 12-7.

Images

FIGURE 12-7 Only the star shape is drawn, constrained by the diamond area.

Or, perhaps you simply may wish to give the diamond shape a border, fill it with one color, and then fill the area of the star within that shape in another color, which is easily achieved by placing the relevant fillStyle assignments before the drawing commands, as shown in Figure 12-8. I’ll leave it up to you to work out how to achieve this effect—it’s very simple (or you can view the commented code in the accompanying example files, downloadable using the link at the start of this lesson).

Images

FIGURE 12-8 The diamond is filled, as is the portion of the star within it.

Images

You may, of course, use any types of fill on a path as well as the solid color fills, including linear and radial gradients and patterns. Simply assign the relevant value to the fillStyle property before making a fill.

The isPointInPath() Function

Sometimes you need to know whether a particular point lies in a path you have constructed. However, you will normally only want to use this function if you are quite proficient with JavaScript. You will generally call it as part of a conditional statement, like this:

Images

If the location specified lies along any of the points in the path, the function returns the value true and the contents of the if() statement are executed. Otherwise, the value false is returned and the contents of the if() statement do not get executed.

Creating Curves

I leapt a little ahead of myself by showing you how to fill in and clip paths, but I wanted to show you some of the fun you could have with them and I couldn’t resist. So now (slightly out of order), here are some more path functions you can use, this time for creating arcs, circles, and complex curves.

As with the previous examples, all of these can be filled in with plain colors, gradient or radial fills and patterns, or you can draw curves using stroke functions and their associated properties such as lineWidth, lineCap, and lineJoin.

The arc() Function

Probably the simplest form of curve is the arc, which is simply a segment of the perimeter of a circle. To create an arc, you include it within a path with the start of the curve connected to the final point on the path previous to it, and the curve’s end connected to the next point in the path after it.

Images

It is possible to create an arc without using the path functions, but a path will be assumed based on the previous and future drawing points, and these will connect up to it. So for precise control, I recommend always using it inside a path.

You must provide six arguments to the function: a pair of coordinates representing the center of the circle upon which the arc is based, the radius required, the radian offset value for the start of the arc, a radiant offset value for the end of the arc, and then a value indicating whether to draw the arc clockwise or counterclockwise. Let’s look at these in turn.

X and Y coordinates The coordinates for an arc are simply the horizontal and vertical offset from the top-left corner of the canvas for the center of the circle, such as 205,85, which is 205 pixels in from the left, and 85 pixels down from the top of the canvas.

Radius This is a value in pixels representing the distance from the center of the circle to its perimeter (or circumference). This is the location at which the arc will be drawn. For example, the value 75 states that the arc will be drawn at a distance of 75 pixels from the center of the circle.

Radian offsets These specify the start and end position on the circle’s perimeter between which the arc should be created. A value of 0 radians specifies the three o’clock position directly to the right of the circle’s center. A radian has the value 180 ÷ Δ (the equivalent of about 57 degrees), and so there are Π × 2 radians in a complete circle of 360 degrees. This means that to draw a quarter circle (for example), from the three o’clock to six o’clock positions, you would use an initial value of 0 radians, and a second value of Δ ÷ 2 radians. For a semicircle, the second value would be Δ radians, and for a circle it would be Π × 2 radians. Remember that Π is the number of times the diameter of a circle fits into its circumference (or about 3.1415927 in decimal).

Direction To create a clockwise arc, the final argument must have a value of false, which is the default value if you omit this argument. For a counterclockwise arc, it should be true.

So, for example, the following code draws four segments of a pie, with the final one filled in using the fill() function, rather than drawn using the stroke() function, as shown in Figure 12-9:

Images

Images

FIGURE 12-9 Arcs created with the arc() function

Math.PI is a convenient way to refer to the value of Π using JavaScript. The first image in the figure uses radian values of 0 and Math.PI / 2, the second image, uses 0 and Math.PI, the third image, uses 0 and Math.PI / 2 * 3, and the fourth image uses Math.PI * 2. In each case, a call tomoveTo() moves the path starting point to the center of the circle, then the next point in the path is the start of the arc, followed by the arc’s end, and then the initial starting point again. By doing this, a slice of the circle is created to clearly show the arcs. In the final image the fill() function was used to illustrate how you can use that instead of stroke() if you wish, as well as the linear, gradient, and pattern fills.

If you wish to draw only the arc portion of the images in Figure 12-9, then you need to close the path after issuing the call to the stroke() function, and you do not need to first move the path start to the center of each circle. So you could use code such as this (which results in Figure 12-10):

Images

Images

FIGURE 12-10 Only the arcs are now drawn.

For this example, I chose not to fill in the final circle so you can see how to draw a complete, outlined circle. Remember too that you can change the stroke width and other properties by assigning the relevant values to the strokeStyle property.

If you wish to draw the arcs in a counterclockwise direction, you can change the final argument in the call to arc() to true. The result is shown in Figure 12-11, in which you will note that you always get a full circle for image four, regardless of the direction of drawing.

Images

FIGURE 12-11 Drawing the arcs in a counterclockwise direction

The arcTo() Function

There’s another way you can draw an arc, which is to use the arcTo() function, which draws a curve based on the current location the path has reached, and arguments that you supply to it representing a pair of imaginary tangent lines touching the circle’s perimeter.

For example, let’s assume that the current path position has been achieved using a moveTo() call, like this, which places the start position of the path at the bottom-left corner of the canvas:

Images

Now a curve can be created with its start point at this location and an end point at location 170,0, like this:

context.arcTo(0, 0, 170, 0, 170)

So, we have the start point of 0,170 from the moveTo() call, and end point of 170,0 being the third and fourth arguments to arcTo(), but what about the first two and final arguments in the arcTo() call?

Well, the first two arguments of 0,0 in the arcTo() call represent the end point of an imaginary tangent line starting at 0,170 and ending at 0,0. Then the third and fourth arguments (as well as being the end point for the arc) represent the end of a tangent line drawn from 0,0 to 170,0.

The points where these two tangents meet the circle’s circumference are the arc’s start and end points and, because a tangent must always be at a right angle to the radius of a circle, the arc to create can now be calculated. Let’s see how this works by first drawing the imaginary lines, with the following code, as shown in Figure 12-12:

Images

Images

FIGURE 12-12 Two lines have been drawn, which are tangential to the circle.

The first line sets a green color to differentiate from the arc that will be drawn in a moment. Then a simple path is created to draw the two lines, which are simply to show where the imaginary tangents would be if they were displayed, so the preceding code is only for illustrative purposes. The arcTo() code is as follows, and results in Figure 12-13:

Images

Images

FIGURE 12-13 The arc has connected the end points of the pair of tangents.

Figure 12-13 also serves to illustrate the purpose of the final argument to the arcTo() function, which is the radius of the circle on which the arc is based. In this example the two tangents are sides of a square (at 90 degrees to each other), and the arc is a quarter circle with a radius of 170 pixels (whose origin—or center—is therefore at location 170,170).

Images

If this function baffles you, try playing with the examples on the companion website (at 20lessons.com), and you’ll soon come to grips with how these tangents work.

The quadraticCurveTo() Function

In addition to arcs, you can even create the most fancy of curves using the function quadraticCurveTo(), which employs an imaginary attractor object that pulls the curve towards it. For example, let’s draw a line between the left and right side of the canvas using a simple stroke() call, like this (as shown in Figure 12-14):

Images

Images

FIGURE 12-14 A simple horizontal line

Now let’s draw a curved line between these positions, but with an imaginary attractor up in the top left-hand corner, at location 0,0, like this (and shown in Figure 12-15):

Images

Images

FIGURE 12-15 A curve has been created using an imaginary attractor.

As you can see from the figure, the entire curve has been pulled toward the attraction point as if the curve were made of an elasticized magnetic material being attracted toward a magnet. The further up the attractor is placed, the higher the curve will be pulled. Similarly, if the attractor is moved to the left or right, then the attraction will also move in that direction.

Images

Sometimes it takes a little trial and error to get just the curve you need, but you should soon get the hang of this function.

The bezierCurveTo() Function

If you thought quadratic curves were funky, then wait till you check out Bézier curves. These are similar but support the use of two imaginary attractors, which can be placed anywhere on (or off) the canvas.

For an example, let’s adapt the previous example to add a second attractor at the bottom-right corner of the canvas by replacing the call to quadraticCurveTo() with one to bezierCurveTo(), like this (resulting in Figure 12-16):

Images

Images

FIGURE 12-16 This curve has two imaginary attractors.

Images

Since you can place the pair of attractors anywhere you like (not necessarily at opposites sides of the curve), you can create any curve that is possible to draw using Bézier curves, although trial and error may again be required.

Summary

You now have all the line, curve, and path tools added to your toolkit. Remember that you may create as complicated and lengthy paths as you like, and you are not limited to the small snippets of examples I have shown you in this lesson. When your code is properly implemented, you can create sketches and logos, or use the functions as part of a design program you can write in JavaScript.

In the following lesson I’ll continue our journey into the vast resource that is the HTML5 canvas by showing you how to write on the canvas using images, how to apply shadows, and even manipulate pixels directly.

Self-Test Questions

Test how much you have learned in this lesson with these questions. If you don’t know an answer, go back and reread the relevant section until your knowledge is complete. You can find the answers in the appendix.

1. With which property can you change the width of line drawing on the canvas?

2. How can you change the way lines start and end, and the way lines join to each other? And how can you extend the limit of mitered line joins?

3. How do you start and end a path?

4. How do you move the drawing position of a path without creating a line?

5. How do you create a line within a path?

6. Which functions apply a path to the canvas as a line, and as a filled area?

7. Which functions draw an outlined rectangle, and a filled rectangle?

8. With which function can you create all or part of a circle?

9. How can you create an arc from one point to another based on imaginary tangents?

10. How can you create a curve that is modified by an imaginary attractor? And two imaginary attractors?