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

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

一、教程简介

Canvas 不仅可以绘制图形,还提供了强大的文本渲染能力。通过 fillText 和 strokeText 可以在画布上绘制填充文本和描边文本,配合 font、textAlign、textBaseline 等属性可以精确控制文本的字体、大小、对齐方式和基线位置。本教程将全面讲解 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 方法获取 actualBoundingBoxAscent 和 actualBoundingBoxDescent,两者之和即为文本精确高度。如果浏览器不支持这些属性,可降级使用 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相关推荐