WebGL Tutorial
and more

绘制文本

撰写时间:2024-04-15

修订时间:2024-04-17

基本用法

ctx.font = '48px Helvetica'; ctx.strokeStyle = 'WHITE'; ctx.strokeText('Hello', 50, 70); ctx.fillStyle = 'DEEPSKYBLUE'; ctx.fillText('Canvas', 100, 130);

ctxfont属性格式为CSS font的格式,详见CSS font

限制文本宽度

ctx.font = 'bold italic 48px Helvetica'; ctx.fillStyle = 'DEEPSKYBLUE'; ctx.fillText('Canvas', 70, 70); let maxWidth = 100; ctx.fillStyle = 'DEEPSKYBLUE'; ctx.fillText('Canvas', 70, 130, maxWidth); ctx.strokeStyle = 'gray'; ctx.strokeRect(70, 130, maxWidth, 1);

strokeTextfillText还有指定一个可选的第4个参数,用于指定最大宽度。

上图中,上面的文本尺寸正常,下面的文本则通过将最后一个参数设置为100,从而将文本的最大宽度限定在100px的范围内。

修改变量maxWidth的值,观察不同的运行效果。如果最大宽度值超过原来尺寸,不会改变原来尺寸;只有最大宽度值小于原来尺寸,才会通过自动改变字体大小来改变原来尺寸。

这个特性便利的地方是可自动计算字体大小来适应最大宽度,但不便利的地方是只可设置最大宽度,但不能设置最大高度。

指定文本开始的偏移位置

因为文本有面积大小,因此其范围是一个矩形。默认情况下,strokeTextfillText方法中的参数指定的位置是该矩形的左下角的位置。

ctx.font = 'bold italic 48px Helvetica'; let x = 50; let y = 50; ctx.fillStyle = 'DEEPSKYBLUE'; ctx.fillText('Canvas', x, y); let r = 5; ctx.beginPath(); ctx.fillStyle = 'orange'; ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();

可通过设置ctxtextBaseline属性值来将参数定位到垂直方向上的特定位置,设置textAlign属性值来将参数定位到水平方向上的特定位置。

ctx.font = 'bold italic 48px Helvetica'; ctx.textBaseline = 'middle'; ctx.textAlign = 'center'; let x = 100; let y = 50; ctx.fillStyle = 'DEEPSKYBLUE'; ctx.fillText('Canvas', x, y); let r = 5; ctx.beginPath(); ctx.fillStyle = 'orange'; ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();

通过上面的指定,我们表达了参数(100, 50)所在的位置位于文本水平方向上的中间以及文本垂直方向上的中间的位置

textAlign的值有:

  • left
  • right
  • center
  • start
  • end

默认值为start

ctx.font = '28px Helvetica'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'DEEPSKYBLUE'; let x = 100; let startY = 30; let offsetY = 30; let r = 5; ['start', 'left', 'center', 'end', 'right'].forEach((align, index) => { let y = startY + offsetY * index; ctx.textAlign = align; ctx.fillText(align, x, y); drawPoint(x, y); }); function drawPoint(x, y) { ctx.save(); ctx.beginPath(); ctx.fillStyle = 'orange'; ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } ctx.beginPath(); ctx.strokeStyle = '#CCC'; ctx.moveTo(x, startY - offsetY); ctx.lineTo(x, startY + offsetY * 5); ctx.stroke();

因为可以通过CSS来指定文本从左到右或从右到左的布局,因此,startend在这些不同的布局下有不同的含义。在从左到右的布局中,startleft的含义相同,endright的含义相同;而在从右到左的布局中,startright的含义相同,endleft的含义相同。

textBaseline指定参数位于垂直方向上的哪个位置。

baseline

其值为:

  • top
  • hanging
  • middle
  • alphabetic
  • ideographic
  • bottom

默认值为alphabetic。在大多数情况下,我们只需选用top, middle, bottom就够了。

ctx.font = '28px Helvetica'; ctx.textAlign = 'center'; ctx.fillStyle = 'DEEPSKYBLUE'; let x = 100; let startY = 30; let offsetY = 70; ['top', 'middle', 'bottom'].forEach((baseline, index) => { let y = startY + offsetY * index; ctx.textBaseline = baseline; ctx.fillText(baseline, x, y); drawHorzline(y); }); function drawHorzline(y) { ctx.beginPath(); ctx.strokeStyle = 'gray'; ctx.moveTo(20, y); ctx.lineTo(180, y); ctx.stroke(); }

文本信息

measureText

可通过measureText来获取相关的文本信息。

ctx.font = '48px Helvetica'; ctx.textBaseline = 'middle'; ctx.textAlign = 'center'; let text = 'Hello'; const metrics = ctx.measureText(text); ctx.fillStyle = 'DEEPSKYBLUE'; ctx.fillText(text, 100, 50);

