Skip to content

文本渲染

Canvas 不仅能绘制图形,还提供了完整的文本渲染能力。通过 fillText()strokeText() 以及相关的字体、对齐、测量 API,可以在画布上绘制任意样式的文字,并精确控制其位置和大小。本节将详细介绍 Canvas 文本渲染的各个方面。

前置知识

阅读本节前,建议先了解:颜色、渐变与阴影

基础概念

Canvas 中的文本是以"笔触"的形式绘制的——它成为画布上的像素,而非 DOM 节点。这意味着文本一旦绘制就不能像 HTML 元素那样被选中、搜索或通过 CSS 修改。Canvas 文本常用于标题、图表标签、水印和游戏 HUD 等场景。

文本渲染两种方式

方法说明使用场景
fillText(text, x, y)填充文字实心文字
strokeText(text, x, y)描边文字空心文字、描边效果

语法

基本文本绘制

javascript
const canvas = document.getElementById('textCanvas');
const ctx = canvas.getContext('2d');

// 填充文字
ctx.fillStyle = '#333';
ctx.font = '24px sans-serif';
ctx.fillText('Hello Canvas', 50, 50);

// 描边文字
ctx.strokeStyle = '#4285f4';
ctx.lineWidth = 2;
ctx.strokeText('Hello Canvas', 50, 100);

// 填充 + 描边组合
ctx.fillText('Hello Canvas', 50, 150);
ctx.strokeText('Hello Canvas', 50, 150);

完整示例

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Canvas 文本渲染</title>
  <style>
    canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
  </style>
</head>
<body>
  <h1>Canvas 文本渲染</h1>
  <canvas id="textCanvas" width="500" height="400"></canvas>

  <script>
    const canvas = document.getElementById('textCanvas');
    const ctx = canvas.getContext('2d');

    // 基本填充文字
    ctx.fillStyle = '#333';
    ctx.font = 'bold 28px sans-serif';
    ctx.fillText('Canvas 文本渲染', 20, 40);

    // 描边文字
    ctx.strokeStyle = '#ea4335';
    ctx.lineWidth = 2;
    ctx.font = 'bold 28px sans-serif';
    ctx.strokeText('描边文字效果', 20, 80);

    // 渐变文字
    const gradient = ctx.createLinearGradient(20, 100, 300, 100);
    gradient.addColorStop(0, '#4285f4');
    gradient.addColorStop(0.5, '#ea4335');
    gradient.addColorStop(1, '#fbbc05');
    ctx.fillStyle = gradient;
    ctx.font = 'bold 28px sans-serif';
    ctx.fillText('渐变色文字', 20, 120);

    // 阴影文字
    ctx.save();
    ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';
    ctx.shadowBlur = 6;
    ctx.shadowOffsetX = 3;
    ctx.shadowOffsetY = 3;
    ctx.fillStyle = '#4285f4';
    ctx.font = 'bold 24px sans-serif';
    ctx.fillText('带阴影的文字', 20, 170);
    ctx.restore();

    // 大号装饰文字
    ctx.fillStyle = 'rgba(66, 133, 244, 0.1)';
    ctx.font = 'bold 80px sans-serif';
    ctx.fillText('CANVAS', 20, 280);
  </script>
</body>
</html>

详细说明

font 属性

font 属性使用与 CSS font 简写属性相同的语法:

javascript
// 完整语法:font-style font-variant font-weight font-size/line-height font-family
ctx.font = 'italic small-caps bold 24px/1.5 "Microsoft YaHei", sans-serif';

// 常用写法
ctx.font = '16px sans-serif';          // 最常用
ctx.font = 'bold 20px "Courier New"';  // 等宽字体
ctx.font = 'italic 18px serif';       // 斜体
ctx.font = '300 24px sans-serif';      // 指定字重

// 必须指定 font-size 和 font-family,否则无效
ctx.font = 'bold';              // 无效!缺少 size 和 family
ctx.font = '24px';              // 无效!缺少 family
ctx.font = 'sans-serif';        // 无效!缺少 size
子属性说明可选值
font-style字体样式normalitalicoblique
font-variant字体变体normalsmall-caps
font-weight字重normalbold100~900
font-size字号像素值(px)、em、rem
font-family字体族字体名称或通用族

textAlign 属性

textAlign 控制文本相对于锚点的水平对齐方式:

说明文字分布
start默认,文字从锚点开始锚点在文字左侧(LTR)
end文字结束于锚点锚点在文字右侧(LTR)
left文字左边界对齐锚点锚点在文字左侧
right文字右边界对齐锚点锚点在文字右侧
center文字居中对齐锚点在文字中心
javascript
// 对齐演示
const x = 250; // 画布中心 X

