2010-02-07
HTML5's canvas element, and a bit of SVG
An SVG image converted to a PNG and displayed as a lame metaphorical representation of an HTML5 canvas.
There are plenty of
tutorials out there on
how to use the
<canvas>
element
in HTML5, so I'm not going to
spend much time on that. Instead, we're going to talk capabilities. What
is <canvas>
and what can it do?
This blog entry is, of course, inspired by the whole Flash iPad Jobs Adobe-is-Lazy Is Flash Relevant HTML5 Thing. Here's my take: is Flash going to be supported on Apple products? No. Is Flash "open" enough to be used? Yes. Can Flash do things HTML5 can't? Absolutely. Is Flash advancing more quickly than HTML5? Yes. Does HTML5 suck? No. Will apps be written for the iPad in HTML5 in lieu of Flash? No, generally they'll be native apps, or web apps that take advantage of iPad-specific functionality. Are Apple users mindless sheep? No. Does the canvas tag have great uses? Yes. Is HTML5 as portable as Flash? No. Will it ever be? Not if history is any indicator. Will canvas+video+audio replace Flash? Sometimes, in specific instances when it makes sense, yes. Does Flash crash? Yes. Does it crash a lot? Not for me. Should Adobe make it crash less? Yes. Should Adobe release 64-bit versions of their player for all platforms? Yes. Should Adobe opensource their player? Tricky, but I think yes-like-Linux. (You can work on Gnash if you want it. Adobe has already opensourced their VM, so you can piggyback off that major chunk, too. An AS3 compiler is free with SWFTools. Vector graphics can be rendered with Cairo. The SWF spec is freely available. So go for it! No, I don't want to.) Will HTML5 kill Flash? If it gets developers more bang-for-the-buck, then yes. So probably "no".
(After this, follow up with my other blog entry: Pixel Manipulation with HTML5 Canvas.)
But first, a bit of history! The W3C years ago came up with a Recommendation (the highest level a spec can achieve) called SVG, Scalable Vector Graphics. It's an open standard for describing vector data in XML, and is importable and exportable from all kinds of drawing tools, and is the native format for the excellent free drawing program Inkscape. It's a declarative format, which means you build an XML file containing information about what you want to see, for example (stolen from w3cschools) this draws a line across the page:
<svg width="100%" height="100%"
version="1.1" xmlns="http://www.w3.org/2000/svg">
<line x1="0" y1="0" x2="300" y2="300"
style="stroke:rgb(99,99,99);stroke-width:2"/>
</svg>
The idea here is that you write an SVG file and load it up with circles, lines, spline curves, arcs, etc., and describe where everything is to be laid out on the page. (SVG is a fairly comprehensive format, with support for things like gradients, clipping shapes, transforms, text on paths, embedded images, and so on.)
Now, it's important to note that you're not saying, "Draw this line then draw this circle"; rather you're saying, "This line is here, and that circle is there." You are declaring what the scene is, rather than the steps you take to draw it.
But what if you don't want to work that way? What if you want to
describe how to draw the scene as a sequence of drawing steps? Maybe you
don't know in advance what the output will be. Maybe you'll need to
paint 100 million rectangles and you can't afford the memory needs that
SVG would require to do that. Maybe you need to do pixel-by-pixel
manipulation of the image for some kind of processing. These are things
that the HTML5 <canvas>
can do, and it can do it better than SVG.
And so <canvas>
was born.
HTML5, which describes the behavior of the <canvas>
element, recently
moved to "Working Draft" to "Last Call Working
Draft",
which means it's still somewhat up-in-the-air. It's not even a
candidate to be a recommendation. Compared to SVG in terms of being a
standard, SVG is level five-out-of-five, while HTML5 is level
two-out-of-five.
However, this means nothing in terms of compatibility and market
penetration, especially when we're talking about a very small portion of
the spec in <canvas>
, which is already well-implemented on all
browsers. Except Internet
Explorer,
of course.
If you want to track individual entities in your image, then SVG is better because each shape has its own node in the XML hierarchy. If you want to animate shapes across the page, it could very well be that SVG is better (because you merely set the shape's position and it becomes there—no need to explicitly redraw it or what was under it).
But if you want fire-and-forget drawing of boxes, arcs, text, and
images, <canvas>
can do it.
And how does it do it? One word:
plastics. No, wait—make
that: JavaScript. You declare
a <canvas>
element in your HTML, give it an "id" attribute, and then
your JavaScript code can grab that element and begin to draw on it. For
example, here's some HTML:
<canvas id="c1" width="480", height="320">
Your browser doesn't support canvas!
</canvas>
This declares a <canvas>
with id "c1
" and a given width and height
in pixels. Browsers that support the <canvas>
element won't show the
child elements. Browsers that don't support it will render whatever
appears below it, like an image, or some text telling the user that
<canvas>
support is required.
To draw on the canvas, you need to first get the <canvas>
element,
then from that get its drawing context. "Drawing context" is a fancy
term for "the current style in which we are drawing", or "the state that
describes how subsequent drawing commands will render in terms of color,
size, position, and so on."
If you set the drawing context to have lines 1 pixel thick, and then you draw 10 lines, they'll all be 1 pixel thick. If then you change the context to have lines 3 pixels thick, and then you draw 5 more lines, those 5 lines will be 3 pixels thick, but the original 10 will still be 1 pixel thick. You can think of setting the context as like "choosing your drawing implement from this point on", but it's actually a little bit more than that; it's everything that controls how drawing primitives will be drawn, including, but not limited to, color and line thickness, fill patterns and gradients.
Let's expand the HTML, above, into this:
<canvas id="c1" width="150", height="100">
Your browser doesn't support canvas!
</canvas>
<script type="text/javascript">
element = document.getElementById("c1");
c = element.getContext("2d");
c.fillStyle = "#aaaaaa";
c.fillRect(0, 0, 150, 100); // background gray
c.strokeRect(20, 10, 80, 60); // black rectangle
</script>
And in your browser, that looks like this:
Exercise: use the
c.scale()
method to make the rectangle a little bigger. Also, fill it with red.
You see that line up there where you call getContext()
? You see how
it takes a "2d" parameter? Presumably someday you might be able to
specify "3d" (or, holy cow, "4d", whatever that would be, Earthling!) to
get a 3D drawing context, but those days aren't really on the horizon.
(At this point, however, I must put out a nod to the nascent
WebGL.) For now, you're stuck in
flatland.
Let's do a little something more complex. In the following example, we're going to draw a series of 50 rectangles, and set the translation, rotation, and scaling of each rectangle so the result appears to snake across the screen like this:
<canvas id="c2" width="320", height="240">Canvas 2</canvas>
<script type="text/javascript">
element = document.getElementById("c2");
c = element.getContext("2d");
// gray backdrop:
c.fillStyle="#aaaaaa";
c.fillRect(0, 0, 320, 240);
// for subsequent boxes:
c.fillStyle="#ffff00";
c.lineWidth = 3;
for (i = 0; i < 50; i++) {
t = i / 50.0; // parameter [0..1] for subsequent ops
c.setTransform(1,0,0,1,0,0); // reset to identity
c.translate(10 + 270 * t, 10 + 220 * t);
c.scale(1 + t * 1.5, 1 + t * 1.5);
c.rotate(4.0 * t);
c.beginPath();
c.rect(0, 0, 30, 20);
c.stroke();
c.fill();
c.closePath();
}
</script>
Each drawing operation is affected by the drawing context, and the context includes the current translation, rotation, and scaling. You can set the rotation, and then subsequent drawing ops will be affected by it, just as if you set the color.
So in this example, we draw 50 rectangles. We calculate a parameter t
that runs from 0 to 1 as the rectangle counter runs from 0 to 49, and we
use t
to determine how much to translate, scale, and rotate the
rectangles.
Then we draw a rectangle using the rect()
method; we draw it at 0, 0
with width 30 and height 20. But if it's drawn at 0,0 every time, how
does it appear to move across the canvas? It's because we've set the
translation in the current drawing context through a call to
translate()
. The position we translate to depends on the parameter
t
, so each rectangle appears in a different place. It's a similar
situation for rotate()
and scale()
.
What does that call to setTransform(1,0,0,1,0,0)
actually do? The
short answer is that it resets the current transform to "normal". It
resets it to what is called "The Identity
Matrix" (which would be
an excellent title for a movie.) In other words, if there were any
rotations, translations, or scalings in effect, those effects are
nullified. The identity matrix looks like this:
$$\begin{vmatrix} \textbf{1} &0 &0 \\ 0 &\textbf{1} &0 \\ 0 &0 &\textbf{1} \end{vmatrix}$$
And when you call setTransform(A,b,c,d,E,f)
, it sets the matrix to
this:
$$\begin{vmatrix} \textbf{A} &b &c \\ d &\textbf{E} &f \\ 0 &0 &\textbf{1} \end{vmatrix}$$
To more fully explain, before every point is drawn, that point is multiplied by the current transformation matrix, which is stored in the drawing context, and the output of that multiplication is where the point actually gets drawn. This is what makes the identity matrix what it is: if you multiply a point by the identity matrix, you get the same point back out, unchanged! If you multiply it by something that's not the identity matrix, the point comes back changed. By properly manipulating the transformation matrix, you can cause the point to come back rotated to a new position, translated, scaled, sheared, or even something else crazy.
Now how do you "properly" manipulate the matrix? The details are (just)
a little bit out of scope for this blog entry, but suffice it to say,
the context methods translate()
, rotate()
, scale()
, and
transform()
all "properly" manipulate the current transformation
matrix for you.
So, if you call rotate()
, it calculates out some
sines and cosines that
represent the rotation and bakes them into the current transformation
matrix. Then if you wanted, you could call translate()
to reposition
the output points by a certain amount in the X and Y directions. Now,
and this is very important, remember that operations on the
transformation matrix are cumulative! This means that your call to
translate()
will be affected by your previous call to rotate()
! If
you've already rotated 45 degrees, then translate(10, 0)
will actually
head off at a 45-degree angle, instead of straight across as you were
perhaps hoping for! But if you call translate(10,0)
first, and
then call rotate()
, you will rotate in place at coordinates 10,0.
The order in which you modify the transformation matrix matters very
much!
Along those same lines, if you call rotate(2)
and then call
rotate(3)
, it's cumulative! It's like calling rotate(5)
(5 being 2 +
3). The same thing happens if you call translate(10)
repeatedly—they
cumulatively add up. All operations on the matrix are affected by
all operations on the matrix that came before them.
(And now you're more ready for some 3D math, because these operations are the same in 3D as they are in 2D—just with an additional coordinate!)
Coming full circle, then, I call setTransform(1,0,0,1,0,0)
each time,
because that is the call that resets the transform to the identity
matrix. It restarts us from scratch.
Finally, you'll see a call to stroke()
in there. This tells the canvas
to "draw all lines (strokes) in this path that we've accumulated so
far." The call to fill()
does the same thing. You don't have to call
fill()
every time you call rect()
; by, for instance, stroke()
ing
all the lines first, and then calling fill()
once, you could fill the
union of all the shapes instead of filling them one at a time.
Exercise: add a call to
c.transform()
in the "Canvas 2" example that will cause each rectangle to be
sheared
just before it is drawn.
Exercise: using the context's
save()
and
restore()
methods (which push and pop the current context on a
stack), remove
the call to setTransform()
in the Canvas 2 example and still achieve
the same result. Hint: the transform automatically starts off as the
identity matrix.
Exercise: using save()
and restore()
, remove the call to
setTransform()
and only call translate()
with constant values and
still achieve the same result.
Further stuff to try: images, text, arcs, gradients, pixel manipulation, pattern fills, carrot cake.
Canvas references: w3cschools tutorial, HTML5 draft
See Also: My other blog entry: Pixel Manipulation with HTML5 Canvas
While using context.drawSvg() i get the response as 206 Partial content, does anyone hv a solution