一般应先将相应的字体属性设置好,再调用measureText方法。该方法返回一个类型为TextMetrics的对象,有较多的关于文本的各类信息。这些信息下节详述。

文本宽度

对于所返回的TextMetrics的实例,其width属性存储了整个文本的宽度,以CSS像素为单位。

ctx.font = '48px Helvetica'; ctx.textBaseline = 'middle'; ctx.textAlign = 'center'; let text = 'Hello'; const metrics = ctx.measureText(text); let textWidth = metrics.width; console.log(textWidth); let textHeight = 70; let x = 100, y = 50; ctx.fillStyle = 'DEEPSKYBLUE'; ctx.fillText(text, x, y); ctx.strokeStyle = 'NAVAJOWHITE'; ctx.strokeRect(x - textWidth / 2, y - textHeight / 2, textWidth, textHeight);

还可通过metricsactualBoundingBoxLeftactualBoundingBoxRight属性值来求出文本的宽度值。

ctx.font = '48px Helvetica'; ctx.textBaseline = 'middle'; let text = 'Hello'; let textWidth; let boundingLeft; let boundingRight; let textHeight = 70; let x = 120; let y = 50; let offsetY = 120; let r = 5; let gap = 20; ['left', 'center', 'right'].forEach(align => { ctx.textAlign = align; mesureText(); drawShapes(align, x, y); y += offsetY; }); function mesureText() { const metrics = ctx.measureText(text); textWidth = metrics.width; boundingLeft = metrics.actualBoundingBoxLeft; boundingRight = metrics.actualBoundingBoxRight; console.log(textWidth); console.log(boundingLeft, boundingRight); console.log(boundingLeft + boundingRight); console.log(); } function drawShapes(align, x, y) { ctx.fillStyle = 'DEEPSKYBLUE'; ctx.fillText(text, x, y); ctx.beginPath(); ctx.moveTo(x, y + textHeight / 2 + gap); ctx.lineTo(x - boundingLeft, y + textHeight / 2 + gap); ctx.moveTo(x, y + textHeight / 2 + gap); ctx.lineTo(x + boundingRight, y + textHeight / 2 + gap); ctx.strokeStyle = 'gray'; ctx.stroke(); if (align === 'left') { ctx.strokeStyle = 'NAVAJOWHITE'; ctx.strokeRect(x, y - textHeight / 2, textWidth, textHeight); ctx.beginPath(); ctx.fillStyle = 'green'; ctx.arc(x + boundingLeft, y + textHeight / 2 + gap, r, 0, Math.PI * 2); ctx.fill(); } else if (align === 'center') { ctx.strokeStyle = 'NAVAJOWHITE'; ctx.strokeRect(x - textWidth / 2, y - textHeight / 2, textWidth, textHeight); ctx.beginPath(); ctx.fillStyle = 'green'; ctx.arc(x, y + textHeight / 2 + gap, r, 0, Math.PI * 2); ctx.fill(); } else if (align === 'right') { ctx.strokeStyle = 'NAVAJOWHITE'; ctx.strokeRect(x - textWidth, y - textHeight / 2, textWidth, textHeight); ctx.beginPath(); ctx.fillStyle = 'green'; ctx.arc(x, y + textHeight / 2 + gap, r, 0, Math.PI * 2); ctx.fill(); } }

从上面的代码及运行结果来看,actualBoundingBoxLeftactualBoundingBoxRight的值与文本水平开始位置有关。actualBoundingBoxLeft是文本水平开始位置至边框左边界的距离,正数表示actualBoundingBoxLeft位于对齐点的左边;而actualBoundingBoxRight是文本水平开始位置至边框右边界的距离,正数表示actualBoundingBoxRight位于对齐点的右边。这些距离都以CSS像素为单位。

而不管文本水平如何对齐,actualBoundingBoxLeft加上actualBoundingBoxRight的值,正好等于width的值。

各个浏览器中上面各个属性值可能有一些细微区别,为方便起见,可选用width作为文本的最外围的宽度。

文本高度

