Over the past decade Adobe Flash became very popular because it allowed us to create and manipulate imagery directly within our web pages. This demand resulted in development of the Canvas specification.
The <canvas>
tag is one of
the most flexible of the new HTML5 tags. This has made the <canvas>
tag the new foundation for
web-based gaming and other interactive components. The <canvas>
tag itself, much like the name, is
a blank slate. It’s a “drawing” surface that gives developers the freedom
and flexibility to create imagery that can blend with and complement the
rest of the DOM (Document Object Model).
The “underdog” HTML5 illustration tool presented in this chapter is Scalable Vector Graphics (SVG). SVG is probably the oldest technology discussed in this book—it has been supported in some browsers for the past 12 years as it was first supported by Microsoft’s Internet Explorer version 6. Rarely does anyone have anything nice to say about IE 6, but in terms of its support of SVG it was ahead of its time.
SVG is an XML markup language that is very similar to HTML, and will probably be pretty easy for those of you who are experienced with HTML. SVG basically does for graphics what HTML did for text. SVG is lightweight and flexible, and can scale to any size with the same lightweight file.
Although there may seem to be a lot of overlap between these two languages, you will quickly see where each technology shines. It’s clear why the HTML5 family encompasses both of these powerhouse visual tools.
Flash became insanely popular because of the flexibility it brought
to the browser. With Flash the Web was free from decorating DOM elements
and became a platform for real drawing and animation. HTML5 brings this
same type of flexibility and power directly to the DOM with the HTML5
<canvas>
tag. This hack starts us
off slow by walking through the creation of basic shapes on a
canvas.
The <canvas>
tag provides
you with a blank slate to create your imagery. In order to do this you
first need to create a <canvas>
tag in the DOM, and then identify the context. The <canvas>
tag is created as a DOM
element:
<canvas
id=
"myCanvas"
width=
"200"
height=
"200"
></canvas>
This basic <canvas>
tag
will be presented as a 200 × 200-px empty block on the page. To add to it,
we need to identify the context:
var
myCanvas
=
document
.
getElementById
(
'myCanvas'
)
var
myCtx
=
myCanvas
.
getContext
(
'2d'
);
Notice that we identify the '2d'
context which may seem to imply that there would also be a '3d'
context, but don’t be fooled: “3d” isn’t
really addressed by the <canvas>
tag; it has only an x- and y-axis. Now that we have the context
identified, we have a host of APIs at our fingertips.
Drawing to a <canvas>
tag
is all about the '2d'
context and
finding the appropriate coordinates on the grid. Generally, one pixel on
the screen correlates to one point in the canvas (this value can vary when
you zoom in or out on a small screen such as on a mobile browser, or when
your element is resized with CSS). The key point on our grid is (0,0) or
the origin, which is the top-lefthand corner of our
canvas. Our canvas is 200 × 200, which means it contains 200 points on the
x-axis and 200 points on the y-axis. Figure 4-1 shows how our canvas
would appear with grid lines on the x- and y-axes over 10 points.
Figure 4-1. The 200 × 200 <canvas> tag with grid markers every tenth point on both the x- and y-axes
We’ll start with one of the simplest shapes: the rectangle. These
are easy to draw into the context of our <canvas>
tag. The '2d'
context gives us access to the API to
draw three basic types of rectangles:
fillRect
Draws a rectangle with a solid color fill
strokeRect
Draws a rectangle that has a border but no fill
clearRect
Clears a rectangle-shaped transparency that removes any imagery or fills in the defined area
Taking our sample canvas from before, let’s combine these three
shapes onto our <canvas>
tag:
var
myCanvas
=
document
.getElementById
(
'myCanvas'
)
var
myCtx
=
myCanvas
.getContext
(
'2d'
);
myCtx
.strokeRect
(
10
,
10
,
180
,
180
);
myCtx
.clearRect
(
50
,
50
,
100
,
100
);
The preceding code laid on top of our <canvas>
tag looks like Figure 4-2.
Each of the three APIs follows the same pattern. They are passed four parameters: the x and y coordinates, along with the width and height of the rectangle.
Rectangles are just the tip of the iceberg when it comes to
drawing on a canvas. Most imagery is produced by combining a series of
lines. Like all methods in the <canvas>
tag, these drawing APIs are
available on the '2d'
context. Paths
require a few steps to start and complete a drawing. To start a drawing
(a single path or series of paths), we use this method:
myContext
.beginPath
();
This method takes no arguments; it simply initiates a drawing.
Once the path has begun, we need to determine where we are going to
start and end the path. To start the path, we will use the moveTo
method. This is similar to determining
where you would move your pencil on a piece of drawing paper. Think of
it as picking up a pencil and putting it down directly on your starting
point. From there, we will use the lineTo
method to determine where our line will
end. Here is the first line of our grid:
myContext
.beginPath
();
myContext
.moveTo
(
0
,
0
);
myContext
.lineTo
(
200
,
0
);
At this point our canvas will still be blank, as we have not yet closed our path. To close the path we use the following method:
myContext
.
closePath
();
Now we have one line on our canvas. To create our grid, we want to
draw multiple lines within our path. To accomplish this, we will begin
the path, and then create a series of moveTo
and lineTo
methods. Once we have all our grid
lines, we will write them to the canvas with our stroke
method. Our code will look something
like this:
var
myCanvas
=
document
.
getElementById
(
'myCanvas'
)
var
myContext
=
myCanvas
.
getContext
(
'2d'
);
var
ctx
=
myContext
;
myContext
.
beginPath
();
for
(
i
=
0
;
i
<
201
;
i
++
){
myContext
.
moveTo
(
0
,
i
);
myContext
.
lineTo
(
200
,
i
);
i
+=
10
;
}
for
(
i
=
0
;
i
<
201
;
i
++
){
myContext
.
moveTo
(
i
,
0
);
myContext
.
lineTo
(
i
,
200
);
i
+=
10
;
}
myContext
.
stroke
();
Paths have a number of different JavaScript APIs that create different line effects. In many cases we may have a few lines that we want to connect and consequently fill the area. To accomplish this we can simply call the following method:
myContext
.
fill
();
We can get pretty far with straight lines in our drawings, but we
can use the canvas to draw arcs as well. Remember, the <canvas>
tag will always be a square,
but we can draw any shape inside the square. To draw an arc on the
canvas, call the following method off the canvas context:
arc(x, y, radius, startAngle, endAngle, anticlockwise);
As illustrated in the preceding code, a number of arguments are
passed into the arc
method. The first
two are the coordinates for the arc’s center, followed by the arc
radius
. The startAngle
and endAngle
parameters declare the start and end
points of the arc in radians, which are measured from the x-axis. The
final optional anticlockwise
parameter, when set to true
, draws
the arc in a counterclockwise direction. The default is false
, which would draw the arc in a clockwise
direction.
Looking back at the radius
argument, we want to make a special note. In CSS, we are comfortable
with declaring values in degrees, but in this case the arc radius is
measured in radians. It’s quite common to see an inline conversion from
radians to degrees using the JavaScript math equation for pi:
myRadians
=
(
Math
.
PI
/
180
)
*
degrees
Let’s put this to good use by creating something recognizable on
the <canvas>
tag. When I think
of circles I think of two things: smiley faces and bombs. To keep the
violence level down, we’ll work on the smiley face in this chapter.
Using a similar 200 × 200 <canvas>
tag let’s center our outer
circle directly in the middle of our tag, and then draw our head:
smileCtx
.
beginPath
();
smileCtx
.
arc
(
100
,
100
,
99
,
0
,
Math
.
PI
*
2
);
We now have a canvas with a circle on it, as shown in Figure 4-3.
This isn’t very exciting. So next we will add the mouth. For this
we will use the moveTo
method, and
then draw a half circle (notice that the radius
will be PI
instead of PI*2
as it was for the full circle):
smileCtx
.
moveTo
(
170
,
100
);
smileCtx
.
arc
(
100
,
100
,
70
,
0
,
Math
.
PI
);
// Mouth
The last two components are the eyes. Since we want our eyes to be
solid fills, we need to make separate strokes for each of them so that
we can apply the fill. The first step to accomplish this is to close the
current stroke. We will then start a new stroke, move to a new start
point, draw a new circle, and call our fill
parameter for each eye:
smileCtx
.
stroke
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
60
,
65
);
smileCtx
.
arc
(
60
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Left eye
smileCtx
.
fill
();
Let’s put all this code together, and see our masterpiece:
var
mySmile
=
document
.
getElementById
(
'mySmile'
)
var
smileCtx
=
mySmile
.
getContext
(
'2d'
);
smileCtx
.
beginPath
();
smileCtx
.
arc
(
100
,
100
,
99
,
0
,
Math
.
PI
*
2
);
// head
smileCtx
.
moveTo
(
170
,
100
);
smileCtx
.
arc
(
100
,
100
,
70
,
0
,
Math
.
PI
);
// Mouth
smileCtx
.
stroke
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
60
,
65
);
smileCtx
.
arc
(
60
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Left eye
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
140
,
65
);
smileCtx
.
arc
(
140
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Right eye
smileCtx
.
fill
();
Our canvas now holds all three strokes to form the face, as shown in Figure 4-4.
We’ve plowed right through lines and arcs, but many illustrations call for lines that can’t be accomplished by either of these shapes. The Canvas specification includes two additional tools for creating custom shapes:
quadraticCurveTo
(
cp1x
,
cp1y
,
x
,
y
);
bezierCurveTo
(
cp1x
,
cp1y
,
cp2x
,
cp2y
,
x
,
y
);
Each of these methods has control points and an ending x,y point.
The control points determine the curvature of the path. The bezierCurveTo
method has a second control
point for an uneven curvature. Additional information about the
implementation of each method is available in the W3C
spec.
We don’t live in a black-and-white Web, which makes it essential to
be able to apply colors and styles to your <canvas>
tag elements. Style your canvas
elements with this familiar CSS syntax.
If you need to catch up on how to create shapes, strokes, or fills
on your <canvas>
tag, read
[Hack #37].
Shapes and strokes have little effect on our applications if we can’t apply color and styles to them. The specification for canvas styles borrows heavily from CSS, so a lot of the syntax should be familiar to you.
Canvas elements can be colored with any CSS color value style, and they even support transparency with RGBA and HSPA colors. Also, canvas strokes and shapes have a default color value of black.
Let’s look at a code example for drawing a grid of lines across a
200 × 200 <canvas>
tag:
var
myCanvas
=
document
.
getElementById
(
'myCanvas'
)
var
myContext
=
myCanvas
.
getContext
(
'2d'
);
myContext
.
beginPath
();
for
(
i
=
0
;
i
<
201
;
i
++
){
myContext
.
moveTo
(
0
,
i
);
myContext
.
lineTo
(
200
,
i
);
i
+=
10
;
}
for
(
i
=
0
;
i
<
201
;
i
++
){
myContext
.
moveTo
(
i
,
0
);
myContext
.
lineTo
(
i
,
200
);
i
+=
10
;
}
myContext
.
stroke
();
This example draws vertical lines every 10 points, and then loops around again to draw horizontal lines every 10 points. As stated previously, the default color for each line is black. To give the look of graph paper we want to make the lines a light blue color. We can accomplish this by adding a single line of code:
myContext.strokeStyle = '#99C4E5';
Since the whole grid is accomplished through one stroke, we only
need to declare the style once. To add a bit of depth to our grid we
will make the horizontal lines slightly darker than the vertical lines.
Since we are going to style the lines in two different ways, we need to
add a few lines of JavaScript to our code to separate our illustration
into two different strokes. To accomplish this, we will end the stroke
after the first for
loop and then
start a new stroke for the second for
loop:
var
myCanvas
=
document
.
getElementById
(
'myCanvas2'
)
var
myContext
=
myCanvas
.
getContext
(
'2d'
);
myContext
.
strokeStyle
=
'#1487E0'
;
myContext
.
beginPath
();
for
(
i
=
0
;
i
<
201
;
i
++
){
myContext
.
moveTo
(
0
,
i
);
myContext
.
lineTo
(
200
,
i
);
i
+=
10
;
}
myContext
.
stroke
();
myContext
.
beginPath
();
myContext
.
strokeStyle
=
'#B1CADD'
;
for
(
i
=
0
;
i
<
201
;
i
++
){
myContext
.
moveTo
(
i
,
0
);
myContext
.
lineTo
(
i
,
200
);
i
+=
10
;
}
As soon as we started the second stroke with the beginPath
method, we set a new, darker stroke
style for the horizontal lines.
There are two different methods for adding color to your shapes.
strokeStyle
applies to lines and the
outline of shapes, and fillStyle
applies to shapes or strokes that have a fill applied to them. It’s
important to note that once you set a stroke or fill style, the setting
will persist in the context until it is changed back to the original
value, or until it is set to a new value.
Just as with other HTML5 elements, adding gradients can provide for deep visual depth, and can be quite useful. Let’s take a look at our example of a simple black-and-white smiley face, before applying a few gradients to spice it up:
var
mySmile
=
document
.
getElementById
(
'mySmile'
)
var
smileCtx
=
mySmile
.
getContext
(
'2d'
);
smileCtx
.
beginPath
();
smileCtx
.
arc
(
100
,
100
,
99
,
0
,
Math
.
PI
*
2
);
// head
smileCtx
.
moveTo
(
170
,
100
);
smileCtx
.
arc
(
100
,
100
,
70
,
0
,
Math
.
PI
);
// Mouth
smileCtx
.
stroke
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
60
,
65
);
smileCtx
.
arc
(
60
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Left eye
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
140
,
65
);
smileCtx
.
arc
(
140
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Right eye
smileCtx
.
fill
();
The preceding code provides us with the basic smiley face shown in Figure 4-5.
As every child who wasn’t raised by wolves knows, smiley faces are supposed to be yellow. Let’s redraw our smiley face with a yellow background:
var
mySmile
=
document
.
getElementById
(
'mySmile'
)
var
smileCtx
=
mySmile
.
getContext
(
'2d'
);
smileCtx
.
beginPath
();
smileCtx
.
fillStyle
=
'#F1F42E'
;
smileCtx
.
arc
(
100
,
100
,
99
,
0
,
Math
.
PI
*
2
);
// head
smileCtx
.
stroke
();
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
170
,
100
);
smileCtx
.
arc
(
100
,
100
,
70
,
0
,
Math
.
PI
);
// Mouth
smileCtx
.
stroke
();
smileCtx
.
beginPath
();
smileCtx
.
fillStyle
=
'black'
;
smileCtx
.
moveTo
(
60
,
65
);
smileCtx
.
arc
(
60
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Left eye
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
140
,
65
);
smileCtx
.
arc
(
140
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Right eye
smileCtx
.
fill
();
This gives us a more iconic version of our smiley face, as shown in Figure 4-6.
In order to accommodate the introduction of color, we had to make a few changes to our code. First, we extracted the mouth from the same stroke that made the head so that the fill would not overwrite the line used for the mouth. Then we added a fill method to the end of the head circle to color it yellow. The last change we made was to reset the fill color back to black for the eyes. Again, once we set the style, we needed to reset it to black to return to the default value.
Now, to prove that we have some artistic talent, we will change our yellow color to a yellow gradient. We can apply two types of gradients:
createLinearGradient
(
x1
,
y1
,
x2
,
y2
)
createRadialGradient
(
x1
,
y1
,
r1
,
x2
,
y2
,
r2
)
The createLinearGradient
method
is passed four different arguments: the start point (x1,y1
) and the end point (x2,y2
) of the gradient.
The createRadialGradient
method
is passed six arguments. The first three define an inner circle with
coordinates (x1,y1
) and one radius
(r1
) and an outer circle with
coordinates and a second radius.
Our example will use a radial gradient to give our smiley face
three-dimensional depth. First we will set our gradient to a variable,
and then we will add a series of color stops to the gradient. In our
code example, we’ll replace the fillStyle
with our gradient:
var
mySmile
=
document
.
getElementById
(
'mySmile'
)
var
smileCtx
=
mySmile
.
getContext
(
'2d'
);
var
radgrad
=
smileCtx
.
createRadialGradient
(
100
,
100
,
10
,
100
,
100
,
100
);
radgrad
.
addColorStop
(.
5
,
'rgba(247,241,192,1)'
);
radgrad
.
addColorStop
(
1
,
'rgba(244,225,56,1)'
);
smileCtx
.
beginPath
();
smileCtx
.
fillStyle
=
radgrad
;
smileCtx
.
arc
(
100
,
100
,
99
,
0
,
Math
.
PI
*
2
);
// head
smileCtx
.
stroke
();
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
170
,
100
);
smileCtx
.
arc
(
100
,
100
,
70
,
0
,
Math
.
PI
);
// Mouth
smileCtx
.
stroke
();
smileCtx
.
beginPath
();
smileCtx
.
fillStyle
=
'black'
;
smileCtx
.
moveTo
(
60
,
65
);
smileCtx
.
arc
(
60
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Left eye
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
140
,
65
);
smileCtx
.
arc
(
140
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Right eye
smileCtx
.
fill
();
We have simply replaced our fill color with the gradient as a fill. This gives us the added depth we want to make our smiley face stick out from the crowd (see Figure 4-7).
You can accomplish a transparency effect with a color of your
choosing, or you can apply the transparency globally to a stroke. To
adjust the transparency level, use the globalAlpha
method:
globalAlpha
=
.
2
;
Unlike the color styles, the globalAlpha
method only applies to the current
stroke. Once a new stroke is started, the globalAlpha
method resets to 1
.
Because they play such a large role in illustrations, lines are
given additional control values in your <canvas>
tag. You can set the following
values for a stroke on your <canvas>
tag:
lineWidth
A numerical value that represents the width in points
lineCap
The shape of the end of a line, which can be declared as butt, round, or square
lineJoin
The shape of a line joint, which can be declared as round, bevel, or miter
miterLimit
Determines how far the outside connection point can be placed from the inside connection point, when the
lineJoin
type ofmiter
is selected
Patterns and shadows can also be applied to canvas elements, and
they follow similar syntax to CSS implementations. For details on these
features and more, see the W3C specification on the <canvas>
tag.
Shapes in a <canvas>
tag
have some of the same controls as other page elements. In this hack,
you’ll learn how to take your canvas illustrations one step further by
utilizing images as fills.
The Canvas specification gives you a lot of flexibility to create your HTML5 illustrations. Other hacks have covered basic shapes, colors, gradients, and other styles, so this hack will focus on importing another object for use on your canvas element.
For details on fills and other styles, see [Hack #38].
To illustrate the use of an image as a fill, we’ll start by looking at a smiley face example with a basic yellow color fill for the head (see Figure 4-8).
We set the background color by adding a color fill to the circle that makes up the head. Once the stroke is started, it’s a simple line of code:
smileCtx
.
fillStyle
=
'#F1F42E'
;
Our end result will have a simple image used as a repeating background (see Figure 4-9).
Figure 4-9. The 200 × 200 <canvas> tag with a drawing of a smiley face and a repeating heart background image
To change that solid color to an image, we will use a very similar API:
smileCtx
.
fillStyle
=
myPattern
;
You can see in the preceding code that we are using the same API for an image background as we are for a fill color (similar to the background attribute in a CSS declaration). However, a bit of additional overhead is required when using an image.
In JavaScript, to use an image you first must have a reference to
it. In our case, we will start by creating the image dynamically, and
then setting its src
attribute:
var
img
=
new
Image
();
img
.
src
=
'/assets/img/heart.png'
;
The image we are using is the small icon-size image shown in Figure 4-10.
That was easy enough; we now have a variable called img
that references our image file. The second
step is to set that image as a pattern to be utilized by the <canvas>
tag:
var
myPattern
=
smileCtx
.
createPattern
(
img
,
'repeat'
);
smileCtx
.
fillStyle
=
myPattern
;
To accomplish this, we used a canvas method called createPattern
. This requires two parameters:
the first is the reference to the image file, and the second is our
DOMstring
repetition. Similar to a
CSS implementation, we can set the DOMstring
repetition to repeat
, repeat-x
, repeat-y
, or no-repeat
. If no value is specified, it
defaults to repeat
.
Now let’s put all of this together and see what it looks like. Here is a view of the code used to generate our smiley face with the image as a background:
var
mySmile
=
document
.
getElementById
(
'mySmile4'
)
var
smileCtx
=
mySmile
.
getContext
(
'2d'
);
// create new image object to use as pattern
var
img
=
new
Image
();
img
.
src
=
'/assets/img/heart.png'
;
// create pattern
var
myPattern
=
smileCtx
.
createPattern
(
img
,
'repeat'
);
smileCtx
.
fillStyle
=
myPattern
;
smileCtx
.
arc
(
100
,
100
,
99
,
0
,
Math
.
PI
*
2
);
// head
smileCtx
.
stroke
();
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
170
,
100
);
smileCtx
.
arc
(
100
,
100
,
70
,
0
,
Math
.
PI
);
// Mouth
smileCtx
.
stroke
();
smileCtx
.
beginPath
();
smileCtx
.
fillStyle
=
'black'
;
smileCtx
.
moveTo
(
60
,
65
);
smileCtx
.
arc
(
60
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Left eye
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
140
,
65
);
smileCtx
.
arc
(
140
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Right eye
smileCtx
.
fill
();
If we were to run this code, we would probably be disappointed with the results. In most cases, our smiley face would look like Figure 4-11.
Figure 4-11. The smiley face canvas rendering with the background image set as in the previous code sample
Can you identify the problem? Think about the load time. The canvas is taking advantage of real-time data. In the preceding sample, we created the image and then set it as a background immediately. Since the pattern failed, the canvas fill reverted back to its default state of black for the fill color. The problem has to do with the availability of the image data, which in our case hasn’t been loaded yet.
To solve this problem we will add a few lines of JavaScript that
wait for the image to load before we execute the necessary canvas code.
Browsers have supported the image onload
event for years. In this example we’ll
use the image onload
event to know
when we have the necessary data loaded:
var
mySmile
=
document
.
getElementById
(
'mySmile4'
)
var
smileCtx
=
mySmile
.
getContext
(
'2d'
);
// create new image object to use as pattern
var
img
=
new
Image
();
img
.
src
=
'/assets/img/heart.png'
;
img
.
onload
=
function
(){
// create pattern
var
myPattern
=
smileCtx
.
createPattern
(
img
,
'repeat'
);
smileCtx
.
fillStyle
=
myPattern
;
smileCtx
.
arc
(
100
,
100
,
99
,
0
,
Math
.
PI
*
2
);
// head
smileCtx
.
stroke
();
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
170
,
100
);
smileCtx
.
arc
(
100
,
100
,
70
,
0
,
Math
.
PI
);
// Mouth
smileCtx
.
stroke
();
smileCtx
.
beginPath
();
smileCtx
.
fillStyle
=
'black'
;
smileCtx
.
moveTo
(
60
,
65
);
smileCtx
.
arc
(
60
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Left eye
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
140
,
65
);
smileCtx
.
arc
(
140
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Right eye
smileCtx
.
fill
();
}
Now we’re sure that our image data has loaded, and the <canvas>
tag can take full advantage of
the image for use in its pattern background.
Adding image onload
s around
whole segments of code can sometimes be cumbersome. A nice shortcut
available in HTML5 browsers is the use of inline image data. We can
easily remove the onload
event from
the preceding example and simply reference the image data. Since the
image data was loaded when the page was loaded, there is no need to wait
for the onload
event to fire before
we attempt to use the image. Our new code would look like this:
var
mySmile
=
document
.
getElementById
(
'mySmile5'
)
var
smileCtx
=
mySmile
.
getContext
(
'2d'
);
// create new image object to use as pattern
var
img2
=
new
Image
();
img2
.
src
=
'data:image/png;base64,iVBORw0K... image data here
...f5v038BfQ3g/3mcvqgAAAAASUVORK5CYII='
;
// create pattern
var
myPattern
=
smileCtx
.
createPattern
(
img2
,
'repeat'
);
smileCtx
.
fillStyle
=
myPattern
;
smileCtx
.
arc
(
100
,
100
,
99
,
0
,
Math
.
PI
*
2
);
// head
smileCtx
.
stroke
();
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
170
,
100
);
smileCtx
.
arc
(
100
,
100
,
70
,
0
,
Math
.
PI
);
// Mouth
smileCtx
.
stroke
();
smileCtx
.
beginPath
();
smileCtx
.
fillStyle
=
'black'
;
smileCtx
.
moveTo
(
60
,
65
);
smileCtx
.
arc
(
60
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Left eye
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
140
,
65
);
smileCtx
.
arc
(
140
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Right eye
smileCtx
.
fill
();
It may not make sense to utilize the Base64 version of your image in all cases, since it results in added weight in the initial page load, but sometimes it may be appropriate in order to utilize and simplify your code. It’s a good practice to have multiple implementation methods to choose from.
When Apple first introduced the Retina display on the iPhone 4,
parts of the Web started to look pretty shabby. The display’s higher
resolution made your quick-loading “web-ready” images look pixelated. In
general, higher-resolution images mean longer load times. This hack uses
the HTML5 <canvas>
tag to provide
Retina-ready imagery without the added weight.
There is a problem with our Retina screens. They look great (few people will debate that), but the way in which they accomplish this has caused a lot of problems for web developers. Apple first introduced the Retina display with the iPhone 4, in an attempt to solve two problems: create a display in which the pixels were indistinguishable to the naked eye, and not make iOS and Apple apps look like crap. To do this, Apple marked the pixel density much higher than was necessary, and in fact gave the display a density that was evenly divisible by the previous iPhone screen density. This enabled Apple to update all the visual assets of the iOS SDK and the iOS operating system to a higher resolution, and simply downsize it for older, less dense screens. For all the other assets in the Apple apps, the company used a method called pixel doubling to help the assets remain at the proper size.
Assets such as images and media on the Web fall prey to pixel doubling. This makes our web pages look pixelated and jagged. The common solution to this problem is to utilize images with twice the pixel resolution, which leaves us with images that are larger and web pages that take significantly longer to load.
The <canvas>
tag is a
drawing space for vector illustrations. Since the <canvas>
tag is created using a set of
definitions, the size of the illustration is inconsequential to the
amount of data that is necessary to create it (unlike images that
required the transfer of additional data to accommodate more pixels).
This being the case, we can make our <canvas>
tag Retina-ready without any
additional page weight.
Let’s start by loading a simple example of a smiley face drawn out
on a 200 × 200-point <canvas>
tag. Here is the code for creating our example:
var
mySmile
=
document
.
getElementById
(
'mySmile2'
)
var
smileCtx
=
mySmile
.
getContext
(
'2d'
);
smileCtx
.
beginPath
();
smileCtx
.
fillStyle
=
'#F1F42E'
;
smileCtx
.
arc
(
100
,
100
,
99
,
0
,
Math
.
PI
*
2
);
// head
smileCtx
.
stroke
();
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
170
,
100
);
smileCtx
.
arc
(
100
,
100
,
70
,
0
,
Math
.
PI
);
// Mouth
smileCtx
.
stroke
();
smileCtx
.
beginPath
();
smileCtx
.
fillStyle
=
'black'
;
smileCtx
.
moveTo
(
60
,
65
);
smileCtx
.
arc
(
60
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Left eye
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
140
,
65
);
smileCtx
.
arc
(
140
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Right eye
smileCtx
.
fill
();
Our smiley face looks great on a standard display, but it’s pretty jagged on the Retina display. Figure 4-12 shows how our canvas image looks on the iPhone 3Gs and the iPhone 4.
Figure 4-12. The canvas image displayed on the iPhone 3GS (left), and on the iPhone 4 with a Retina display (right)
In order to have the illustration be smooth for the Retina display, we need to counteract the pixel doubling that is taking place. To accomplish this, we will add a few simple lines of code to our JavaScript:
if
(
window
.
devicePixelRatio
==
2
)
{
mySmile
.
setAttribute
(
'width'
,
400
);
mySmile
.
setAttribute
(
'height'
,
400
);
smileCtx6
.
scale
(
2
,
2
);
}
We will insert this into our code right after we declare our
context, but before we start to apply our elements to the <canvas>
tag. In essence, we have
detected when pixel doubling is being applied (by checking the device
pixel ratio) and then doubled the size of our <canvas>
tag. These lines of code will
result in the big, fat smiley face shown in Figure 4-13.
Now we need to rescale our <canvas>
tag to fit our original page
space. In order to have the page render all the pixels in half the size,
we will set our canvas to 400 and then use CSS to shrink it back down to
200 px. Let’s add this CSS to the top of our page:
#mySmile
{
height
:
200px
;
width
:
200px
;
}
With just a few lines of code we have essentially Retina-enabled
our <canvas>
tag without having
to increase the page weight significantly. Let’s go back to our iPhone
3GS–iPhone 4 comparison to see our results (see Figure 4-14).
Figure 4-14. The canvas image displayed on the iPhone 3GS (left), and on the iPhone 4 with a Retina display after the addition of the new JavaScript code
We’ve improved the experience for our Retina-display users without
affecting the rest of our user base. You can apply this technique to any
<canvas>
tag, whether it is a
page illustration or a canvas element used as a CSS background image.
The only time you will not benefit from using this technique is when you
import an image into your <canvas>
tag that doesn’t support the
Retina display’s higher resolution.
Use of the <canvas>
tag is
often one of the most efficient ways to create animations in your web
applications. This hack digs into the nitty-gritty of creating animations
while using the <canvas>
tag.
Clean animation can make or break your web applications. Native applications on desktop and mobile devices have raised users’ expectations: if your web application fails to include clean, concise animations, users will often write it off as being a poorly performing app.
Canvas animation can be a powerful tool for web animations. As more and more browser makers enable the GPU for canvas animations, it becomes even more beneficial to perform your animations with a canvas element.
Animation on a <canvas>
tag is reminiscent of early cartoon animations where each frame is drawn
out and then displayed in the correct order and at the determined frame
rate. Canvas animation basically consists of these three steps:
Draw on the canvas.
Erase what you just drew.
Repeat steps 1 and 2 until the animation is complete.
In JavaScript, when things need to be called over and over again
we often use methods such as setTimeout
and setInterval
to call our drawing methods. The
problem with each of these methods is they need to be set to a specific
amount of time. If we set that time to, say, 100 milliseconds, we would
never be able to achieve a frame rate higher than 10 frames per
second.
A powerful new standard has been introduced to address this issue
with the <canvas>
tag: the
requestAnimationFrame
method. With
this method, you are asking the browser to render the next frame as soon
as it is available for rendering, as opposed to attempting to render at
a fixed interval. The goal of requestAnimationFrame
is 60 frames per second,
but it doesn’t fail if it can’t render that quickly; it simply renders
as soon as it can. Note that this method isn’t limited to use in canvas
animations; it’s available for any web drawing technology, including
WebGL.
Let’s take a good look at an example of a canvas animation. If
you’ve worked your way through the previous hacks in this chapter you
have seen the smiley face examples. Each example drew the smiley face on
a 200 × 200 canvas element. For this illustration we will draw it on a
much larger canvas to give us room to move. Let’s start by dropping our
<canvas>
tag onto the
page:
<canvas
id=
"moveSmile"
width=
"800"
height=
"200"
></canvas>
Now that we have a big, fat, blank canvas, we will draw the smiley face on top of it. To do this, we’ll pull in a few lines of JavaScript to build our page elements:
var
canvas
=
document
.
getElementById
(
"moveSmile"
);
var
smileCtx
=
canvas
.
getContext
(
"2d"
);
smileCtx
.
beginPath
();
smileCtx
.
fillStyle
=
'#F1F42E'
;
smileCtx
.
arc
(
100
,
100
,
99
,
0
,
Math
.
PI
*
2
);
// head
smileCtx
.
stroke
();
smileCtx
.
fill
();
smileCtx
.
beginPath
();
smileCtx
.
moveTo
(
170
,
100
);
smileCtx
.
arc
(
100
,
100
,
70
,
0
,
Math
.
PI
);
// Mouth
smileCtx
.
stroke
();
smileCtx
.
beginPath
();
smileCtx6
.
fillStyle
=
'black'
;
smileCtx6
.
moveTo
(
60
,
65
);
smileCtx6
.
arc
(
60
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Left eye
smileCtx6
.
fill
();
smileCtx6
.
beginPath
();
smileCtx6
.
moveTo
(
140
,
65
);
smileCtx6
.
arc
(
140
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Right eye
smileCtx6
.
fill
();
Our code simply draws out this smiley face on the lefthand side of
the <canvas>
tag. For
illustration purposes, a 1 px border has been added to the <canvas>
tag so that we can see the
boundaries (see Figure 4-15).
Going back to our three-step process, once we draw our illustration we need to erase what we’ve drawn:
smileCtx
.
clearRect
(
0
,
0
,
800
,
200
);
//smileCtx is the 2d context
For simplicity I’m erasing the whole canvas, but to optimize
performance you should focus on erasing what is changing for the next
frame. In the preceding method I am clearing the whole canvas by setting
the clearRect
coordinates from the
top-lefthand corner of the canvas to the bottom-righthand corner. This
erases a rectangular shape the size of the canvas.
Our canvas should now be void of illustration, as shown in Figure 4-16.
Figure 4-16. The 800 × 200 <canvas> tag after the clearRect method has cleared the entire canvas context
Now, for step 3 we will redraw our smiley face, but we will move
it slightly to the right. In order to do this, we will move the x
position of both our moveTo
methods and our element start position
(arc
in this case).
To accomplish this, we will replace each number with a simple equation to generate the proper x coordinate each time the element is drawing:
x
+
startingposition
Our code will now look like this:
var
x
=
0
;
smileCtx6
.
beginPath
();
smileCtx6
.
fillStyle
=
'#F1F42E'
;
smileCtx6
.
arc
(
x
+
100
,
100
,
99
,
0
,
Math
.
PI
*
2
);
// head
smileCtx6
.
stroke
();
smileCtx6
.
fill
();
smileCtx6
.
beginPath
();
smileCtx6
.
moveTo
(
x
+
170
,
100
);
smileCtx6
.
arc
(
x
+
100
,
100
,
70
,
0
,
Math
.
PI
);
// Mouth
smileCtx6
.
stroke
();
smileCtx6
.
beginPath
();
smileCtx6
.
fillStyle
=
'black'
;
smileCtx6
.
moveTo
(
x
+
60
,
65
);
smileCtx6
.
arc
(
x
+
60
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Left eye
smileCtx6
.
fill
();
smileCtx6
.
beginPath
();
smileCtx6
.
moveTo
(
x
+
140
,
65
);
smileCtx6
.
arc
(
x
+
140
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Right eye
smileCtx6
.
fill
();
For the preceding code x
is set
to 0
, but in order to move the smiley
face across the screen we need to change the x
position. We’ll do this with a simple
statement that increases or decreases the x
value appropriately (this will move it
across the screen and then back again).
There is one additional value we need to determine: the speed of
the animation. If we simply increment the value by 1, the smiley face
will only move one pixel per iteration. We want to put a little bit of
pep in this animation, so we will create a new variable called speed
and set it to 6
. When this number is added to the current
x
position, it will move the smiley
face forward or back six pixels, thus increasing the speed. Let’s look
at the code:
var
speed
=
6
;
//px it moves on each loop determines how fast it moves
x
+=
speed
;
if
(
x
<=
0
||
x
>=
600
){
//as far as we can go without cutting off
speed
=
-
speed
;
//determines if it moves forwards or backwards;
}
As mentioned earlier, requestAnimationFrame
is a new specification
in the HTML5 family. It’s so new that most browsers only support a
prefixed version of it. In order to utilize it in modern browsers, we
need to do a quick check to see which version of the method we need to
use, and then build a reference to it.
We will use the requestAnimationFrame
method in our example to
iterate through our animation. To accomplish this, we will use it to
call the same draw
method cyclically.
Remember, the frame rate will be determined by requestAnimationFrame
, as it will call the
draw
method as soon as the browser is
ready to draw another screen.
The requestAnimationFrame
method is really the glue that holds this example together. To get
everything working properly, we will set our variables at the top of our
page and then break our code into two methods. The first will determine
the new x
value and then call the
draw
method.
The draw
method will first
clear the canvas from the previous frame and then draw out the new
frame. This method gets called over and over again. Our final code
assembles into this:
var
x
=
0
;
var
speed
=
6
;
//px it moves on loop determines how fast it moves
var
canvas
=
document
.
getElementById
(
"moveSmile"
);
var
smileCtx
=
canvas
.
getContext
(
"2d"
);
function
animate
(){
reqAnimFrame
=
window
.
mozRequestAnimationFrame
||
window
.
webkitRequestAnima
tionFrame
||
window
.
msRequestAnimationFrame
||
window
.
oRequestAnimationFrame
reqAnimFrame
(
animate
);
x
+=
speed
;
if
(
x
<=
0
||
x
>=
600
){
speed
=
-
speed
;
//see if it moves forwards or backwards;
}
draw
();
}
function
draw
()
{
smileCtx6
.
clearRect
(
0
,
0
,
800
,
200
);
smileCtx6
.
beginPath
();
smileCtx6
.
fillStyle
=
'#F1F42E'
;
smileCtx6
.
arc
(
x
+
100
,
100
,
99
,
0
,
Math
.
PI
*
2
);
// head
smileCtx6
.
stroke
();
smileCtx6
.
fill
();
smileCtx6
.
beginPath
();
smileCtx6
.
moveTo
(
x
+
170
,
100
);
smileCtx6
.
arc
(
x
+
100
,
100
,
70
,
0
,
Math
.
PI
);
// Mouth
smileCtx6
.
stroke
();
smileCtx6
.
beginPath
();
smileCtx6
.
fillStyle
=
'black'
;
smileCtx6
.
moveTo
(
x
+
60
,
65
);
smileCtx6
.
arc
(
x
+
60
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Left eye
smileCtx6
.
fill
();
smileCtx6
.
beginPath
();
smileCtx6
.
moveTo
(
x
+
140
,
65
);
smileCtx6
.
arc
(
x
+
140
,
65
,
12
,
0
,
Math
.
PI
*
2
);
// Right eye
smileCtx6
.
fill
();
}
animate
();
Figure 4-17 shows a snapshot from our example. Our smiley face starts at the far-left side of the canvas element, and then animates to the far-right side. It will then repeat this step over and over again.
Scalable Vector Graphics (SVG) is usually the most “familiar” graphics format in the HTML5 family of technologies. This hack will quickly get you working with the SVG format as though it were part of the DOM (hint: it really is part of the DOM!).
Scalable Vector Graphics is the W3C’s recommendation for web illustrations. Similar to Flash, SVG is a markup language for describing two-dimensional vector graphics, but it’s an open XML-based language as opposed to being proprietary. Think of SVG as being the graphical equivalent to HTML, and like HTML, SVG works seamlessly with other browser technologies such as JavaScript, CSS, and the DOM.
Compared to all the other graphics and media-based technologies introduced in HTML5, SVG has some major advantages. The primary advantage is the technology itself. Being an XML-based language, SVG doesn’t require an editing program like Flash, Photoshop, or even Paint. You can create and edit SVG images with any simple text editor, or with your favorite web editor. The S in SVG stands for Scalable, and scalable it is! SVG is resolution-independent. Your SVG images can be zoomed in or out, and even printed at any size, and they will still maintain their quality, which is the primary benefit of the technology.
Being pure XML, SVG is natively searchable, indexable, and easily compressible. It’s also quite natural to embed text within your SVG files and then style them with CSS. It’s also easy to make SVG graphics compliant with the Americans with Disabilities Act (ADA), by embedding descriptions of the images within the SVG file itself.
In most cases SVG is managed in its own file. This is a text-based file ending with .svg. You would then embed that file into the DOM in a manner similar to how you would work with an image. In our example, we’ll start with a new SVG file named smiley.svg and embed it into our sample page with the following code:
<object
data=
"smiley.svg"
type=
"image/svg+xml"
/>
Technically, our SVG file is an object on the page, not an image, and therefore is embedded with an object file. At this point we will see our object in the DOM, but it will not display anything, as the SVG file is blank. But we will fix that.
Now, to really impress our friends and enemies we’ll build an SVG object that demonstrates the cross-cultural symbol for love, peace, and hope: the smiley face.
Unlike a JPEG or PNG image, where the image is transmitted in
Unicode, an SVG image is drawn out by a series of rules that follow the
XML schema. This tends to make the images lightweight and
ultra-scalable. In the preceding code example, we created an object
element that has a data
attribute pointing to an SVG file. This
SVG file contains a few lines of code that draw out our smiley face.
Before we start, let’s see how our end product will look (see Figure 4-18).
This cheeky smiley face is truly simple. The SVG file consists of only five elements, and each element becomes a discrete DOM element once it’s imported into the page. As DOM elements, they follow all the same rules and have access to the same APIs as all other page elements. Let’s take a quick look at each element comprising our smiley face:
<circle
cx=
"300"
cy=
"164"
r=
"160"
fill=
"yellow"
stroke=
"black"
stroke-width=
"2"
/>
This first element is the yellow circle that represents the head
of the smiley face. As you can glean from the preceding code, the
element is actually a circle
element
that has attributes representing the following:
cx
,cy
These are the
x
andy
positions of the circle as it relates to the SVG object in the page.r
This is the radius of the circle represented in points (a numeric value).
fill
This refers to how the inside of the object is painted. An element can be filled with a color, a gradient, or a pattern (such as an imported image).
stroke
This represents the actual shape of the object or line (including the text). The stroke can be colored with the same options as the fill.
stroke-width
This is only necessary when you have a stroke declared. As is obvious in the attribute name, this declares the width of the stroke in points. The default is
1
.
The next two elements are the eyes of the smiley face. They
contain many of the same attributes as the previous circle. The two
circles are identical to each other except for the x
positions that draw them on different sides
of the head.
<circle
cx=
"210"
cy=
"100"
r=
"20"
fill=
"black"
/>
<circle
cx=
"380"
cy=
"100"
r=
"20"
fill=
"black"
/>
The clip path may be an unexpected element for this illustration. Not to give away the ending, but the final element will be another circle that represents the smiling mouth in the illustration.
<clipPath
id=
"MyClip"
>
<rect
x=
"30"
y=
"200"
width=
"600"
height=
"100"
/>
</clipPath>
The clip path is a parent element to other SVG elements that have a clipping effect instead of a painting effect. The clip path has a single attribute:
id
The
id
looks a lot like a DOMid
because it is a DOMid
, and it’s necessary in this case for us to reference the clip path by another SVG element.
The clip path contains another element:
<rect
x=
"30"
y=
"200"
width=
"600"
height=
"100"
/>
This is exactly what it looks like. You were already introduced to
the circle
element; well, this is a
rectangle element. If the rect
element wasn’t contained by a clip path, it would draw a 600 × 100
rectangle on the SVG object.
<circle
cx=
"300"
cy=
"160"
r=
"120"
fill-opacity=
"0"
stroke=
"black"
stroke-width=
"5"
clip-path=
"url(#MyClip)"
/>
This final object is the mouth. This circle has two new attributes that we want to look at:
fill-opacity
This is a value between
0
and1
that declares how opaque our fill should be. Since we want the circle to appear empty, we have it set to0
.clip-path
This references the DOM
id
for the clip path within our SGV file. When we reference the clip path, the shape gets applied to this element, in a manner that clips off anything (fill or stroke) within the image.
We could have drawn a line for the mouth of the smiley face, but it would have been a lot more work to draw out that shape in XML than it would be to just declare a whole circle, and then clip half of it off.
When we take all those SVG elements and wrap them in an <svg>
tag, we can see how simple the
code really is:
<svg
version=
"1.1"
baseProfile=
"full"
xmlns=
"http://www.w3.org/2000/svg"
>
<circle
cx=
"300"
cy=
"164"
r=
"160"
fill=
"yellow"
stroke=
"black"
stroke-width=
"2"
/>
<circle
cx=
"210"
cy=
"100"
r=
"20"
fill=
"black"
/>
<circle
cx=
"380"
cy=
"100"
r=
"20"
fill=
"black"
/>
<clipPath
id=
"MyClip"
>
<rect
x=
"30"
y=
"200"
width=
"600"
height=
"100"
/>
</clipPath>
<circle
cx=
"300"
cy=
"160"
r=
"120"
fill-opacity=
"0"
stroke=
"black"
stroke-width=
"5"
clip-path=
"url(#MyClip)"
/>
</svg>
These 11 lines of code are all we need to draw out our friendly smiley face. The code is significantly lighter than a JPEG or even a GIF that would represent the same image. Additionally, you inherit all the benefits of first-class DOM objects, as we previously discussed.
SVG has the same privileges as all other DOM elements, including the ability to be styled with CSS. This hack demonstrates how easy it is to create elements with SVG, and then turn them into illustrations with CSS.
The most powerful part of SVG is its standing in the DOM. SVG elements are first-class elements in HTML5, and they have every privilege that other DOM elements have. That being said, it’s simple to control the presentation of these elements with CSS.
For a refresher on how to implement SGV, see [Hack #42], which discusses how to create SVG elements.
SVG has the ability to control the presentation of its elements by setting attributes on the elements themselves. Here are some of the more popular presentation attributes in SVG:
fill
stroke
stroke-width
fill-opacity
height
width
x
,y
cx
,cy
orientation
color
cursor
clipPath
Many times it makes sense to embed these attributes within the SVG itself where it can be downloaded as one file. Other times it may be more flexible to create our base SVG elements within the SVG file, and style them with a language we are very familiar with: CSS.
To get started we will illustrate a simple smiley face with SVG elements. This is a basic illustration that consists of five elements, and nine lines of code:
<svg
version=
"1.1"
baseProfile=
"full"
xmlns=
"http://www.w3.org/2000/svg"
>
<circle
cx=
"300"
cy=
"164"
r=
"160"
fill=
"yellow"
stroke=
"black"
stroke-width=
"2"
/>
<circle
cx=
"210"
cy=
"100"
r=
"20"
fill=
"black"
/>
<circle
cx=
"380"
cy=
"100"
r=
"20"
fill=
"black"
/>
<clipPath
id=
"MyClip"
>
<rect
x=
"30"
y=
"200"
width=
"600"
height=
"100"
/>
</clipPath>
<circle
cx=
"300"
cy=
"160"
r=
"120"
fill-opacity=
"0"
stroke=
"black"
stroke-width=
"5"
clip-path=
"url(#MyClip)"
/>
</svg>
This simple code snippet gets stored in a file called smiley.svg and embedded into our page with an
<object>
tag as follows:
<object
data=
"smiley.svg"
type=
"image/svg+xml"
/>
Once the object is on the page, we see the SVG image as it should appear (see Figure 4-19).
In order to move our visual aspects of the illustration to CSS, we need to strip all the visual aspects out of our SVG. We basically want to leave ourselves with some raw shapes that we can manipulate. We will do this by removing most of the attributes from the SVG file. The one attribute we will not remove is the circle radius, as there is no CSS equivalent to this. Here is what our plain Jane SVG will look like:
<svg
version=
"1.1"
baseProfile=
"full"
xmlns=
"http://www.w3.org/2000/svg"
>
<circle
r=
"160"
class=
"head"
/>
<circle
r=
"20"
class=
"eye leftEye"
/>
<circle
r=
"20"
class=
"eye rightEye"
/>
<clipPath
id=
"MyClip"
>
<rect
class=
"clipBox"
width=
"100%"
height=
"100%"
/>
</clipPath>
<circle
r=
"120"
class=
"mouth"
/>
I want to point out a few things about the preceding SVG code.
First, note the rect
element with the
class of clipBox
. We have inserted a
width
and height
of 100%
. At the time of this writing, current
implementations of clip boxes require some height
and width
attributes set in the element to take
effect. Second, I have added a class
attribute to each element and assigned at least one class name to each
element. Although I could have assigned all the CSS via pseudotags based
on DOM position, I prefer to use class names, as they’re more flexible
if the DOM should change.
Since our elements have no look and feel to them, we end up with an SVG element that looks like Figure 4-20.
Our SVG elements have no visual characteristics or positioning, so
we’re starting with a series of circles stacked on top of the SVG
object. Think of your SVG object as being like an
iframe (an HTML element that loads a new page
inside your current page), having its own separate DOM. As an aside, if
you don’t want to use an <object>
tag to create your element on
the page, you can use an iframe to create SVG elements on the page as
well.
In the preceding example we have a few simple class names, such as
head
and eye
. If we write CSS declarations based on
these class names and put them in our master stylesheet, they won’t
actually affect our SVG elements, as the CSS will not cascade down to
the SVG elements. To resolve this issue we need to load our CSS in one
of three ways. The first way is with inline CSS where we put a style
attribute on the element itself and set
our styles directly in the element:
<circle
class=
"head"
r=
"160"
style=
fill:
yellow
;
stroke:
black
;
stroke-width:
2px
;
"
/>
The second way is by attaching a class name to the element (as we have) and then referencing it in an embedded style block. It’s important to remember that the style block needs to be directly in the SVG file, not in the page DOM:
<circle
class=
"head"
r=
"160"
/>
<style>
.head
{
fill
:
yellow
;
stroke
:
black
;
stroke
-
width
:
2px
;}
</style>
The third method, and our choice for this hack, is to attach a class name to the element and then reference an external stylesheet. Again, it’s important to reference the stylesheet from the SVG file and not from the HTML page file. There is also a bit of a twist on this stylesheet. SVG is XML-based but it isn’t HTML, so our traditional link reference will not work properly within the SVG file. For external CSS, the SVG specification references an ancient specification on referencing a stylesheet within an XML document. My only assumption was that this specification was part of the Dead Sea Scrolls discovery or something. You can find the old specification at w3.org.
According to this specification, the stylesheet is loaded with a tag at the top of the SVG file, like so:
<?xml-stylesheet type="text/css" href="/assets/css/svg.css"?>
This, of course, will have an href
that will point to the location of your
stylesheet on the server, so yours may look different from this
example.
Now that our structure is in place, let’s dig into the CSS that we will use to return our little smiley face to its full glory. We basically have two factors to deal with for each element. The first is the visual attributes, and the second is the position. The visual attributes are quite simple: you will see in our CSS that we have basically taken our old inline attributes of stroke, fill, stroke size, and the like and set them in CSS. Here is a sample from our CSS:
.head
{
fill
:
yellow
;
stroke
:
black
;
stroke
-
width
:
2px
;
}
.mouth
{
stroke
:
black
;
fill
-
opacity
:
0
;
stroke
-
width
:
5px
;
}
That was simple enough. The second factor to address is
positioning. In order to not have all our elements stacked up on top of
one another, we need to tell them where to go. For this, we will pull
out one of our new CSS3 attributes called transform
, which we will use to move our
elements into place. Here is a sample of our CSS3 transforms within our
CSS:
.eye
{
transform
:
translate
(
210px
,
100px
);
}
.rightEye
{
transform
:
translate
(
380px
,
100px
);
}
The transform
specifies the
translate
(or repositioning) of each
element from its current position, which again is with the radius
centered at (0,0) or the top-left corner of the SVG element.
Each element has CSS specified to provide the visual attributes and the positioning. When we put it all together, our CSS file contains the following declarations:
.head
{
fill
:
yellow
;
stroke
:
black
;
stroke
-
width
:
2px
;
transform
:
translate
(
300px
,
164px
);
}
.eye
{
fill
:
black
;
transform
:
translate
(
210px
,
100px
);
}
.rightEye
{
transform
:
translate
(
380px
,
100px
);
}
.mouth
{
stroke
:
black
;
fill
-
opacity
:
0
;
stroke
-
width
:
5px
;
clip
-
path
:
url(#MyClip)
;
transform
:
translate
(
0px
,
0px
);
}
.clipBox
{
width
:
600px
;
height
:
100px
;
transform
:
translate
(
30px
,
200px
);
}
For more information on how the clip path works, see [Hack #42] where we clarify how and why we use clip paths.
With this CSS, our finished product looks identical to the one we started with where all our attributes were directly within the SVG elements. Figure 4-21 shows our finished product.
Easily turn your SVG illustrations into SVG animations by adding a few lines of HTML or CSS. That’s right, no JavaScript is necessary for this easy animation.
Before HTML5, animation was cumbersome. It was never intended to be done on the Web, which might be why developers worked so hard to make it happen. Before HTML5, all animation had to be done with JavaScript. It took us back to the days of stop-frame animation where we had to move the object being animated one frame at a time. With JavaScript, we would slowly change the attribute we were trying to animate one or two pixels at a time. Whether it was height (to make a window slide open) or position (to animate something across the screen), JavaScript would repetitively alter the style attributes until the “animation” was complete. As you can imagine, it wasn’t only code-heavy, but processor-heavy as well.
Along comes SVG, bringing with it some easy-to-perform, hardware-accelerated animations. In this hack we’ll look at two animation options in our SVG tool belt.
SVG is completely XML-based. So it only makes sense that it has a tag for animation. Let’s start with a simple box and bouncing ball. This requires only a few lines of SVG:
<svg
version=
"1.1"
baseProfile=
"full"
xmlns=
"http://www.w3.org/2000/svg"
>
<rect
x=
"100"
y=
"0"
width=
"400"
height=
"100"
fill=
"pink"
stroke=
"black"
stroke-width=
"1"
/>
<circle
cx=
"120"
cy=
"50"
r=
"20"
fill=
"blue"
stroke=
"black"
stroke-width=
"1"
/>
</svg>
From this code we end up with a rectangle with a circle inside it (see Figure 4-22).
In order to animate this ball moving from one side of the
rectangle to the other, we will add a new tag and nest it inside the
circle
element, as a child element
(think of it as a command associated with the circle
element). Let’s look at our new SVG and
then we will walk through the details of the new tag:
<svg
version=
"1.1"
baseProfile=
"full"
xmlns=
"http://www.w3.org/2000/svg"
>
<rect
x=
"100"
y=
"0"
width=
"400"
height=
"100"
fill=
"pink"
stroke=
"black"
stroke-width=
"1"
/>
<circle
cx=
"120"
cy=
"50"
r=
"20"
fill=
"blue"
stroke=
"black"
stroke-width=
"1"
>
<animateMotion
path=
"M 0 0 H 380 Z"
dur=
"3s"
repeatCount=
"indefinite"
/>
</circle>
</svg>
This new animateMotion
tag
allows us to animate the circle
element while all other elements stay fixed. In this tag we are
utilizing three attributes:
path
The path is the hardest part of this tag. It appears to be a random list of numbers and letters that somehow give us a perfect path from one end of the rectangle to the other. This path is actually a wrap-up of our motion command. Breaking it down, the
M
represents the command to “move to” a new location, the0 0
is the x,y start position, andH
tells it to move horizontally. From there,380
is the distance it should move measured in points, and theZ
command closes the path and tells it to start back at the beginning. This notation is all part of the SMIL (Synchronized Multimedia Integration Language) Specification, the details of which you can access on the W3C website.dur
(duration)This attribute defines how long it will take to complete a full path. Values are represented in seconds with the format of
3s
.repeatCount
This attribute defines how many times the path will “repeat.” Don’t be fooled by the word repeat; a value of
1
will run the path only once, and a value of5
will run the path five times. In our case, we set it toindefinite
, so it will run until the page is closed or the value is changed.
Our ball will now bounce back and forth within the rectangle. With
SVG, animation is at the root of the language. Just as any other
component becomes a value in the DOM, so does our <animation>
tag, and it can be accessed
and altered with JavaScript. Figure 4-23 shows a view of
our end product.
In our first example, we made the <animation>
tag a child tag to the
element that was being animated. In many cases you may have a group of
tags that you want to animate. To address such situations we will pull
out some code from a previous hack of our smiley face created in SVG. If
we want to animate this smiley face back and forth on the screen, we
certainly don’t want to have to animate each element separately. This
would be both time-consuming to code and intensive on our processor, as
the engine would be calculating each element separately. Let’s look at
two different code samples showing how to animate a group of SVG tags
together.
Here is our first sample:
<svg
version=
"1.1"
baseProfile=
"full"
xmlns=
"http://www.w3.org/2000/svg"
>
<g>
<circle
cx=
"300"
cy=
"164"
r=
"160"
fill=
"yellow"
stroke=
"black"
stroke-width=
"2"
/>
<circle
cx=
"210"
cy=
"100"
r=
"20"
fill=
"black"
/>
<circle
cx=
"380"
cy=
"100"
r=
"20"
fill=
"black"
/>
<clipPath
id=
"MyClip"
>
<rect
x=
"30"
y=
"200"
width=
"600"
height=
"100"
/>
</clipPath>
<circle
cx=
"300"
cy=
"160"
r=
"120"
fill-opacity=
"0"
stroke=
"black"
stroke-width=
"5"
clip-path=
"url(#MyClip)"
/>
<animateMotion
path=
"M 0 0 H 300 Z"
dur=
"3s"
repeatCount=
"indefinite"
></animateMotion>
</g>
</svg>
Here is our second sample:
<svg
version=
"1.1"
baseProfile=
"full"
xmlns=
"http://www.w3.org/2000/svg"
>
<animateMotion
path=
"M 0 0 H 300 Z"
dur=
"3s"
repeatCount=
"indefinite"
>
<circle
cx=
"300"
cy=
"164"
r=
"160"
fill=
"yellow"
stroke=
"black"
stroke-width=
"2"
/>
<circle
cx=
"210"
cy=
"100"
r=
"20"
fill=
"black"
/>
<circle
cx=
"380"
cy=
"100"
r=
"20"
fill=
"black"
/>
<clipPath
id=
"MyClip"
>
<rect
x=
"30"
y=
"200"
width=
"600"
height=
"100"
/>
</clipPath>
<circle
cx=
"300"
cy=
"160"
r=
"120"
fill-opacity=
"0"
stroke=
"black"
stroke-width=
"5"
clip-path=
"url(#MyClip)"
/>
</animateMotion>
</svg>
In the first sample we had a parent element to our code, named
g
, which is code for “group”. Once we
group our code together, it’s treated as one element (on that level) and
our <animateMotion>
tag simply
becomes another child tag to the g
element whose job is to animate the group of elements.
In the second sample, instead of introducing the new tag we simply
use the <animateMotion>
tag as
a parent to enclose the tags that produce the smiley face. The <animateMotion>
parent tag animates the
tags nested inside it as a group, and the process is streamlined
significantly as compared to animating each element individually.
Don’t you just love SVG? Just like HTML, there is always more than one way to accomplish everything. This flexibility allows you to pick the method that works best in your particular situation. With SVG animation, there is no shortage of options.
Keeping in mind that SVG elements become DOM elements just like
any other HTML page elements, we can animate our SVG just as we would
HTML, by using CSS. In the
preceding sample that introduced the g
element, we can remove the <animateMotion>
tag completely, and set
an id
on the g
element. From here, we can use a CSS3
transform to create the same animation. For more on applying CSS to SVG
elements, see [Hack #43].
You can embed SVG directly within your HTML file, negating the need for an external .svg file. With HTML5, your SVG elements can live in the same DOM as your HTML, and you’ll be removing some of the barriers of managing the two code bases separately.
SVG is powerful and can be quite complex, creating limitless illustrations and animations with a simple XML-based language. But in some cases you may only have a simple illustration that doesn’t require the rigor of an external file to manage the code. Just as HTML5 provides the ability to inline images directly in your markup, SVG can be embedded directly within your HTML as well.
Looking at the code that is involved, you can see that it’s exactly
what you’d expect it to be. We have our HTML page, and instead of using an
<object>
tag that points to an
external SVG file, we see the entire content of the previously external
SVG file directly within our HTML. In our example, we will use our trusty
old smiley face SVG illustration embedded directly within our HTML:
<doctype
!
html
>
<html>
<head>
<meta
charset=
"utf-8"
>
<title>
SVG Sample</title>
<link
href=
"assets/css/bootstrap.css"
rel=
"stylesheet"
/>
<link
href=
"assets/css/bootstrap-responsive.css"
rel=
"stylesheet"
/>
<head>
<body>
<div
class=
"navbar... ...</div>
<h1> My Inline SVG Sample</h1>
<div id="
svgWrapper
"
class=
"row"
>
<svg
version=
"1.1"
baseProfile=
"full"
xmlns=
"http://www.w3.org/2000/svg"
>
<circle
cx=
"300"
cy=
"164"
r=
"160"
fill=
"yellow"
stroke=
"black"
stroke-width=
"2"
/>
<circle
cx=
"210"
cy=
"100"
r=
"20"
fill=
"black"
/>
<circle
cx=
"380"
cy=
"100"
r=
"20"
fill=
"black"
/>
<clipPath
id=
"MyClip"
>
<rect
x=
"30"
y=
"200"
width=
"600"
height=
"100"
/>
</clipPath>
<circle
cx=
"300"
cy=
"160"
r=
"120"
fill-opacity=
"0"
stroke=
"black"
stroke-width=
"5"
clip-path=
"url(#MyClip)"
/>
</svg>
</div>
</body>
</html>
In the preceding example, once the renderer sees the SVG declaration tag it switches parsers from HTML to SVG. When the tag closes it goes back from SVG to HTML. Our results look exactly as they did when the SVG was in an external file (see Figure 4-24).
Aside from the obvious “ease of use” argument, there are additional benefits to putting your SVG inline. Above all, inline SVG can be used as a performance enhancement. In some scenarios, you may benefit from not having to load the external file (as it will require an additional call to your server) and can put the SVG code directly in the page. Keep in mind that this is not always a performance enhancement, especially when you have a large amount of SVG code, which your browser may be able to load in parallel with your elements to save time. In cases like ours, when your illustration consists of only a few lines of code, it will generally be better in terms of performance to inline it and remove the additional call.
An additional benefit of inline SVG can be for DOM reference. In general, your SVG file is a separate DOM from your page when it’s loaded in an external SVG file (think of it as an iframe, an HTML element that loads a separate page inside the element). That being the case, any CSS that affects your SGV must be placed in or linked from your SVG file, and therefore can’t be applied to the HTML or the page. JavaScript similarly needs to access the SVG elements through the SVG object and falls prey to the same limitations as accessing items in an iframe. Moving the SVG into the DOM directly removes those barriers and allows you to truly treat your SVG elements just as any other DOM element.
Let’s look at a quick and quirky example of how inline SVG is affected by CSS declarations. In our example, we have a simple CSS declaration:
<style>
circle
{
stroke
:
red
;
stroke
-
width
:
12px
;
}
</style>
This style block is embedded directly into our HTML page, and in our page we have two SVG smiley faces. The first face is loaded as an external SVG image (Figure 4-25), and the second is loaded as an inline SVG image (Figure 4-26).
As you can see, the CSS applies to none of the circles within our embedded SVG and to every circle within our inline SVG. Embedded SVG may not always be the best choice for your documents, but it’s always nice to have options.
Get HTML5 Hacks 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.