pin_drop当前位置:知识文库 ❯ 图文

多媒体:绘制文本 - 完整教程与代码示例

一、教程简介

Canvas 不仅可以绘制图形,还提供了强大的文本渲染能力。通过 fillTextstrokeText 可以在画布上绘制填充文本和描边文本,配合 fonttextAligntextBaseline 等属性可以精确控制文本的字体、大小、对齐方式和基线位置。本教程将全面讲解 Canvas 文本绘制的各个方面。


二、核心概念

文本绘制方法

方法 说明
fillText(text, x, y, maxWidth) 绘制填充文本
strokeText(text, x, y, maxWidth) 绘制描边文本

文本样式属性

属性 说明 默认值
font 字体样式 10px sans-serif
textAlign 水平对齐 start
textBaseline 基线对齐 alphabetic
direction 文本方向 inherit

文本测量

方法 说明
measureText(text) 返回文本的度量信息

三、语法与用法

fillText - 填充文本

代码示例

ctx.fillText(text, x, y);
ctx.fillText(text, x, y, maxWidth);
参数 说明
text 要绘制的文本
x, y 文本绘制位置的坐标
maxWidth 可选,最大宽度。文本会缩放以适应

strokeText - 描边文本

代码示例

ctx.strokeText(text, x, y);
ctx.strokeText(text, x, y, maxWidth);

参数与 fillText 相同。

font - 字体设置

代码示例

ctx.font = 'bold 24px "Microsoft YaHei", sans-serif';

遵循 CSS font 简写语法:style variant weight size/line-height family

代码示例

// 完整语法
ctx.font = 'italic small-caps bold 24px/1.5 "Microsoft YaHei", sans-serif';

// 常用写法
ctx.font = '24px sans-serif';           // 仅大小和字体
ctx.font = 'bold 24px serif';           // 加粗
ctx.font = 'italic 18px monospace';     // 斜体

textAlign - 水平对齐

代码示例

ctx.textAlign = 'left';     // 左对齐
ctx.textAlign = 'right';    // 右对齐
ctx.textAlign = 'center';   // 居中
ctx.textAlign = 'start';    // 默认,与文本方向一致(通常等同 left)
ctx.textAlign = 'end';      // 与文本方向相反(通常等同 right)

对齐基于 fillText/strokeText 中指定的 x 坐标。

textBaseline - 基线对齐

代码示例

ctx.textBaseline = 'top';          // 文本顶部对齐
ctx.textBaseline = 'hanging';      // 悬挂基线
ctx.textBaseline = 'middle';       // 文本垂直居中
ctx.textBaseline = 'alphabetic';   // 默认,字母基线
ctx.textBaseline = 'ideographic';  // 表意文字基线
ctx.textBaseline = 'bottom';       // 文本底部对齐

基线基于 fillText/strokeText 中指定的 y 坐标。

measureText - 文本测量

代码示例

const metrics = ctx.measureText('Hello World');
console.log(metrics.width);           // 文本宽度
console.log(metrics.actualBoundingBoxAscent);   // 实际上边界
console.log(metrics.actualBoundingBoxDescent);  // 实际下边界

四、代码示例

