文本渲染
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 | 字体样式 | normal、italic、oblique |
font-variant | 字体变体 | normal、small-caps |
font-weight | 字重 | normal、bold、100~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 | 字体度量下降量 |
emHeightAscent | em 方框上升量 |
emHeightDescent | em 方框下降量 |
实战示例
示例 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 结果,避免每帧测量 |
| 复杂字体 | 提前加载,避免渲染时闪烁 |
下一节
继续学习:图像操作