Beej's Bit Bucket

 ⚡ Tech and Programming Fun

HTML5's canvas element, and a bit of SVG

2010-02-07
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
<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:

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:

HTML
<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:


Your browser doesn't support HTML canvas, so you get the above plain image, instead.
Get a modern browser such as Chrome, Firefox, Opera, or IE9.

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 2 image
Your browser doesn't support HTML canvas, so you get the above plain image, instead.

HTML
<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:

1   0   0⎤
   ⎢0   1   0⎥
   ⎣0   0   1

And when you call setTransform(A,b,c,D,e,f), it sets the matrix to this:

A   c   e⎤
   ⎢b   D   f⎥
   ⎣0   0   1

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 AlsoMy other blog entry: Pixel Manipulation with HTML5 Canvas

Share me!

Historic Comments

 zendrej 2011-03-03 04:53:37

While using context.drawSvg() i get the response as 206 Partial content, does anyone hv a solution

Comments

blog comments powered by Disqus
Blog  ⚡  Email beej@beej.us  ⚡  Home page