示例1:文本样式与对齐全面演示

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>文本样式与对齐全面演示</title>
  <style>
    body {
      margin: 0;
      padding: 20px;
      background: #0d1117;
      color: #c9d1d9;
      font-family: 'Segoe UI', sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    canvas {
      border: 1px solid #30363d;
      border-radius: 8px;
      background: #161b22;
    }
  </style>
</head>
<body>
  <h1>文本样式与对齐全面演示</h1>
  <canvas id="myCanvas" width="750" height="600"></canvas>

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

    // === 1. fillText vs strokeText ===
    ctx.fillStyle = '#c9d1d9';
    ctx.font = 'bold 14px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('fillText vs strokeText', 375, 25);

    // 填充文本
    ctx.font = 'bold 36px sans-serif';
    ctx.fillStyle = '#58a6ff';
    ctx.fillText('Fill Text', 150, 75);

    // 描边文本
    ctx.strokeStyle = '#3fb950';
    ctx.lineWidth = 2;
    ctx.strokeText('Stroke Text', 450, 75);

    // 填充+描边
    ctx.fillStyle = '#0f3460';
    ctx.fillText('Both', 375, 120);
    ctx.strokeStyle = '#58a6ff';
    ctx.lineWidth = 1;
    ctx.strokeText('Both', 375, 120);

    // === 2. textAlign 演示 ===
    ctx.fillStyle = '#c9d1d9';
    ctx.font = 'bold 14px sans-serif';
    ctx.fillText('textAlign 水平对齐', 375, 165);

    // 参考线
    ctx.strokeStyle = '#e94560';
    ctx.lineWidth = 1;
    ctx.setLineDash([4, 4]);
    ctx.beginPath();
    ctx.moveTo(375, 180);
    ctx.lineTo(375, 280);
    ctx.stroke();
    ctx.setLineDash([]);

    const aligns = ['left', 'center', 'right', 'start', 'end'];
    aligns.forEach((align, i) => {
      const y = 200 + i * 20;
      ctx.textAlign = align;
      ctx.fillStyle = '#8b949e';
      ctx.font = '14px sans-serif';
      ctx.fillText('textAlign = ' + align, 375, y);
    });
    ctx.textAlign = 'center';

    // === 3. textBaseline 演示 ===
    ctx.fillStyle = '#c9d1d9';
    ctx.font = 'bold 14px sans-serif';
    ctx.fillText('textBaseline 基线对齐', 375, 310);

    // 参考线
    ctx.strokeStyle = '#e94560';
    ctx.lineWidth = 1;
    ctx.setLineDash([4, 4]);
    ctx.beginPath();
    ctx.moveTo(50, 370);
    ctx.lineTo(700, 370);
    ctx.stroke();
    ctx.setLineDash([]);

    const baselines = ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom'];
    baselines.forEach((baseline, i) => {
      const x = 70 + i * 110;
      ctx.textBaseline = baseline;
      ctx.textAlign = 'center';
      ctx.fillStyle = '#58a6ff';
      ctx.font = '16px sans-serif';
      ctx.fillText('Agp', x, 370);

      ctx.fillStyle = '#8b949e';
      ctx.font = '10px sans-serif';
      ctx.textBaseline = 'top';
      ctx.fillText(baseline, x, 385);
    });
    ctx.textBaseline = 'alphabetic';

    // === 4. font 样式演示 ===
    ctx.fillStyle = '#c9d1d9';
    ctx.font = 'bold 14px sans-serif';
    ctx.textBaseline = 'alphabetic';
    ctx.fillText('font 字体样式', 375, 430);

    const fonts = [
      { font: '16px sans-serif', label: '16px sans-serif' },
      { font: 'bold 20px serif', label: 'bold 20px serif' },
      { font: 'italic 18px monospace', label: 'italic 18px monospace' },
      { font: '24px "Courier New"', label: '24px Courier New' },
      { font: 'bold italic 22px Georgia', label: 'bold italic 22px Georgia' }
    ];

    fonts.forEach((item, i) => {
      const y = 455 + i * 28;
      ctx.font = item.font;
      ctx.fillStyle = '#58a6ff';
      ctx.textAlign = 'left';
      ctx.fillText('Hello 你好', 50, y);

      ctx.font = '11px sans-serif';
      ctx.fillStyle = '#8b949e';
      ctx.textAlign = 'right';
      ctx.fillText(item.label, 700, y);
    });
    ctx.textAlign = 'center';

    // === 5. maxWidth 缩放 ===
    ctx.fillStyle = '#c9d1d9';
    ctx.font = 'bold 14px sans-serif';
    ctx.textAlign = 'left';
    ctx.fillText('maxWidth 文本缩放', 50, 455);

    ctx.font = '24px sans-serif';
    ctx.fillStyle = '#f8b500';

    const maxW = [500, 300, 200, 100];
    maxW.forEach((w, i) => {
      const y = 510 + i * 25;
      ctx.fillText('Hello Canvas Text', 50, y, w);

      ctx.font = '10px sans-serif';
      ctx.fillStyle = '#8b949e';
      ctx.fillText('maxWidth=' + w, 560, y);
      ctx.font = '24px sans-serif';
      ctx.fillStyle = '#f8b500';
    });
  </script>
</body>
</html>

示例2:文本特效与实际应用

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>文本特效与实际应用</title>
  <style>
    body {
      margin: 0;
      padding: 20px;
      background: #1a1a2e;
      color: #eee;
      font-family: 'Segoe UI', sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    canvas {
      border: 1px solid #333;
      border-radius: 8px;
      background: #16213e;
    }
  </style>
</head>
<body>
  <h1>文本特效与实际应用</h1>
  <canvas id="myCanvas" width="700" height="500"></canvas>

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

    // === 1. 渐变文字 ===
    const gradText = '渐变文字';
    ctx.font = 'bold 48px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';

    const gradient = ctx.createLinearGradient(100, 40, 300, 40);
    gradient.addColorStop(0, '#e94560');
    gradient.addColorStop(0.5, '#f8b500');
    gradient.addColorStop(1, '#3fb950');
    ctx.fillStyle = gradient;
    ctx.fillText(gradText, 200, 50);

    // === 2. 描边文字 ===
    ctx.font = 'bold 48px sans-serif';
    ctx.strokeStyle = '#58a6ff';
    ctx.lineWidth = 2;
    ctx.strokeText('描边文字', 500, 50);

    // === 3. 阴影文字 ===
    ctx.shadowColor = '#e94560';
    ctx.shadowBlur = 15;
    ctx.shadowOffsetX = 3;
    ctx.shadowOffsetY = 3;
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 40px sans-serif';
    ctx.fillText('阴影文字', 350, 130);
    ctx.shadowColor = 'transparent';
    ctx.shadowBlur = 0;
    ctx.shadowOffsetX = 0;
    ctx.shadowOffsetY = 0;

    // === 4. 立体文字 ===
    ctx.font = 'bold 48px sans-serif';
    ctx.textAlign = 'center';
    const depth = 5;
    for (let i = depth; i > 0; i--) {
      ctx.fillStyle = `rgba(88, 166, 255, ${0.15 * i})`;
      ctx.fillText('立体文字', 350 + i * 2, 220 + i * 2);
    }
    ctx.fillStyle = '#58a6ff';
    ctx.fillText('立体文字', 350, 220);

    // === 5. 文本换行 ===
    ctx.fillStyle = '#c9d1d9';
    ctx.font = 'bold 14px sans-serif';
    ctx.textAlign = 'left';
    ctx.fillText('文本自动换行:', 50, 280);

    function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
      const lines = text.split('\n');
      let currentY = y;

      lines.forEach(line => {
        let words = line.split('');
        let currentLine = '';

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

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

      return currentY;
    }

    ctx.font = '14px sans-serif';
    ctx.fillStyle = '#8b949e';
    wrapText(ctx, 'Canvas 本身不支持文本自动换行,需要通过 measureText 方法手动实现。这段文字演示了如何将长文本按照指定宽度自动换行显示。', 50, 305, 280, 22);

    // === 6. 文本居中绘制 ===
    ctx.fillStyle = '#c9d1d9';
    ctx.font = 'bold 14px sans-serif';
    ctx.fillText('文本居中绘制:', 450, 280);

    // 绘制按钮
    function drawButton(ctx, x, y, w, h, text, bgColor, textColor) {
      // 背景
      ctx.beginPath();
      const r = 6;
      ctx.moveTo(x + r, y);
      ctx.arcTo(x + w, y, x + w, y + h, r);
      ctx.arcTo(x + w, y + h, x, y + h, r);
      ctx.arcTo(x, y + h, x, y, r);
      ctx.arcTo(x, y, x + w, y, r);
      ctx.closePath();
      ctx.fillStyle = bgColor;
      ctx.fill();

      // 文本居中
      ctx.fillStyle = textColor;
      ctx.font = 'bold 14px sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(text, x + w / 2, y + h / 2);
    }

    drawButton(ctx, 400, 300, 120, 40, 'Primary', '#58a6ff', '#fff');
    drawButton(ctx, 540, 300, 120, 40, 'Success', '#3fb950', '#fff');
    drawButton(ctx, 400, 360, 120, 40, 'Warning', '#f8b500', '#000');
    drawButton(ctx, 540, 360, 120, 40, 'Danger', '#e94560', '#fff');

    // === 7. 文本测量 ===
    ctx.textAlign = 'left';
    ctx.textBaseline = 'alphabetic';
    ctx.fillStyle = '#c9d1d9';
    ctx.font = 'bold 14px sans-serif';
    ctx.fillText('measureText 文本测量:', 400, 420);

    ctx.font = '24px sans-serif';
    const testStr = 'Hello Canvas';
    const metrics = ctx.measureText(testStr);

    ctx.fillStyle = '#58a6ff';
    ctx.fillText(testStr, 420, 455);

    // 宽度标注
    ctx.strokeStyle = '#e94560';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(420, 462);
    ctx.lineTo(420 + metrics.width, 462);
    ctx.stroke();

    ctx.fillStyle = '#8b949e';
    ctx.font = '11px sans-serif';
    ctx.fillText('width: ' + metrics.width.toFixed(2) + 'px', 420, 480);
  </script>
</body>
</html>

五、浏览器兼容性

属性/方法 Chrome Firefox Safari Edge IE
fillText 2+ 3.5+ 4+ 12+ 9+
strokeText 2+ 3.5+ 4+ 12+ 9+
font 2+ 3.5+ 4+ 12+ 9+
textAlign 2+ 3.5+ 4+ 12+ 9+
textBaseline 2+ 3.5+ 4+ 12+ 9+
measureText 2+ 3.5+ 4+ 12+ 9+
measureText 完整指标 22+ 31+ 8+ 12+ 不支持

六、注意事项与最佳实践

1. 字体加载

Canvas 使用的字体必须已经加载完成,否则会使用降级字体:

代码示例

// 等待字体加载完成
document.fonts.ready.then(function() {
  ctx.font = '24px "Custom Font"';
  ctx.fillText('Hello', 100, 100);
});

// 或者使用 FontFace API
const font = new FontFace('CustomFont', 'url(font.woff2)');
font.load().then(function(loadedFont) {
  document.fonts.add(loadedFont);
  ctx.font = '24px CustomFont';
  ctx.fillText('Hello', 100, 100);
});

2. 文本不支持换行

Canvas 的 fillText 不会自动换行,需要手动实现:

代码示例

function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
  let line = '';
  for (let i = 0; i < text.length; i++) {
    const testLine = line + text[i];
    if (ctx.measureText(testLine).width > maxWidth) {
      ctx.fillText(line, x, y);
      line = text[i];
      y += lineHeight;
    } else {
      line = testLine;
    }
  }
  ctx.fillText(line, x, y);
}

3. 文本居中的正确方式

代码示例

// 水平居中
ctx.textAlign = 'center';
ctx.fillText(text, centerX, y);

// 垂直居中
ctx.textBaseline = 'middle';
ctx.fillText(text, x, centerY);

// 完全居中
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, centerX, centerY);

4. 文本与矩形对齐

代码示例

// 在矩形中居中文字
function drawTextInRect(ctx, text, rx, ry, rw, rh) {
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText(text, rx + rw / 2, ry + rh / 2);
}

5. 性能考虑

文本绘制相对较慢,对于大量文本或频繁更新,考虑:

  • 离屏缓存:使用离屏 Canvas 缓存静态文本

  • 减少切换:减少字体切换次数

  • 避免频繁测量:避免在动画循环中频繁调用 measureText


七、代码规范示例

代码示例

// 推荐:封装文本绘制函数
function drawText(ctx, text, x, y, options) {
  const {
    font = '14px sans-serif',
    fill = '#000000',
    stroke = null,
    lineWidth = 1,
    align = 'left',
    baseline = 'alphabetic',
    maxWidth = undefined,
    shadow = null
  } = options || {};

  ctx.font = font;
  ctx.textAlign = align;
  ctx.textBaseline = baseline;

  if (shadow) {
    ctx.shadowColor = shadow.color || 'rgba(0,0,0,0.5)';
    ctx.shadowBlur = shadow.blur || 4;
    ctx.shadowOffsetX = shadow.offsetX || 2;
    ctx.shadowOffsetY = shadow.offsetY || 2;
  }

  if (fill) {
    ctx.fillStyle = fill;
    ctx.fillText(text, x, y, maxWidth);
  }

  if (stroke) {
    ctx.strokeStyle = stroke;
    ctx.lineWidth = lineWidth;
    ctx.strokeText(text, x, y, maxWidth);
  }

  // 重置阴影
  if (shadow) {
    ctx.shadowColor = 'transparent';
    ctx.shadowBlur = 0;
    ctx.shadowOffsetX = 0;
    ctx.shadowOffsetY = 0;
  }
}

// 推荐:文本自动换行
function drawWrappedText(ctx, text, x, y, maxWidth, lineHeight, options) {
  const { font = '14px sans-serif', fill = '#000000' } = options || {};
  ctx.font = font;
  ctx.fillStyle = fill;
  ctx.textAlign = 'left';
  ctx.textBaseline = 'top';

  let currentY = y;
  let line = '';

  for (let i = 0; i < text.length; i++) {
    const ch = text[i];
    if (ch === '\n') {
      ctx.fillText(line, x, currentY);
      line = '';
      currentY += lineHeight;
      continue;
    }
    const testLine = line + ch;
    if (ctx.measureText(testLine).width > maxWidth && line.length > 0) {
      ctx.fillText(line, x, currentY);
      line = ch;
      currentY += lineHeight;
    } else {
      line = testLine;
    }
  }
  if (line) ctx.fillText(line, x, currentY);
  return currentY + lineHeight;
}

// 使用示例
drawText(ctx, 'Hello World', 100, 100, {
  font: 'bold 24px sans-serif',
  fill: '#58a6ff',
  align: 'center',
  baseline: 'middle',
  shadow: { color: 'rgba(0,0,0,0.3)', blur: 8, offsetX: 2, offsetY: 2 }
});

drawWrappedText(ctx, '这是一段很长的文本,需要自动换行显示。Canvas 本身不支持自动换行。', 50, 200, 300, 22, {
  font: '14px sans-serif',
  fill: '#c9d1d9'
});

八、常见问题与解决方案

常见问题

Canvas 上的中文显示异常怎么办?

原因是字体不支持中文或字体未加载完成。解决方案是使用支持中文的字体(如 "Microsoft YaHei"、"PingFang SC"),并通过 document.fonts.ready 等待字体加载完成后再绘制文本。

如何获取文本的精确高度?

使用 measureText 方法获取 actualBoundingBoxAscentactualBoundingBoxDescent,两者之和即为文本精确高度。如果浏览器不支持这些属性,可降级使用 fontBoundingBoxAscent + fontBoundingBoxDescent

文本模糊怎么办?

原因是高 DPI 屏幕未处理设备像素比。解决方案是通过 window.devicePixelRatio 获取设备像素比,将 Canvas 的宽高设置为显示尺寸乘以 DPR,再通过 ctx.scale(dpr, dpr) 缩放上下文。

如何实现文本垂直排列?

方案一:逐字绘制,通过循环遍历每个字符,依次在不同 y 坐标上绘制。方案二:使用 Canvas 变换,通过 ctx.rotate(-Math.PI / 2) 旋转画布后绘制文本。

maxWidth 参数导致文字变形怎么办?

maxWidth 会等比缩放文字导致变形。替代方案:方案一是截断文本并在末尾添加省略号;方案二是逐步缩小字体大小直到文本宽度满足要求。


九、总结

Canvas 文本绘制是界面开发的重要技能,关键要点:

  • fillText/strokeText:分别绘制填充文本和描边文本,可组合使用

  • font:使用 CSS font 语法设置字体,注意中文字体兼容性

  • textAlign:控制水平对齐,基于 x 坐标

  • textBaseline:控制基线对齐,基于 y 坐标

  • measureText:测量文本宽度,用于换行和布局计算

  • maxWidth:限制文本宽度,文本会等比缩放

  • 文本换行:Canvas 不支持自动换行,需手动实现

  • 字体加载:确保字体加载完成后再绘制文本

小贴士

下一教程将介绍 Canvas 渐变的使用。渐变可以为文本和图形添加丰富的色彩效果,包括线性渐变(createLinearGradient)和径向渐变(createRadialGradient),是提升 Canvas 视觉效果的重要手段。

标签: Canvas文本 fillText strokeText 文本对齐 字体设置 文本测量 文本换行 文本特效

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 多媒体:贝塞尔曲线 - 完整教程与代码示例 下一篇: 多媒体:渐变 - 完整教程与代码示例

poll相关推荐