Drawing Text

Filling, Stroking and Measuring Text

ctx.font takes the CSS font shorthand and ignores values lacking a size or family. textAlign places text against x and textBaseline (alphabetic, top, middle, bottom and others) against y. fillText() and strokeText() draw it; measureText() returns a TextMetrics whose actualBoundingBox* values give the ink's extent.

A title's measured ink box, its baseline, and outlined textHTML
<canvas id="c" style="width:600px; height:82px"></canvas><script src="hidpi.js"></script>
<script>
  const ctx = hidpi(c), title = 'Salt and Saffron';
  ctx.font = 'bold 36px Georgia';
  const m = ctx.measureText(title);
  ctx.strokeStyle = '#b5452f';  ctx.strokeRect(10 - m.actualBoundingBoxLeft,
    52 - m.actualBoundingBoxAscent, m.actualBoundingBoxLeft + m.actualBoundingBoxRight,
    m.actualBoundingBoxAscent + m.actualBoundingBoxDescent);          // the ink box
  ctx.fillStyle = '#e09a10';  ctx.fillText(title, 10, 52);
  ctx.fillRect(0, 52, 330, 1);                                        // the baseline
  ctx.lineWidth = 8;  ctx.lineJoin = 'round';  ctx.strokeStyle = '#263238';
  ctx.strokeText('SOLD OUT', 360, 52);                                // outline first...
  ctx.fillStyle = '#fff3e0';  ctx.fillText('SOLD OUT', 360, 52);      // ...then the fill
  ctx.font = '13px sans-serif';  ctx.fillStyle = '#263238';
  ctx.fillText(`width ${m.width.toFixed(1)}, ascent ${m.actualBoundingBoxAscent}`, 10, 76);
</script>
Browser output of Listing 2.13
Browser output of 13

Stroking first and filling on top keeps the letters intact, like SVG's paint-order (<text> and <tspan>). Load a web font before drawing, or the fallback is painted for good: await document.fonts.load('36px Lobster').

fillText, strokeText and the ink box from measureText, with every textBaselineHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; background: #fafaf7; }
  canvas { width: 100%; max-width: 600px; height: 190px; display: block; background: #fff; border: 1px solid #ddd; }
</style>
<canvas id="c"></canvas>
<script>
  function hidpi(c, r = devicePixelRatio) {
    c.width = c.clientWidth * r;  c.height = c.clientHeight * r;
    const ctx = c.getContext('2d');  ctx.scale(r, r);  return ctx;
  }
  const ctx = hidpi(document.getElementById('c')), title = 'Salt and Saffron';

  // 1. Measure the title and outline its actual ink box
  ctx.font = 'bold 36px Georgia';
  const m = ctx.measureText(title);
  ctx.strokeStyle = '#b5452f';
  ctx.strokeRect(10 - m.actualBoundingBoxLeft, 52 - m.actualBoundingBoxAscent,
                 m.actualBoundingBoxLeft + m.actualBoundingBoxRight,
                 m.actualBoundingBoxAscent + m.actualBoundingBoxDescent);
  ctx.fillStyle = '#e09a10';  ctx.fillText(title, 10, 52);
  ctx.fillRect(0, 52, 330, 1);                                   // the baseline

  // 2. Outline first, then fill on top, so the stroke doesn't eat the letters
  ctx.lineWidth = 8;  ctx.lineJoin = 'round';  ctx.strokeStyle = '#263238';
  ctx.strokeText('SOLD OUT', 370, 52);
  ctx.fillStyle = '#fff3e0';  ctx.fillText('SOLD OUT', 370, 52);

  ctx.font = '13px sans-serif';  ctx.fillStyle = '#263238';
  ctx.fillText(`width ${m.width.toFixed(1)}px, ascent ${m.actualBoundingBoxAscent.toFixed(1)}px, ` +
               `descent ${m.actualBoundingBoxDescent.toFixed(1)}px`, 10, 80);

  // 3. textBaseline positions the text against the y coordinate (red line at y = 140)
  ctx.fillStyle = '#b5452f';  ctx.fillRect(0, 140, 600, 1);
  ctx.font = '18px Georgia';  ctx.fillStyle = '#1f5f8b';
  ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom'].forEach((base, i) => {
    ctx.textBaseline = base;
    ctx.fillText('Abg', 14 + i * 98, 140);
    ctx.save();  ctx.textBaseline = 'alphabetic';  ctx.font = '11px sans-serif';  ctx.fillStyle = '#555';
    ctx.fillText(base, 14 + i * 98, 180);  ctx.restore();
  });
</script>