第8章:文本渲染
8.1 章节概述
在前面的章节中,我们学习了如何绑制图形、处理图像、进行合成。但在实际应用中,文本是不可或缺的元素——标签、标题、说明文字、数据展示等都需要文本渲染。
Canvas 提供了完整的文本渲染能力,虽然不如 HTML 文本那样灵活(没有自动换行、没有 CSS 样式),但足够满足大多数图形应用的需求。
本章将深入讲解:
- 文本绘制基础:fillText 和 strokeText
- 字体设置:font 属性的完整语法
- 文本对齐:水平和垂直对齐方式
- 文本测量:获取文本尺寸
- 高级技巧:多行文本、文本效果、文本路径
- 实战应用:标签、数据可视化、艺术字
8.2 文本绘制基础
8.2.1 fillText - 填充文本
fillText() 使用当前填充样式绘制实心文本:
javascript
ctx.fillText(text, x, y);
ctx.fillText(text, x, y, maxWidth); // 可选:限制最大宽度| 参数 | 说明 |
|---|---|
| text | 要绘制的文本字符串 |
| x | 文本起始位置的 X 坐标 |
| y | 文本基线位置的 Y 坐标 |
| maxWidth | 可选,文本最大宽度(超出会被压缩) |
javascript
// 基础文本绘制
ctx.fillStyle = '#333';
ctx.font = '24px Arial';
ctx.fillText('Hello, Canvas!', 50, 50);
// 带最大宽度限制
ctx.fillText('This is a very long text that will be compressed', 50, 100, 200);8.2.2 strokeText - 描边文本
strokeText() 使用当前描边样式绘制空心文本:
javascript
ctx.strokeText(text, x, y);
ctx.strokeText(text, x, y, maxWidth);javascript
// 描边文本
ctx.strokeStyle = '#FF6B6B';
ctx.lineWidth = 2;
ctx.font = '48px Arial';
ctx.strokeText('Outline Text', 50, 100);
// 同时填充和描边(先填充后描边效果更好)
ctx.font = '48px Arial';
ctx.fillStyle = '#4D7CFF';
ctx.fillText('Styled Text', 50, 180);
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.strokeText('Styled Text', 50, 180);8.2.3 文本绘制位置的理解
重要:fillText(text, x, y) 中的 y 是**基线(baseline)**位置,不是文本顶部!
文本 "Hello"
← 顶线 (top)
H ll ← 上升部分
H e ll o ← 基线 (baseline) ← y 参数指定这里
H l ← 下降部分 (如 g, y, p)
← 底线 (bottom)javascript
// 可视化基线位置
const y = 100;
// 绘制参考线
ctx.strokeStyle = 'red';
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
// 绘制文本
ctx.fillStyle = '#333';
ctx.font = '36px Arial';
ctx.fillText('Hello Baseline', 50, y);
// 文本的底部会在红线上方,部分字母(如 y, g)可能在线下方8.3 字体设置
8.3.1 font 属性
ctx.font 使用 CSS font 语法设置字体:
javascript
ctx.font = '[font-style] [font-variant] [font-weight] font-size[/line-height] font-family';| 部分 | 可选值 | 必需 |
|---|---|---|
| font-style | normal, italic, oblique | 可选 |
| font-variant | normal, small-caps | 可选 |
| font-weight | normal, bold, 100-900 | 可选 |
| font-size | 12px, 1em, 100% 等 | 必需 |
| line-height | 1.5, 20px 等 | 可选 |
| font-family | Arial, "Times New Roman" 等 | 必需 |
8.3.2 常用字体设置示例
javascript
// 基础
ctx.font = '16px Arial';
// 粗体
ctx.font = 'bold 16px Arial';
// 斜体
ctx.font = 'italic 16px Arial';
// 粗斜体
ctx.font = 'italic bold 16px Arial';
// 特定字重
ctx.font = '300 16px Arial'; // 细体
ctx.font = '700 16px Arial'; // 粗体
// 多字体族(带回退)
ctx.font = '16px "Helvetica Neue", Helvetica, Arial, sans-serif';
// 小型大写字母
ctx.font = 'small-caps 16px Arial';8.3.3 字体单位
javascript
// 像素(最常用)
ctx.font = '24px Arial';
// em(相对于父元素)
ctx.font = '1.5em Arial';
// rem(相对于根元素)
ctx.font = '1.5rem Arial';
// 百分比
ctx.font = '150% Arial';
// pt(印刷点)
ctx.font = '12pt Arial';8.3.4 使用 Web 字体
javascript
// 确保字体已加载
async function useWebFont() {
// 使用 FontFace API 加载字体
const font = new FontFace('CustomFont', 'url(/fonts/custom.woff2)');
await font.load();
document.fonts.add(font);
// 现在可以使用
ctx.font = '24px CustomFont';
ctx.fillText('Custom Font Text', 50, 50);
}
// 或等待所有字体加载
document.fonts.ready.then(() => {
ctx.font = '24px CustomFont';
ctx.fillText('Text with custom font', 50, 50);
});8.3.5 动态字体大小
javascript
/**
* 根据容器宽度自动调整字体大小
*/
function fitTextToWidth(ctx, text, maxWidth, fontFamily = 'Arial') {
let fontSize = 100; // 从大字号开始
do {
ctx.font = `${fontSize}px ${fontFamily}`;
const metrics = ctx.measureText(text);
if (metrics.width <= maxWidth) {
break;
}
fontSize--;
} while (fontSize > 1);
return fontSize;
}
// 使用
const text = 'This text will fit';
const fontSize = fitTextToWidth(ctx, text, 300);
ctx.font = `${fontSize}px Arial`;
ctx.fillText(text, 50, 100);8.4 文本对齐
8.4.1 水平对齐 - textAlign
textAlign 控制文本相对于 x 坐标的水平对齐方式:
javascript
ctx.textAlign = 'start' | 'end' | 'left' | 'right' | 'center';| 值 | 说明 |
|---|---|
start | 文本起点在 x 位置(默认,与文字方向有关) |
end | 文本终点在 x 位置 |
left | 文本左边缘在 x 位置 |
right | 文本右边缘在 x 位置 |
center | 文本中心在 x 位置 |
textAlign 效果图示 (x = 中间虚线位置):
│
left │Hello World
│
│
center │ Hello World
│ (文本中心对齐)
│
│
right │ Hello World
│javascript
const x = canvas.width / 2;
// 绘制参考线
ctx.strokeStyle = '#ccc';
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
ctx.font = '24px Arial';
ctx.fillStyle = '#333';
// 左对齐
ctx.textAlign = 'left';
ctx.fillText('Left aligned', x, 50);
// 居中对齐
ctx.textAlign = 'center';
ctx.fillText('Center aligned', x, 100);
// 右对齐
ctx.textAlign = 'right';
ctx.fillText('Right aligned', x, 150);8.4.2 垂直对齐 - textBaseline
textBaseline 控制文本相对于 y 坐标的垂直对齐方式:
javascript
ctx.textBaseline = 'alphabetic' | 'top' | 'hanging' | 'middle' | 'ideographic' | 'bottom';| 值 | 说明 |
|---|---|
alphabetic | 字母基线(默认) |
top | 文本顶部 |
hanging | 悬挂基线(用于某些文字系统) |
middle | 文本中间 |
ideographic | 表意文字基线 |
bottom | 文本底部 |
textBaseline 效果图示 (y = 水平线位置):
─────────────────── top
H e l l o
─────────────────── hanging
H e l l o
─────────────────── middle
H e l l o
─────────────────── alphabetic (默认)
H e l l o
y p g (下降部分)
─────────────────── bottomjavascript
const y = canvas.height / 2;
// 绘制参考线
ctx.strokeStyle = '#ccc';
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
ctx.font = '24px Arial';
ctx.fillStyle = '#333';
const baselines = ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom'];
baselines.forEach((baseline, index) => {
ctx.textBaseline = baseline;
ctx.fillText(baseline, 50 + index * 120, y);
});8.4.3 实现完美居中
javascript
/**
* 在矩形区域内居中绘制文本
*/
function drawCenteredText(ctx, text, x, y, width, height) {
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const centerX = x + width / 2;
const centerY = y + height / 2;
ctx.fillText(text, centerX, centerY);
ctx.restore();
}
// 在 Canvas 正中央绘制文本
drawCenteredText(ctx, 'Centered!', 0, 0, canvas.width, canvas.height);8.5 文本测量
8.5.1 measureText 方法
measureText() 返回文本的测量信息:
javascript
const metrics = ctx.measureText(text);8.5.2 TextMetrics 对象
javascript
const metrics = ctx.measureText('Hello World');
// 常用属性
console.log(metrics.width); // 文本宽度
console.log(metrics.actualBoundingBoxLeft); // 从基线到左边界的距离
console.log(metrics.actualBoundingBoxRight); // 从基线到右边界的距离
console.log(metrics.actualBoundingBoxAscent); // 从基线到顶部的距离
console.log(metrics.actualBoundingBoxDescent); // 从基线到底部的距离
// 计算实际高度
const actualHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;TextMetrics 属性图示:
actualBoundingBoxAscent
↑
┌─────────────────────────┴─────────────────────────┐
│ Hello World │
├─────────────────────────┬─────────────────────────┤
│ ← baseline (y 位置)
└─────────────────────────┬─────────────────────────┘
↓
actualBoundingBoxDescent
←─────── width ───────→8.5.3 获取文本边界框
javascript
/**
* 获取文本的完整边界框
*/
function getTextBoundingBox(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
};
}
// 使用:绘制文本及其边界框
ctx.font = '36px Arial';
const text = 'Hello World';
const x = 50, y = 100;
// 获取边界框
const bbox = getTextBoundingBox(ctx, text, x, y);
// 绘制背景框
ctx.fillStyle = '#eee';
ctx.fillRect(bbox.x, bbox.y, bbox.width, bbox.height);
// 绘制文本
ctx.fillStyle = '#333';
ctx.fillText(text, x, y);8.5.4 文本是否超出容器
javascript
/**
* 检查文本是否会超出指定宽度
*/
function isTextOverflow(ctx, text, maxWidth) {
const metrics = ctx.measureText(text);
return metrics.width > maxWidth;
}
/**
* 截断文本并添加省略号
*/
function truncateText(ctx, text, maxWidth, ellipsis = '...') {
if (!isTextOverflow(ctx, text, maxWidth)) {
return text;
}
const ellipsisWidth = ctx.measureText(ellipsis).width;
const availableWidth = maxWidth - ellipsisWidth;
let truncated = '';
for (let i = 0; i < text.length; i++) {
const testText = text.substring(0, i + 1);
if (ctx.measureText(testText).width > availableWidth) {
break;
}
truncated = testText;
}
return truncated + ellipsis;
}
// 使用
ctx.font = '16px Arial';
const longText = 'This is a very long text that needs to be truncated';
const truncated = truncateText(ctx, longText, 200);
ctx.fillText(truncated, 50, 50); // "This is a very long..."8.6 多行文本
Canvas 的 fillText() 不支持自动换行,需要手动处理。
8.6.1 手动换行
javascript
/**
* 绘制多行文本
*/
function drawMultilineText(ctx, text, x, y, lineHeight) {
const lines = text.split('\n');
lines.forEach((line, index) => {
ctx.fillText(line, x, y + index * lineHeight);
});
}
// 使用
ctx.font = '16px Arial';
const text = `第一行文本
第二行文本
第三行文本`;
drawMultilineText(ctx, text, 50, 50, 24);8.6.2 自动换行
javascript
/**
* 自动换行绘制文本
* @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 words = text.split(' ');
let line = '';
let currentY = y;
const lines = [];
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i] + ' ';
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && i > 0) {
lines.push({ text: line.trim(), y: currentY });
line = words[i] + ' ';
currentY += lineHeight;
} else {
line = testLine;
}
}
lines.push({ text: line.trim(), y: currentY });
// 绘制
lines.forEach(lineObj => {
ctx.fillText(lineObj.text, x, lineObj.y);
});
return lines;
}
// 使用
ctx.font = '16px Arial';
const paragraph = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
wrapText(ctx, paragraph, 50, 50, 300, 24);8.6.3 支持中文的自动换行
javascript
/**
* 支持中文的自动换行
* 中文每个字符都可以作为换行点
*/
function wrapTextChinese(ctx, text, x, y, maxWidth, lineHeight) {
const lines = [];
let currentLine = '';
let currentY = y;
for (let i = 0; i < text.length; i++) {
const char = text[i];
// 遇到换行符
if (char === '\n') {
lines.push({ text: currentLine, y: currentY });
currentLine = '';
currentY += lineHeight;
continue;
}
const testLine = currentLine + char;
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && currentLine.length > 0) {
lines.push({ text: currentLine, y: currentY });
currentLine = char;
currentY += lineHeight;
} else {
currentLine = testLine;
}
}
if (currentLine) {
lines.push({ text: currentLine, y: currentY });
}
// 绘制
lines.forEach(lineObj => {
ctx.fillText(lineObj.text, x, lineObj.y);
});
return lines;
}
// 使用
ctx.font = '16px "Microsoft YaHei", sans-serif';
const chineseText = '这是一段很长的中文文本,需要自动换行显示。Canvas 本身不支持自动换行,所以我们需要自己实现这个功能。';
wrapTextChinese(ctx, chineseText, 50, 50, 300, 28);8.6.4 文本框组件
javascript
/**
* 文本框组件 - 支持背景、内边距、对齐
*/
class TextBox {
constructor(ctx, options = {}) {
this.ctx = ctx;
this.x = options.x || 0;
this.y = options.y || 0;
this.width = options.width || 200;
this.maxHeight = options.maxHeight || Infinity;
this.padding = options.padding || 10;
this.lineHeight = options.lineHeight || 1.4;
this.font = options.font || '16px Arial';
this.textColor = options.textColor || '#333';
this.backgroundColor = options.backgroundColor || null;
this.borderColor = options.borderColor || null;
this.borderWidth = options.borderWidth || 1;
this.textAlign = options.textAlign || 'left';
}
draw(text) {
const { ctx, x, y, width, padding, font, textColor } = this;
ctx.save();
ctx.font = font;
// 计算行高(像素)
const fontSize = parseInt(font);
const lineHeightPx = fontSize * this.lineHeight;
// 换行处理
const maxTextWidth = width - padding * 2;
const lines = this.wrapText(text, maxTextWidth);
// 计算总高度
const textHeight = lines.length * lineHeightPx;
const totalHeight = Math.min(textHeight + padding * 2, this.maxHeight);
// 绘制背景
if (this.backgroundColor) {
ctx.fillStyle = this.backgroundColor;
ctx.fillRect(x, y, width, totalHeight);
}
// 绘制边框
if (this.borderColor) {
ctx.strokeStyle = this.borderColor;
ctx.lineWidth = this.borderWidth;
ctx.strokeRect(x, y, width, totalHeight);
}
// 绘制文本
ctx.fillStyle = textColor;
ctx.textBaseline = 'top';
ctx.textAlign = this.textAlign;
const textX = this.textAlign === 'center' ? x + width / 2 :
this.textAlign === 'right' ? x + width - padding :
x + padding;
lines.forEach((line, index) => {
const lineY = y + padding + index * lineHeightPx;
if (lineY + lineHeightPx <= y + totalHeight) {
ctx.fillText(line, textX, lineY);
}
});
ctx.restore();
return { width, height: totalHeight };
}
wrapText(text, maxWidth) {
const lines = [];
let currentLine = '';
for (const char of text) {
if (char === '\n') {
lines.push(currentLine);
currentLine = '';
continue;
}
const testLine = currentLine + char;
if (this.ctx.measureText(testLine).width > maxWidth && currentLine) {
lines.push(currentLine);
currentLine = char;
} else {
currentLine = testLine;
}
}
if (currentLine) {
lines.push(currentLine);
}
return lines;
}
}
// 使用
const textBox = new TextBox(ctx, {
x: 50,
y: 50,
width: 300,
padding: 15,
font: '16px Arial',
backgroundColor: '#f5f5f5',
borderColor: '#ddd',
textAlign: 'left'
});
textBox.draw('这是一个文本框组件示例,支持自动换行、背景色、边框和内边距。');8.7 文本效果
8.7.1 文本阴影
javascript
ctx.font = '48px Arial';
ctx.fillStyle = '#333';
// 设置阴影
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.fillText('Shadow Text', 50, 100);
// 重置阴影
ctx.shadowColor = 'transparent';8.7.2 渐变文本
javascript
ctx.font = 'bold 48px Arial';
// 创建线性渐变
const gradient = ctx.createLinearGradient(50, 50, 50, 100);
gradient.addColorStop(0, '#FF6B6B');
gradient.addColorStop(0.5, '#4D7CFF');
gradient.addColorStop(1, '#51CF66');
ctx.fillStyle = gradient;
ctx.fillText('Gradient Text', 50, 100);8.7.3 描边 + 填充
javascript
ctx.font = 'bold 72px Arial';
// 先填充
ctx.fillStyle = '#4D7CFF';
ctx.fillText('OUTLINED', 50, 100);
// 再描边
ctx.strokeStyle = '#fff';
ctx.lineWidth = 3;
ctx.strokeText('OUTLINED', 50, 100);8.7.4 3D 文字效果
javascript
/**
* 绘制 3D 文字
*/
function draw3DText(ctx, text, x, y, depth, color, shadowColor) {
ctx.font = 'bold 72px Arial';
// 绘制阴影层(从后往前)
for (let i = depth; i > 0; i--) {
ctx.fillStyle = shadowColor;
ctx.fillText(text, x + i, y + i);
}
// 绘制主文字
ctx.fillStyle = color;
ctx.fillText(text, x, y);
}
draw3DText(ctx, '3D TEXT', 50, 150, 5, '#4D7CFF', '#2a5298');8.7.5 霓虹灯效果
javascript
/**
* 霓虹灯文字效果
*/
function drawNeonText(ctx, text, x, y, color) {
ctx.font = 'bold 48px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 多层发光
const glowLayers = [
{ blur: 20, alpha: 0.3 },
{ blur: 10, alpha: 0.5 },
{ blur: 5, alpha: 0.7 },
{ blur: 2, alpha: 0.9 }
];
glowLayers.forEach(layer => {
ctx.save();
ctx.shadowColor = color;
ctx.shadowBlur = layer.blur;
ctx.globalAlpha = layer.alpha;
ctx.fillStyle = color;
ctx.fillText(text, x, y);
ctx.restore();
});
// 中心亮色
ctx.fillStyle = '#fff';
ctx.fillText(text, x, y);
}
// 黑色背景
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawNeonText(ctx, 'NEON', canvas.width / 2, 100, '#FF6B6B');
drawNeonText(ctx, 'GLOW', canvas.width / 2, 180, '#4D7CFF');8.7.6 打字机效果
javascript
/**
* 打字机效果
*/
class TypewriterEffect {
constructor(ctx, text, x, y, options = {}) {
this.ctx = ctx;
this.text = text;
this.x = x;
this.y = y;
this.font = options.font || '24px monospace';
this.color = options.color || '#333';
this.speed = options.speed || 50; // 毫秒/字符
this.currentIndex = 0;
this.isComplete = false;
}
start() {
this.interval = setInterval(() => {
this.currentIndex++;
this.draw();
if (this.currentIndex >= this.text.length) {
clearInterval(this.interval);
this.isComplete = true;
}
}, this.speed);
}
draw() {
const displayText = this.text.substring(0, this.currentIndex);
// 清除区域
this.ctx.clearRect(this.x - 5, this.y - 30, 500, 40);
this.ctx.font = this.font;
this.ctx.fillStyle = this.color;
this.ctx.fillText(displayText, this.x, this.y);
// 绘制光标
if (!this.isComplete) {
const cursorX = this.x + this.ctx.measureText(displayText).width;
this.ctx.fillRect(cursorX + 2, this.y - 20, 2, 25);
}
}
}
// 使用
const typewriter = new TypewriterEffect(ctx, 'Hello, World!', 50, 100, {
font: '24px monospace',
speed: 100
});
typewriter.start();8.8 实战应用
8.8.1 数据标签
javascript
/**
* 在图表上绘制数据标签
*/
function drawDataLabel(ctx, value, x, y, options = {}) {
const {
font = '12px Arial',
textColor = '#333',
backgroundColor = '#fff',
padding = 4,
borderRadius = 3
} = options;
ctx.font = font;
const text = typeof value === 'number' ? value.toFixed(1) : value;
const metrics = ctx.measureText(text);
const width = metrics.width + padding * 2;
const height = parseInt(font) + padding * 2;
// 绘制背景
ctx.fillStyle = backgroundColor;
roundRect(ctx, x - width / 2, y - height / 2, width, height, borderRadius);
ctx.fill();
// 绘制文本
ctx.fillStyle = textColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, x, y);
}
// 辅助函数:圆角矩形
function roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}8.8.2 工具提示
javascript
/**
* 工具提示组件
*/
class Tooltip {
constructor(ctx) {
this.ctx = ctx;
this.visible = false;
this.x = 0;
this.y = 0;
this.text = '';
}
show(x, y, text) {
this.x = x;
this.y = y;
this.text = text;
this.visible = true;
}
hide() {
this.visible = false;
}
draw() {
if (!this.visible) return;
const { ctx, x, y, text } = this;
ctx.save();
ctx.font = '14px Arial';
const padding = 8;
const metrics = ctx.measureText(text);
const width = metrics.width + padding * 2;
const height = 14 + padding * 2;
// 确定位置(避免超出边界)
let tooltipX = x + 10;
let tooltipY = y - height - 5;
if (tooltipX + width > ctx.canvas.width) {
tooltipX = x - width - 10;
}
if (tooltipY < 0) {
tooltipY = y + 20;
}
// 阴影
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
ctx.shadowBlur = 4;
ctx.shadowOffsetY = 2;
// 背景
ctx.fillStyle = '#333';
roundRect(ctx, tooltipX, tooltipY, width, height, 4);
ctx.fill();
// 文本
ctx.shadowColor = 'transparent';
ctx.fillStyle = '#fff';
ctx.textBaseline = 'middle';
ctx.fillText(text, tooltipX + padding, tooltipY + height / 2);
ctx.restore();
}
}8.8.3 文字云
javascript
/**
* 简单的文字云
*/
function drawWordCloud(ctx, words, centerX, centerY, maxRadius) {
// words: [{ text: 'word', weight: 10 }, ...]
// 按权重排序
const sorted = [...words].sort((a, b) => b.weight - a.weight);
const maxWeight = sorted[0].weight;
// 螺旋放置
let angle = 0;
let radius = 0;
const placed = [];
sorted.forEach(word => {
// 根据权重计算字号
const fontSize = Math.max(12, (word.weight / maxWeight) * 48);
ctx.font = `${fontSize}px Arial`;
const metrics = ctx.measureText(word.text);
const wordWidth = metrics.width;
const wordHeight = fontSize;
// 寻找不重叠的位置
let attempts = 0;
let x, y;
while (attempts < 500) {
x = centerX + radius * Math.cos(angle);
y = centerY + radius * Math.sin(angle);
// 检查是否重叠
const overlaps = placed.some(p =>
Math.abs(x - p.x) < (wordWidth + p.width) / 2 &&
Math.abs(y - p.y) < (wordHeight + p.height) / 2
);
if (!overlaps &&
x - wordWidth / 2 > 0 &&
x + wordWidth / 2 < ctx.canvas.width &&
y - wordHeight / 2 > 0 &&
y + wordHeight / 2 < ctx.canvas.height) {
break;
}
angle += 0.5;
radius += 0.5;
attempts++;
}
// 绘制
ctx.fillStyle = `hsl(${Math.random() * 360}, 70%, 50%)`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(word.text, x, y);
placed.push({ x, y, width: wordWidth, height: wordHeight });
});
}
// 使用
const words = [
{ text: 'Canvas', weight: 100 },
{ text: 'JavaScript', weight: 80 },
{ text: 'HTML5', weight: 70 },
{ text: '图形', weight: 60 },
{ text: '动画', weight: 50 },
// ... 更多词
];
drawWordCloud(ctx, words, canvas.width / 2, canvas.height / 2, 200);8.9 本章小结
本章详细介绍了 Canvas 的文本渲染系统:
核心方法
| 方法 | 作用 |
|---|---|
fillText(text, x, y) | 填充文本 |
strokeText(text, x, y) | 描边文本 |
measureText(text) | 测量文本尺寸 |
重要属性
| 属性 | 作用 |
|---|---|
font | 字体样式(CSS 语法) |
textAlign | 水平对齐 |
textBaseline | 垂直对齐 |
关键技巧
- 基线理解:y 坐标指向基线
- 完美居中:
textAlign='center'+textBaseline='middle' - 自动换行:需要手动实现
- 文本测量:
measureText()获取尺寸
8.10 练习题
基础练习
实现居中显示的标题文本
创建带背景色和圆角的标签组件
实现支持换行的段落文本显示
进阶练习
实现打字机动画效果
创建带阴影和渐变的艺术字
挑战练习
- 构建一个完整的文本编辑器组件,支持:
- 多行文本输入
- 光标显示
- 文本选择高亮
下一章预告:在第9章中,我们将学习动画与帧循环——如何创建流畅的 Canvas 动画。
文档版本:v1.0
字数统计:约 13,000 字
代码示例:35+ 个
