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 argument
  • drawImage() 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

Another option (the one I've looked into) is to use SVG, which manipulable with the DOM. Why not use that?
In theory Canvas has broader support (the currently shipped version of Safari supports it, but only nightly builds support SVG). It's also possible to get it working in IE by layering it on top of VML (the same might be possible with SVG, but it's a more complex spec).

In practice Flash is the better/faster/more broadly supported solution, so it's all an academic discussion.
Interesting; has the disadvantage that non-ASCII characters stop the animation, annoying if you need to use a Euro sign, or a language more demanding than Swahili in its character repertoire.
You can pre-render a font texture with as many characters as necessary, but yes, it can get unwieldly if support for many languages is necessary.
Is this slow or you have set some parameter?
Right now it's drawing as fast as it can. Performance will vary depending on CPU speed (and to a lesser extent, browser).
This doesn't work for a dashboard widget; change the check for agent "safari" to "webkit" to be a little more general.
I've extended the features of S5 1.3 (aka Reloaded) with dynamically generated, scalable pie charts in canvas objects through parsing html tables. I needed text for the percentage strings (0123456789%.).

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