ctx.strokeStyle = '#ea4335';
ctx.beginPath();
ctx.moveTo(x, 10);
ctx.lineTo(x, 250);
ctx.stroke(); // 参考线

const alignments = ['left', 'center', 'right', 'start', 'end'];
const y = 30;

alignments.forEach((align, i) => {
  ctx.textAlign = align;
  ctx.font = '16px sans-serif';
  ctx.fillStyle = '#333';
  ctx.fillText(`textAlign: "${align}"`, x, y + i * 30);
});

textBaseline 属性

textBaseline 控制文本相对于锚点的垂直对齐方式:

说明
alphabetic默认,基于字母基线
top基于文本顶部
hanging基于悬挂基线
middle基于文本中间
ideographic基于表意文字基线
bottom基于文本底部
javascript
const y = 100; // 参考线 Y

ctx.strokeStyle = '#4285f4';
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(500, y);
ctx.stroke();

const baselines = ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom'];

baselines.forEach((baseline, i) => {
  const x = 20 + i * 80;
  ctx.textBaseline = baseline;
  ctx.font = '14px sans-serif';
  ctx.fillStyle = '#333';
  ctx.fillText(baseline, x, y);

  // 标注圆点
  ctx.beginPath();
  ctx.arc(x - 5, y, 3, 0, Math.PI * 2);
  ctx.fillStyle = '#ea4335';
  ctx.fill();
});

textDirection 属性

textDirection 控制文本方向:

javascript
ctx.direction = 'ltr';       // 从左到右(默认)
ctx.direction = 'rtl';       // 从右到左(阿拉伯语、希伯来语)

ctx.fillText('Hello', 250, 50);    // LTR 文字
ctx.fillText('مرحبا', 250, 80);    // RTL 文字

measureText()

measureText() 返回 TextMetrics 对象,包含文本的度量信息:

javascript
const text = 'Canvas 文本渲染';
const metrics = ctx.measureText(text);

console.log(metrics.width);                // 文本宽度(像素)
console.log(metrics.actualBoundingBoxLeft);   // 左边界到锚点的距离
console.log(metrics.actualBoundingBoxRight);  // 右边界到锚点的距离
console.log(metrics.actualBoundingBoxAscent);  // 上边界到锚点的距离
console.log(metrics.actualBoundingBoxDescent); // 下边界到锚点的距离
console.log(metrics.fontBoundingBoxAscent);     // 字体上升量
console.log(metrics.fontBoundingBoxDescent);    // 字体下降量

TextMetrics 属性说明

属性说明
width文本宽度
actualBoundingBoxLeft文本左边缘到锚点的距离(可能为负)
actualBoundingBoxRight文本右边缘到锚点的距离
actualBoundingBoxAscent文本上边缘到锚点的距离(负值方向)
actualBoundingBoxDescent文本下边缘到锚点的距离(正值方向)
fontBoundingBoxAscent字体度量上升量
fontBoundingBoxDescent字体度量下降量
emHeightAscentem 方框上升量
emHeightDescentem 方框下降量

实战示例

示例 1:文本自动换行

javascript
/**
 * 自动换行文本绘制
 * @param {CanvasRenderingContext2D} ctx
 * @param {string} text - 要绘制的文本
 * @param {number} x - 起始 X
 * @param {number} y - 起始 Y
 * @param {number} maxWidth - 最大行宽
 * @param {number} lineHeight - 行高
 */
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
  // 将文本按字符拆分(支持中文)
  const chars = text.split('');
  let line = '';
  let currentY = y;

  for (let i = 0; i < chars.length; i++) {
    const testLine = line + chars[i];
    const metrics = ctx.measureText(testLine);

    if (metrics.width > maxWidth && line.length > 0) {
      ctx.fillText(line, x, currentY);
      line = chars[i];
      currentY += lineHeight;
    } else {
      line = testLine;
    }
  }

  // 绘制最后一行
  ctx.fillText(line, x, currentY);

  return currentY + lineHeight; // 返回结束 Y
}

// 使用
ctx.font = '16px "Microsoft YaHei", sans-serif';
ctx.fillStyle = '#333';
wrapText(
  ctx,
  'Canvas 是 HTML5 提供的强大绘图工具,可以用于绘制图形、文本、图像等。通过 JavaScript API,开发者可以实现丰富的交互式图形应用。',
  20, 40, 300, 24
);

示例 2:文本居中对齐绘制

