Lesson 13 - Bitmap Fonts and Canvas 2d



Lesson 13 in NeHe is about using bitmapped fonts. This was important in the old days because support for vector fonts was very limited. For a long time, only Windows supported vector fonts (via the infamous "wgl" functions, so named because they were all prefixed with wgl). So being able to render text via bitmap fonts was actually important. Lesson 13 showed how to paint text on the screen via function glPrint, whch wrapped the wgl functions.

The sequence was

  • Load a font with specfied font-family, font-weight, size, etc. using the wgl functions
  • Specify the location of the text to be rendered
  • Create a raster image on the fly with the text passed in
  • Render it at the specified location

The wgl fucntions (as well as the Apple agl and Linux glx functions) no longer exist. Three.js supports vector fonts (lesssons 14 and 15) so being able to render bitmap fonts per se isn't that interesting. Instead, this lesson will show how to combine three.js' support for creating textures from HTML5's Canvas2D and three.js sprites to generate "text sprites". Sprites are a special type of geometry where the face of the geometry is always directly facing the camera. This can be handy if you are displaying some explanatory material or other info that isn't really intended to be part of the scene or SHOULD always be facing the camera.

So this lesson's content are some functions that allow you to render a sprite with specified text at a specified location. In additon, you can specify:

  • Font family, size, weight, etc.
  • Position of the sprite
  • Alignment of the sprite
  • Color of the sprite's background (including transparency)
  • Rounded corners

Introduction to the Sprites

So let's walk through the code and see how it works.

The demo creates a wireframe cube just to provide some reference (as well as the usual axes):

var cubeGeometry = new THREE.BoxGeometry( 1, 1, 1 );
var cubeMaterial = new THREE.MeshNormalMaterial( { wireframe:true } );
cube = new THREE.Mesh( cubeGeometry, cubeMaterial );

The function that makes the sprite is makeTextSprite( message, x, y, z, parameters )where message is the string to be rendered, x,y,z are the coordinates where the text is to be rendered, expressed in the current coordinate system. parameters is a JSON array which can contain the following parameters:

  • fontface (default is Arial)
  • fontsize (in pixels, default is 18)
  • borderThickness (in pixels, default is 4)
  • borderColor (expressed as a JSON array, default is none)
  • fillColor (expressed as a JSON array, default is { r:255, g:255, b:255, a:1.0 })
  • textColor (expressed as a JSON array, default is { r:0, g:0, b:0, a:1.0 })
  • radius (radius of corner, in pixels, default is 6)
  • vAlign (how text is aligned vertically, one of "top, center, bottom", default is "center")
  • hAlign (how text is aligned horizontally, one of "left, center, right", default is "center")

Here is one call as an example

var txSprite1 = makeTextSprite( "Bottom-Right", -1, 1, 0.25, { fontsize: 72, 
            fontface: "Georgia", borderColor: {r:0, g:0, b:255, a:1.0}, 
            borderThickness:4, fillColor: {r:255, g:255, b:255, a:1.0}, 
            radius:0, vAlign:"bottom", hAlign:"right" } );
gfxScene.add( txSprite1 );

A word about the alignment flags. The flags indicate which part of the text string is aligned to the specified position. bottom aligns the baseline of the string to the position. top aligns top of the character cell to the point. right aligns the end of bounding box for the string to the point, while left aligns the beginning of the bounding box. center for horizontal alignment uses the center of the string's bounding box. center for vertical alignment uses the midpoint between the baseline and top of the character cell. Note that the descenders of the string (e.g. 'g', 'p' and other characters whose lower part extend below the baseline aren't taken into account. So here is what the above call would result in:

The crosshairs and little red dot are simply debug references, they aren't normally drawn.

Creating the Sprites

So let's take a look at how the sprite is created. The first step is to create a canvas-2d context.

var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');

canvas.width = 1800;
canvas.height = 900;

Note that set an arbitrarily large size to the canvas. We don't care if it is oversize since the sprite is transparent, so only what we explicitly draw will be visible. First we set up the font. We set alignment to "left" since we'll do the alignment ourselves.

context.font = fontsize + "px " + fontface;
context.textBaseline = "alphabetic";
context.textAlign = "left";

Then we fetch all the various metrics information we'll need to position the text. Then we find the center of the canvas and the half of the font width and height. We do it this way because the sprite's position is the CENTER of the sprite.

var metrics = context.measureText( message );
var textWidth = metrics.width;
var cx = canvas.width / 2;
var cy = canvas.height / 2;  
var tx = textWidth/ 2.0;
var ty = fontsize / 2.0;

Then we calculate the offsets for the alignment. Since the position is the center of the sprite, then if the request is to center the text, we don't have to do anything.

if ( vAlign == "bottom")
    ty = 0;
else if (vAlign == "top")
    ty = fontsize;

if (hAlign == "left")
    tx = 0;
else if (hAlign == "right")
    tx = textWidth;

Next, we draw the rectangle (assuming the user asked for one).

roundRect(context, cx - tx , cy + ty + 0.28 * fontsize,
				textWidth, fontsize * DESCENDER_ADJUST, radius, 
                borderThickness, borderColor, fillColor);

The constant DESCENDER_ADJUST is an extra fudge factor for text with descenders the below baseline since we don't know the true bounding box of the text (only the height above the baseline). roundRect itself is very straightforward, we just set the border color and thickness if requested and draw the rect.

Finally, we set the text color and draw the text. Note that we have to wait until we draw the text to set its color because roundRect may have set the fillstyle to fill the rect.

context.fillStyle = getCanvasColor(textColor);
context.fillText( message, cx - tx, cy + ty);

Almost done. Create a texture from the canvas, then a SpriteMaterial from the texture and finally, the sprite itself.

var texture = new THREE.Texture(canvas);
texture.needsUpdate = true;

var spriteMaterial = new THREE.SpriteMaterial( { map: texture } );
var sprite = new THREE.Sprite( spriteMaterial );

The last step is to set the sprite's scale to 2:1. The canvas is already at a 2:1 scale (1800x900), but the sprite itself is square: 1.0 x 1.0. Note also that the size of the scale factors controls the actual size of the text-label. The value of the scale is somewhat arbitrary. Finally, set the sprite's position, which is in the CENTER of the sprite.

sprite.position.set(x, y, z);

And that's it! Click on this link to see the actual rendered demo in all its spritely glory!

As always, the original sources are on github here.

About Us | Contact Us | ©2017 Geo-F/X