Rendering Text Inside the Canvas Object #
I recently had an idea for a cool hack involving the <canvas>
tag/object that is supported by Safari 1.3/2.0, Firefox 1.5 and Opera 9.0. However, I quickly realized that the object does not support text rendering, which made it seemingly useless. The WHATWG spec had this to say:
// drawing text is not supported in this version of the API
// (there is no way to predict what metrics the fonts will have,
// which makes fonts very hard to use for painting)
I'm not entirely sure why predictable font metrics are necessary (for pixel-perfect compliance testing I suppose), but the situation doesn't seem too hopeful. Mozilla's solution to this was the drawWindow
, function that could be used with an iframe to render text. However, creating arbitrary windows for text rendering seemed like a lot of overhead, and I wanted something that worked in all browsers.
I remembered from my OpenGL hacking days that a similar shortcoming existed in that environment, and that there were a few workarounds available. One was to render fonts yourself, using the basic line/arc primitives to rasterize TrueType/PostScript fonts. However, this meant finding a font and mapping its drawing operations to canvas ones, which seemed like more work than I was willing to put in for a quick hack. Additionally, having to draw many complex strokes per letter seemed like it would impact performance.
The alternative was to use a font texture. This is usually an image composed of all the necessary letters and symbols for a font. By drawing pieces of it one after another, words can be composed. Since the font has been already rasterized into the texture image, it shouldn't matter how complex each letter is. This also mapped well onto the drawImage
call supported by the canvas 2D context. Some quick Googling turned up a ready-made font texture and (more importantly) a table of character coordinates positions within it. If doing this from scratch, Bitmap Font Builder looks handy.
This is the result of putting all of that together. It has decent performance in Safari and Opera 9.0, but Firefox can only manage about ten frames per second. It was even slower when I used drawImage()
with an image object. I can only assume that Gecko will decompress the image for each function call instead of keeping the raw pixels around. Thankfully there is an overloaded version of the function that accepts a canvas object. By rendering the desired image into a canvas first and then passing that, performance improved significantly. However, Safari does not seem to support canvas objects as arguments for drawImage()
, so a bit of browser detection is necessary.
Update on 2/27/2006: I was curious about the drawImage()
performance in Firefox with images vs. canvases that I decided to do a more thorough investigation. Using a simple test bed, I measured the speed of various image rendering calls:
drawImage()
with an image argumentdrawImage()
with a canvas argument- Creating a pattern with an image and then using
fillRect()
with it - Creating a pattern with a canvas and then using
fillRect()
with it
I then ran 50 iterations of each in Firefox 1.5, Safari 2.0 and Opera 9.0 preview 2, all on a dual 2.3 Ghz G5, with these results:
Method | 8-bit opqaue GIF | 8-bit transparent GIF* | 8-bit opaque PNG* | 8-bit transparent PNG* | 24-bit opaque PNG | 24-bit transparent PNG | JPEG |
---|---|---|---|---|---|---|---|
Firefox 1.5 | |||||||
drawImage w/ image | 74 | 138 | 593 | 574 | 242 | 1959 | 227 |
drawImage w/ canvas | 446 | 4378 | 4433 | 4444 | 443 | 495 | 454 |
fillRect w/ pattern w/ image | 10 | 22 | 75 | 33 | 32 | 118 | 27 |
fillRect w/ pattern w/ canvas | err | err | err | err | err | err | err |
Safari 2.0 | |||||||
drawImage w/ image | 15 | 27 | 97 | 34 | 47 | 123 | 62 |
drawImage w/ canvas | NV | NV | NV | NV | NV | NV | NV |
fillRect w/ pattern w/ image | NV | NV | NV | NV | NV | NV | NV |
fillRect w/ pattern w/ canvas | err | err | err | err | err | err | err |
Opera 9.0 preview 2 | |||||||
drawImage w/ image | 521 | 273 | 3313 | 880 | 1651 | 4007 | err |
drawImage w/ canvas | 3817 | 37612 | 37186 | 38024 | 3753 | 3862 | err |
fillRect w/ pattern w/ image | 3773 | 36019 | 36735 | 37303 | 3709 | 3571 | err |
fillRect w/ pattern w/ canvas | NV | NV | NV | NV | NV | NV | err |
* 500 iterations
NV: No visible output (but no exceptions thrown either)
As it can be seen, the Firefox performance boost that I saw with drawImage()
and a canvas argument only occurs with 24-bit PNGs with an alpha channel. In general, using a pattern is the fastest way to draw an image in Firefox. The one trade-off is that you don't get to use scaling (by playing with the source/destination rectangles), but that can be accomplished with the global matrix transform anyway. Since paterns are always drawn beginning at the top/left corner of the target rectangle, some use of clipping is necesary if only a portion of the image is necessary. However, even with clipping, the use of patterns brings Firefox speed in the text rendering test to ~36 fps instead of ~10 fps.
The Opera numbers are much lower than the others because Opera seems to do some event handling and extra screen refreshes during the benchmark. In general, the fastest approach in Opera is to use drawImage()
with a canvas object. Safari seems to have the most trouble supporting alternate approaches, presumably because it had the earliest implementation of canvas and the spec didn't actually exist at that point.
8 Comments
In practice Flash is the better/faster/more broadly supported solution, so it's all an academic discussion.
I've tested html-, image- and stroke-text.
The implementation of vector fonts is the wrong way because of the complexity. But integrating the very simple stroke fonts from CAD programs is much more interesting.
Here is the sample link:
http://www.netzgesta.de/S5/canvas-text.html
Post a Comment