javascript
/**
 * 在矩形区域内居中绘制文本
 */
function centerText(ctx, text, rect) {
  const { x, y, width, height } = rect;
  const metrics = ctx.measureText(text);

  // 水平居中
  ctx.textAlign = 'center';
  // 垂直居中:使用 middle 基线
  ctx.textBaseline = 'middle';

  ctx.fillText(text, x + width / 2, y + height / 2);

  // 恢复默认
  ctx.textAlign = 'start';
  ctx.textBaseline = 'alphabetic';
}

// 使用
ctx.fillStyle = '#4285f4';
ctx.fillRect(50, 50, 200, 60);
ctx.fillStyle = '#fff';
ctx.font = 'bold 18px sans-serif';
centerText(ctx, '居中文本', { x: 50, y: 50, width: 200, height: 60 });

示例 3:多行文本绘制

javascript
/**
 * 绘制多行文本(按 \n 分割)
 */
function drawMultilineText(ctx, text, x, y, lineHeight) {
  const lines = text.split('\n');

  lines.forEach((line, index) => {
    ctx.fillText(line, x, y + index * lineHeight);
  });

  return y + lines.length * lineHeight;
}

// 使用
ctx.font = '16px sans-serif';
ctx.fillStyle = '#333';
drawMultilineText(
  ctx,
  '第一行文字\n第二行文字\n第三行文字',
  50, 50, 24
);

示例 4:文本效果综合展示

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>文本效果</title>
  <style>
    canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
  </style>
</head>
<body>
  <h1>Canvas 文本效果</h1>
  <canvas id="effectCanvas" width="500" height="400"></canvas>

  <script>
    const canvas = document.getElementById('effectCanvas');
    const ctx = canvas.getContext('2d');

    // === 1. 渐变填充文字 ===
    const grad1 = ctx.createLinearGradient(20, 0, 250, 0);
    grad1.addColorStop(0, '#4285f4');
    grad1.addColorStop(1, '#34a853');
    ctx.fillStyle = grad1;
    ctx.font = 'bold 32px sans-serif';
    ctx.fillText('渐变填充', 20, 40);

    // === 2. 描边文字 ===
    ctx.strokeStyle = '#ea4335';
    ctx.lineWidth = 1.5;
    ctx.font = 'bold 32px sans-serif';
    ctx.strokeText('描边效果', 20, 85);

    // === 3. 阴影文字 ===
    ctx.save();
    ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
    ctx.shadowBlur = 8;
    ctx.shadowOffsetX = 3;
    ctx.shadowOffsetY = 3;
    ctx.fillStyle = '#4285f4';
    ctx.font = 'bold 28px sans-serif';
    ctx.fillText('投影效果', 20, 135);
    ctx.restore();

    // === 4. 描边 + 填充双层文字 ===
    ctx.font = 'bold 28px sans-serif';
    ctx.fillStyle = '#ffffff';
    ctx.fillText('描边填充组合', 20, 185);
    ctx.strokeStyle = '#333';
    ctx.lineWidth = 1;
    ctx.strokeText('描边填充组合', 20, 185);

    // === 5. 发光文字 ===
    ctx.save();
    ctx.shadowColor = '#00e676';
    ctx.shadowBlur = 20;
    ctx.fillStyle = '#00e676';
    ctx.font = 'bold 28px sans-serif';
    ctx.fillText('发光文字', 20, 235);
    ctx.restore();

    // === 6. 图案填充文字 ===
    // 创建条纹图案
    const patternCanvas = document.createElement('canvas');
    patternCanvas.width = 10;
    patternCanvas.height = 10;
    const patternCtx = patternCanvas.getContext('2d');
    patternCtx.fillStyle = '#4285f4';
    patternCtx.fillRect(0, 0, 10, 10);
    patternCtx.fillStyle = '#ea4335';
    patternCtx.fillRect(0, 0, 5, 5);
    patternCtx.fillRect(5, 5, 5, 5);

    const pattern = ctx.createPattern(patternCanvas, 'repeat');
    ctx.fillStyle = pattern;
    ctx.font = 'bold 28px sans-serif';
    ctx.fillText('图案文字', 20, 285);

    // === 7. 裁剪文字(文字内显示图片) ===
    // 先绘制彩色背景
    const bgGrad = ctx.createLinearGradient(20, 300, 350, 360);
    bgGrad.addColorStop(0, '#ea4335');
    bgGrad.addColorStop(0.5, '#fbbc05');
    bgGrad.addColorStop(1, '#34a853');

    ctx.font = 'bold 48px sans-serif';
    ctx.save();
    // 用文字路径作为裁剪区域
    ctx.beginPath();
    // 使用 Path2D 模拟文本路径(简化版)
    const textMetrics = ctx.measureText('CLIP');
    ctx.rect(20, 310, textMetrics.width, 48);
    ctx.clip();
    ctx.fillStyle = bgGrad;
    ctx.fillRect(20, 300, 400, 70);
    ctx.restore();

    // 重新绘制文字轮廓
    ctx.strokeStyle = '#333';
    ctx.lineWidth = 1;
    ctx.strokeText('CLIP', 20, 345);
  </script>
