Understanding The Canvas Measure Text Metrics

Published on Friday, October 2nd, 2020

When doing canvas drawing with text you may want to measure how the text is placed. Different fonts and different font sizes result in different pixel rendering, and it can be difficult to work with text pixel positioning in graphics programing.

One problem is that the TextMetrics object that comes out of canvas rendering context measureText() has a lot of information and it's not super clear what it all means.

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

ctx.font = 'italic 50px serif';

const metrics = ctx.measureText('Some text, but at what length?');

The metrics.width Is A Lie

You probably want to add actualBoundingBoxLeft and actualBoundingBoxRight, this will be a more acurate than just width. The reason is some fonts have glyphs that hang or stretch over their width used for text placement. This is especially true if you want to ensure text renders within certain confines, like UI for a game.

const metrics = ctx.measureText('Some text, but at what length?');
const width = metrics.width; // this can be slightly smaller

Use absolute values to calculate the width, depending on alignment and text these values may be negative.

const metrics = ctx.measureText('Some text, but at what length?');
const width = Math.abs(metrics.actualBoundingBoxLeft) +
Math.abs(metrics.actualBoundingBoxRight);

There Is No Height (or Spoon)

The only way to get the height of a piece of text is to measure from the text baseline the ascent or descent. Before this was avaialable the only thing was to guess the pixel height using the font size 😢.

The baseline is the horizontal line that the text rests against in some way. Some characters in a piece of text can be above or below in some amount from this line. The main textBaseline (and the default) is the alphabeticBaseline where text normally sits this is 0 by default. There are 2 other baselines, hangingBaseline and ideographicBaseline.

Calculating the real height of some text is the sum of actualBoundingBoxAscent and actualBoundingBoxDescent. Guessing by the font size doesn't really work.

const height = Math.abs(metrics.actualBoundingBoxAscent) + 
Math.abs(metrics.actualBoundingBoxDescent);

Drawing Bounds Around Text

Canvas text is drawn from the bottom left, which is different from the rectangle apis that draw from the top left.

One way to remember this is that the position specified in fillText('some text', 20, 20) is on the baseline. The text rests on the same line that the position does.

With this in mind to draw bounds around text we subtract the the actualBoundingBoxAscent from the y coordinate and actualBoundingBoxLeft from the x coordinate.

// [x, y]
const pos = [10, 100];
const bounds = {
top: pos[1] - metrics.actualBoundingBoxAscent,
right: pos[0] + metrics.actualBoundingBoxRight,
bottom: pos[1] + metrics.actualBoundingBoxDescent,
left: pos[0] - metrics.actualBoundingBoxLeft
};

Cool Things with Bounds

Once you have an accurate bounding box for the text there are a lot of cool things that can be done.

Finding the center of the text is the average of the correct bounds, let's you flip and rotate with ease!

const center = [
(bounds.left + bounds.right) / 2,
(bounds.top + bounds.bottom) / 2
];
// Move the transform to center in the middle of the text
ctx.save();
ctx.translate(center[0], center[1]);
// flip in y
ctx.scale(1, -1);
// rotate 45 degrees
ctx.rotate(Math.PI / 4);
ctx.fillText(text, pos[0] - center[0], pos[1] - center[1]);
ctx.restore();

Hope this helps with your canvas text endeavors! -Erik