ctx.font = '72px Helvetica'; ctx.textAlign = 'center'; let text = 'Hello'; let x = 120; let y = 100; const LABEL_PADDING = 10; let HORZ_LINE_START_X = 0; let HORZ_LINE_END_X = 220; ctx.textBaseline = 'middle'; ctx.fillStyle = 'DEEPSKYBLUE'; ctx.fillText(text, x, y); let metrics = ctx.measureText(text); console.log(`actualBoundingBoxAscent: ${metrics.actualBoundingBoxAscent}`); console.log(`actualBoundingBoxDescent: ${metrics.actualBoundingBoxDescent}`); console.log(`fontBoundingBoxAscent: ${metrics.fontBoundingBoxAscent}`); console.log(`fontBoundingBoxDescent: ${metrics.fontBoundingBoxDescent}`); console.log(`emHeightAscent: ${metrics.emHeightAscent}`); console.log(`emHeightDescent: ${metrics.emHeightDescent}`); console.log(`hangingBaseline: ${metrics.hangingBaseline}`); console.log(`alphabeticBaseline: ${metrics.alphabeticBaseline}`); console.log(`ideographicBaseline: ${metrics.ideographicBaseline}`); drawHelpers(); function drawHelpers() { const SECOND_COL_CAP = 130; ctx.save(); ctx.strokeStyle = 'lightgreen'; ctx.font = '10px Arial'; ctx.textAlign = 'left'; ctx.fillStyle = 'cyan'; ctx.beginPath(); ctx.moveTo(HORZ_LINE_START_X, y - metrics.actualBoundingBoxAscent); ctx.lineTo(HORZ_LINE_END_X, y - metrics.actualBoundingBoxAscent); ctx.stroke(); ctx.fillText('actualBoundingBoxAscent', HORZ_LINE_END_X + LABEL_PADDING, y - metrics.actualBoundingBoxAscent); ctx.beginPath(); ctx.moveTo(HORZ_LINE_START_X, y - metrics.fontBoundingBoxAscent); ctx.lineTo(HORZ_LINE_END_X, y - metrics.fontBoundingBoxAscent); ctx.stroke(); ctx.fillText('fontBoundingBoxAscent', HORZ_LINE_END_X + LABEL_PADDING + SECOND_COL_CAP, y - metrics.fontBoundingBoxAscent); ctx.beginPath(); ctx.moveTo(HORZ_LINE_START_X, y - metrics.emHeightAscent); ctx.lineTo(HORZ_LINE_END_X, y - metrics.emHeightAscent); ctx.stroke(); ctx.fillText('emHeightAscent', HORZ_LINE_END_X + LABEL_PADDING, y - metrics.emHeightAscent); ctx.beginPath(); ctx.moveTo(HORZ_LINE_START_X, y + metrics.actualBoundingBoxDescent); ctx.lineTo(HORZ_LINE_END_X, y + metrics.actualBoundingBoxDescent); ctx.stroke(); ctx.fillText('actualBoundingBoxDescent', HORZ_LINE_END_X + LABEL_PADDING, y + metrics.actualBoundingBoxDescent); ctx.beginPath(); ctx.moveTo(HORZ_LINE_START_X, y + metrics.fontBoundingBoxDescent); ctx.lineTo(HORZ_LINE_END_X, y + metrics.fontBoundingBoxDescent); ctx.stroke(); ctx.fillText('fontBoundingBoxDescent', HORZ_LINE_END_X + LABEL_PADDING + SECOND_COL_CAP, y + metrics.fontBoundingBoxDescent); ctx.beginPath(); ctx.moveTo(HORZ_LINE_START_X, y + metrics.emHeightDescent); ctx.lineTo(HORZ_LINE_END_X, y + metrics.emHeightDescent); ctx.stroke(); ctx.fillText('emHeightDescent', HORZ_LINE_END_X + LABEL_PADDING, y + metrics.emHeightDescent); ctx.beginPath(); ctx.moveTo(HORZ_LINE_START_X, y - metrics.hangingBaseline); ctx.lineTo(HORZ_LINE_END_X, y - metrics.hangingBaseline); ctx.stroke(); ctx.fillText('hangingBaseline', HORZ_LINE_END_X + LABEL_PADDING + SECOND_COL_CAP * 2, y - metrics.hangingBaseline); ctx.beginPath(); ctx.moveTo(HORZ_LINE_START_X, y - metrics.alphabeticBaseline); ctx.lineTo(HORZ_LINE_END_X, y - metrics.alphabeticBaseline); ctx.stroke(); ctx.fillText('alphabeticBaseline', HORZ_LINE_END_X + LABEL_PADDING + SECOND_COL_CAP, y - metrics.alphabeticBaseline); ctx.beginPath(); ctx.moveTo(HORZ_LINE_START_X, y - metrics.ideographicBaseline); ctx.lineTo(HORZ_LINE_END_X, y - metrics.ideographicBaseline); ctx.stroke(); ctx.fillText('ideographicBaseline', HORZ_LINE_END_X + LABEL_PADDING + SECOND_COL_CAP * 2, y - metrics.ideographicBaseline); ctx.restore(); }

在各个与高度有关的属性中,fontBoundingBoxAscent位于最上面,fontBoundingBoxDescent位于最下面,可选用这两个属性值作为最外围的边界。

在Chrome浏览器中,涉及到文本高度的属性中,只有actualBoundingBoxAscent, actualBoundingBoxDescent, fontBoundingBoxAscentfontBoundingBoxDescent这4个属性,无emHeightAscent, emHeightDescent, hangingBaseline, alphabeticBaselineideographicBaseline这5个属性。

参考资源

  1. HTML5 Canvas Element
  2. CSS Snapshot 2023
  3. CSS Fonts Module Level 3
  4. CSS Writing Modes Level 4