</body>
</html>

注意事项

1. 字体加载时机

Canvas 绘制文字时,字体必须已经加载完毕。如果字体尚未加载完成,Canvas 会使用回退字体。

javascript
// 使用 document.fonts 确保字体已加载
document.fonts.load('bold 24px "MyCustomFont"').then(() => {
  ctx.font = 'bold 24px "MyCustomFont"';
  ctx.fillText('自定义字体文字', 50, 50);
});

// 或使用 FontFace API
const fontFace = new FontFace('MyFont', 'url(my-font.woff2)');
fontFace.load().then((loadedFont) => {
  document.fonts.add(loadedFont);
  ctx.font = '24px "MyFont"';
  ctx.fillText('自定义字体', 50, 50);
});

2. 中文字体渲染

中文文本需要注意字体选择:

javascript
// 推荐:使用系统自带的中文字体
ctx.font = '16px "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", sans-serif';

// 注意:中文字符宽度不统一,不能简单用字符数估算宽度
const text = 'Canvas';
const metrics = ctx.measureText(text);
console.log(metrics.width); // 使用 measureText 获取精确宽度

3. 高 DPI 文本渲染

在高 DPI 屏幕上,文本可能模糊。确保正确处理设备像素比:

javascript
function setupHDCanvas(canvas, w, h) {
  const dpr = window.devicePixelRatio || 1;
  canvas.width = w * dpr;
  canvas.height = h * dpr;
  canvas.style.width = w + 'px';
  canvas.style.height = h + 'px';
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);
  return ctx;
}

最佳实践

1. 文本绘制工具封装

javascript
const TextRenderer = {
  /**
   * 绘制自动换行文本
   */
  wrap(ctx, text, x, y, maxWidth, lineHeight) {
    const paragraphs = text.split('\n');
    let currentY = y;

    paragraphs.forEach(paragraph => {
      if (paragraph === '') {
        currentY += lineHeight;
        return;
      }

      const chars = paragraph.split('');
      let line = '';

      for (const char of chars) {
        const testLine = line + char;
        if (ctx.measureText(testLine).width > maxWidth && line) {
          ctx.fillText(line, x, currentY);
          line = char;
          currentY += lineHeight;
        } else {
          line = testLine;
        }
      }
      ctx.fillText(line, x, currentY);
      currentY += lineHeight;
    });

    return currentY;
  },

  /**
   * 在矩形区域内绘制居中文本
   */
  centered(ctx, text, rect) {
    ctx.save();
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(text, rect.x + rect.width / 2, rect.y + rect.height / 2);
    ctx.restore();
  },

  /**
   * 绘制带背景的文本
   */
  withBackground(ctx, text, x, y, padding, bgColor) {
    const metrics = ctx.measureText(text);
    const bgWidth = metrics.width + padding * 2;
    const bgHeight = parseInt(ctx.font) + padding * 2;

    ctx.fillStyle = bgColor;
    ctx.fillRect(
      x - padding,
      y - parseInt(ctx.font) - padding,
      bgWidth,
      bgHeight
    );

    ctx.fillStyle = '#333';
    ctx.fillText(text, x, y);
  }
};

2. 文本测量与布局

javascript
/**
 * 获取文本包围盒
 */
function getTextBounds(ctx, text, x, y) {
  const metrics = ctx.measureText(text);

  return {
    x: x + metrics.actualBoundingBoxLeft,
    y: y - metrics.actualBoundingBoxAscent,
    width: metrics.actualBoundingBoxLeft + metrics.actualBoundingBoxRight,
    height: metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent
  };
}

3. 性能提示

场景建议
静态文本预渲染到离屏 Canvas,用 drawImage 显示
大量文本考虑使用 DOM 元素覆盖在 Canvas 上
动态文本缓存 TextMetrics 结果,避免每帧测量
复杂字体提前加载,避免渲染时闪烁

下一节

继续学习:图像操作

参考链接