1. 文本绘制基础
1.1 基本文本方法
Canvas提供了两个主要的文本绘制方法:
// 获取canvas和上下文
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 1. fillText() - 填充文本
ctx.font = '24px Arial';
ctx.fillStyle = 'black';
ctx.fillText('Hello Canvas!', 50, 50);
// 2. strokeText() - 描边文本
ctx.font = '24px Arial';
ctx.strokeStyle = 'red';
ctx.lineWidth = 1;
ctx.strokeText('Stroke Text', 50, 100);
// 3. 同时使用填充和描边
ctx.fillStyle = 'blue';
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.fillText('Combined Text', 50, 150);
ctx.strokeText('Combined Text', 50, 150);
1.2 文本属性设置
// 字体设置
ctx.font = 'italic bold 32px serif';
// 格式:[style] [variant] [weight] size family
// style: normal, italic, oblique
// variant: normal, small-caps
// weight: normal, bold, bolder, lighter, 100-900
// 文本对齐
ctx.textAlign = 'center'; // left, right, center, start, end
ctx.textBaseline = 'middle'; // top, hanging, middle, alphabetic, ideographic, bottom
// 文本方向
ctx.direction = 'ltr'; // ltr (left-to-right), rtl (right-to-left), inherit
1.3 文本对齐示例
// 演示不同的文本对齐方式
function demonstrateTextAlignment() {
const centerX = 400;
const centerY = 250;
// 绘制参考线
ctx.strokeStyle = 'lightgray';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(centerX, centerY - 100);
ctx.lineTo(centerX, centerY + 100);
ctx.moveTo(centerX - 200, centerY);
ctx.lineTo(centerX + 200, centerY);
ctx.stroke();
// 不同的textAlign设置
const alignments = ['left', 'center', 'right'];
const baselines = ['top', 'middle', 'bottom'];
ctx.font = '16px Arial';
alignments.forEach((align, i) => {
baselines.forEach((baseline, j) => {
const x = centerX + (i - 1) * 150;
const y = centerY + (j - 1) * 60;
ctx.textAlign = align;
ctx.textBaseline = baseline;
ctx.fillStyle = `hsl(${(i * 3 + j) * 40}, 70%, 50%)`;
ctx.fillText(`${align}-${baseline}`, x, y);
// 绘制参考点
ctx.fillStyle = 'red';
ctx.fillRect(x - 2, y - 2, 4, 4);
});
});
}
demonstrate TextAlignment();
2. 字体管理
2.1 Web字体加载
// 检查字体是否可用
function isFontAvailable(fontName) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 使用默认字体测量文本
ctx.font = '12px monospace';
const defaultWidth = ctx.measureText('test').width;
// 使用目标字体测量文本
ctx.font = `12px ${fontName}, monospace`;
const testWidth = ctx.measureText('test').width;
return defaultWidth !== testWidth;
}
// 加载Web字体
function loadWebFont(fontFamily, fontUrl) {
return new Promise((resolve, reject) => {
const font = new FontFace(fontFamily, `url(${fontUrl})`);
font.load().then((loadedFont) => {
document.fonts.add(loadedFont);
resolve(loadedFont);
}).catch(reject);
});
}
// 使用示例
/*
loadWebFont('CustomFont', 'fonts/custom-font.woff2')
.then(() => {
ctx.font = '24px CustomFont';
ctx.fillText('Custom Font Text', 50, 300);
})
.catch(error => {
console.error('字体加载失败:', error);
});
*/
2.2 字体回退机制
// 字体回退示例
function demonstrateFontFallback() {
const fontStacks = [
'24px "Helvetica Neue", Helvetica, Arial, sans-serif',
'24px "Times New Roman", Times, serif',
'24px "Courier New", Courier, monospace',
'24px "Comic Sans MS", cursive',
'24px Impact, fantasy'
];
fontStacks.forEach((font, index) => {
ctx.font = font;
ctx.fillStyle = `hsl(${index * 60}, 70%, 50%)`;
ctx.fillText(`Font Stack ${index + 1}`, 50, 350 + index * 40);
});
}
demonstrateFontFallback();
3. 文本测量
3.1 基础文本测量
// 测量文本尺寸
function measureTextDemo() {
const text = 'Measure This Text';
ctx.font = '32px Arial';
// 获取文本度量信息
const metrics = ctx.measureText(text);
console.log('文本宽度:', metrics.width);
console.log('实际边界框宽度:', metrics.actualBoundingBoxRight + metrics.actualBoundingBoxLeft);
console.log('字体边界框高度:', metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent);
// 绘制文本
const x = 50;
const y = 550;
ctx.fillStyle = 'black';
ctx.fillText(text, x, y);
// 绘制边界框
ctx.strokeStyle = 'red';
ctx.lineWidth = 1;
ctx.strokeRect(
x,
y - metrics.fontBoundingBoxAscent,
metrics.width,
metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent
);
// 绘制实际边界框
ctx.strokeStyle = 'blue';
ctx.strokeRect(
x - metrics.actualBoundingBoxLeft,
y - metrics.actualBoundingBoxAscent,
metrics.actualBoundingBoxRight + metrics.actualBoundingBoxLeft,
metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent
);
}
measureTextDemo();
3.2 文本换行计算
// 文本自动换行
function wrapText(text, x, y, maxWidth, lineHeight) {
const words = text.split(' ');
let line = '';
let currentY = y;
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i] + ' ';
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && i > 0) {
ctx.fillText(line, x, currentY);
line = words[i] + ' ';
currentY += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line, x, currentY);
return currentY + lineHeight;
}
// 使用示例
const longText = 'This is a very long text that needs to be wrapped across multiple lines to fit within the specified width constraint.';
ctx.font = '16px Arial';
ctx.fillStyle = 'black';
wrapText(longText, 50, 650, 300, 20);
3.3 文本居中和对齐
// 在矩形区域内居中文本
function centerTextInRect(text, rectX, rectY, rectWidth, rectHeight) {
const metrics = ctx.measureText(text);
const textWidth = metrics.width;
const textHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
const x = rectX + (rectWidth - textWidth) / 2;
const y = rectY + (rectHeight + textHeight) / 2 - metrics.fontBoundingBoxDescent;
// 绘制矩形背景
ctx.fillStyle = 'lightblue';
ctx.fillRect(rectX, rectY, rectWidth, rectHeight);
// 绘制居中文本
ctx.fillStyle = 'darkblue';
ctx.fillText(text, x, y);
}
// 示例
ctx.font = '20px Arial';
centerTextInRect('Centered Text', 400, 600, 200, 80);
4. 高级文本效果
4.1 文本阴影
// 文本阴影效果
function textWithShadow() {
ctx.font = '48px Arial';
// 设置阴影
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 3;
ctx.fillStyle = 'white';
ctx.fillText('Shadow Text', 50, 750);
// 清除阴影设置
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
textWithShadow();
4.2 渐变文本
// 渐变填充文本
function gradientText() {
ctx.font = 'bold 48px Arial';
// 创建线性渐变
const gradient = ctx.createLinearGradient(0, 0, 400, 0);
gradient.addColorStop(0, '#ff6b6b');
gradient.addColorStop(0.5, '#4ecdc4');
gradient.addColorStop(1, '#45b7d1');
ctx.fillStyle = gradient;
ctx.fillText('Gradient Text', 50, 820);
// 径向渐变文本
const radialGradient = ctx.createRadialGradient(250, 870, 0, 250, 870, 100);
radialGradient.addColorStop(0, '#ffd700');
radialGradient.addColorStop(1, '#ff4500');
ctx.fillStyle = radialGradient;
ctx.fillText('Radial Text', 50, 890);
}
gradientText();
4.3 描边文本效果
// 多层描边文本
function multiStrokeText() {
const text = 'Outlined Text';
const x = 400;
const y = 750;
ctx.font = 'bold 36px Arial';
// 外层描边(粗)
ctx.strokeStyle = 'black';
ctx.lineWidth = 6;
ctx.strokeText(text, x, y);
// 中层描边(中等)
ctx.strokeStyle = 'white';
ctx.lineWidth = 3;
ctx.strokeText(text, x, y);
// 内层填充
ctx.fillStyle = 'red';
ctx.fillText(text, x, y);
}
multiStrokeText();
4.4 文本路径效果
// 沿路径绘制文本
function textAlongPath() {
const text = 'Text Along Curve';
const centerX = 500;
const centerY = 850;
const radius = 80;
ctx.font = '16px Arial';
ctx.fillStyle = 'purple';
// 绘制参考圆
ctx.strokeStyle = 'lightgray';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.stroke();
// 沿圆形路径绘制文本
for (let i = 0; i < text.length; i++) {
const char = text[i];
const angle = (i / text.length) * Math.PI * 2 - Math.PI / 2;
const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius;
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle + Math.PI / 2);
ctx.textAlign = 'center';
ctx.fillText(char, 0, 0);
ctx.restore();
}
}
textAlongPath();
5. 文本动画
5.1 打字机效果
// 打字机动画效果
class TypewriterEffect {
constructor(text, x, y, speed = 100) {
this.text = text;
this.x = x;
this.y = y;
this.speed = speed;
this.currentIndex = 0;
this.isAnimating = false;
}
start() {
this.isAnimating = true;
this.animate();
}
animate() {
if (!this.isAnimating || this.currentIndex >= this.text.length) {
this.isAnimating = false;
return;
}
// 清除之前的文本区域
ctx.clearRect(this.x - 10, this.y - 30, 400, 40);
// 绘制当前文本
const currentText = this.text.substring(0, this.currentIndex + 1);
ctx.font = '24px monospace';
ctx.fillStyle = 'green';
ctx.fillText(currentText, this.x, this.y);
// 绘制光标
if (Math.floor(Date.now() / 500) % 2) {
const metrics = ctx.measureText(currentText);
ctx.fillRect(this.x + metrics.width, this.y - 20, 2, 24);
}
this.currentIndex++;
setTimeout(() => {
requestAnimationFrame(() => this.animate());
}, this.speed);
}
stop() {
this.isAnimating = false;
}
}
// 使用示例
const typewriter = new TypewriterEffect('Hello, Canvas World!', 50, 950, 150);
// 添加按钮控制
const typeButton = document.createElement('button');
typeButton.textContent = '开始打字机效果';
typeButton.onclick = () => typewriter.start();
document.body.appendChild(typeButton);
5.2 文本淡入淡出
// 文本淡入淡出动画
class FadeTextAnimation {
constructor(text, x, y, duration = 2000) {
this.text = text;
this.x = x;
this.y = y;
this.duration = duration;
this.startTime = null;
this.isAnimating = false;
}
start() {
this.startTime = performance.now();
this.isAnimating = true;
this.animate();
}
animate() {
if (!this.isAnimating) return;
const currentTime = performance.now();
const elapsed = currentTime - this.startTime;
const progress = Math.min(elapsed / this.duration, 1);
// 计算透明度(淡入然后淡出)
let alpha;
if (progress < 0.5) {
alpha = progress * 2; // 淡入
} else {
alpha = (1 - progress) * 2; // 淡出
}
// 清除区域
ctx.clearRect(this.x - 10, this.y - 40, 300, 50);
// 绘制文本
ctx.font = '32px Arial';
ctx.fillStyle = `rgba(255, 0, 0, ${alpha})`;
ctx.fillText(this.text, this.x, this.y);
if (progress < 1) {
requestAnimationFrame(() => this.animate());
} else {
this.isAnimating = false;
}
}
}
// 使用示例
const fadeText = new FadeTextAnimation('Fading Text', 400, 950, 3000);
const fadeButton = document.createElement('button');
fadeButton.textContent = '开始淡入淡出';
fadeButton.onclick = () => fadeText.start();
document.body.appendChild(fadeButton);
5.3 文本波浪效果
// 波浪文本动画
class WaveTextAnimation {
constructor(text, x, y) {
this.text = text;
this.x = x;
this.y = y;
this.time = 0;
this.isAnimating = false;
}
start() {
this.isAnimating = true;
this.animate();
}
animate() {
if (!this.isAnimating) return;
this.time += 0.1;
// 清除区域
ctx.clearRect(this.x - 10, this.y - 50, 400, 80);
ctx.font = '24px Arial';
// 为每个字符计算波浪位置
let currentX = this.x;
for (let i = 0; i < this.text.length; i++) {
const char = this.text[i];
const waveY = this.y + Math.sin(this.time + i * 0.5) * 20;
ctx.fillStyle = `hsl(${(this.time * 50 + i * 30) % 360}, 70%, 50%)`;
ctx.fillText(char, currentX, waveY);
currentX += ctx.measureText(char).width;
}
requestAnimationFrame(() => this.animate());
}
stop() {
this.isAnimating = false;
}
}
// 使用示例
const waveText = new WaveTextAnimation('Wave Effect!', 50, 1050);
const waveButton = document.createElement('button');
waveButton.textContent = '开始波浪效果';
waveButton.onclick = () => waveText.start();
document.body.appendChild(waveButton);
6. 文本交互
6.1 可点击文本
// 可点击的文本链接
class ClickableText {
constructor(text, x, y, onClick) {
this.text = text;
this.x = x;
this.y = y;
this.onClick = onClick;
this.isHovered = false;
this.bounds = null;
this.calculateBounds();
this.setupEvents();
}
calculateBounds() {
ctx.font = '20px Arial';
const metrics = ctx.measureText(this.text);
this.bounds = {
x: this.x,
y: this.y - metrics.fontBoundingBoxAscent,
width: metrics.width,
height: metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent
};
}
draw() {
ctx.font = '20px Arial';
ctx.fillStyle = this.isHovered ? 'blue' : 'darkblue';
ctx.fillText(this.text, this.x, this.y);
// 绘制下划线(如果悬停)
if (this.isHovered) {
ctx.strokeStyle = 'blue';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(this.bounds.x, this.bounds.y + this.bounds.height);
ctx.lineTo(this.bounds.x + this.bounds.width, this.bounds.y + this.bounds.height);
ctx.stroke();
}
}
contains(x, y) {
return x >= this.bounds.x && x <= this.bounds.x + this.bounds.width &&
y >= this.bounds.y && y <= this.bounds.y + this.bounds.height;
}
setupEvents() {
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const wasHovered = this.isHovered;
this.isHovered = this.contains(x, y);
if (wasHovered !== this.isHovered) {
canvas.style.cursor = this.isHovered ? 'pointer' : 'default';
this.redraw();
}
});
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (this.contains(x, y)) {
this.onClick();
}
});
}
redraw() {
// 清除文本区域
ctx.clearRect(
this.bounds.x - 5,
this.bounds.y - 5,
this.bounds.width + 10,
this.bounds.height + 10
);
this.draw();
}
}
// 创建可点击文本
const clickableText = new ClickableText(
'Click me!',
400,
1050,
() => alert('Text clicked!')
);
clickableText.draw();
6.2 文本选择
// 文本选择功能
class SelectableText {
constructor(text, x, y) {
this.text = text;
this.x = x;
this.y = y;
this.selectionStart = -1;
this.selectionEnd = -1;
this.isSelecting = false;
this.setupEvents();
}
draw() {
ctx.font = '18px monospace';
// 绘制选择背景
if (this.selectionStart >= 0 && this.selectionEnd >= 0) {
const start = Math.min(this.selectionStart, this.selectionEnd);
const end = Math.max(this.selectionStart, this.selectionEnd);
const beforeText = this.text.substring(0, start);
const selectedText = this.text.substring(start, end);
const beforeWidth = ctx.measureText(beforeText).width;
const selectedWidth = ctx.measureText(selectedText).width;
ctx.fillStyle = 'lightblue';
ctx.fillRect(this.x + beforeWidth, this.y - 16, selectedWidth, 20);
}
// 绘制文本
ctx.fillStyle = 'black';
ctx.fillText(this.text, this.x, this.y);
}
getCharIndexAtPosition(x) {
ctx.font = '18px monospace';
for (let i = 0; i <= this.text.length; i++) {
const textWidth = ctx.measureText(this.text.substring(0, i)).width;
if (this.x + textWidth > x) {
return Math.max(0, i - 1);
}
}
return this.text.length;
}
setupEvents() {
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 检查是否在文本区域内
if (y >= this.y - 20 && y <= this.y + 5) {
this.isSelecting = true;
this.selectionStart = this.getCharIndexAtPosition(x);
this.selectionEnd = this.selectionStart;
this.redraw();
}
});
canvas.addEventListener('mousemove', (e) => {
if (!this.isSelecting) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
this.selectionEnd = this.getCharIndexAtPosition(x);
this.redraw();
});
canvas.addEventListener('mouseup', () => {
this.isSelecting = false;
});
}
redraw() {
// 清除文本区域
ctx.clearRect(this.x - 5, this.y - 25, 400, 30);
this.draw();
}
getSelectedText() {
if (this.selectionStart >= 0 && this.selectionEnd >= 0) {
const start = Math.min(this.selectionStart, this.selectionEnd);
const end = Math.max(this.selectionStart, this.selectionEnd);
return this.text.substring(start, end);
}
return '';
}
}
// 创建可选择文本
const selectableText = new SelectableText(
'This text can be selected with mouse',
50,
1150
);
selectableText.draw();
7. 文本性能优化
7.1 文本缓存
// 文本渲染缓存
class TextCache {
constructor() {
this.cache = new Map();
}
getCacheKey(text, font, fillStyle, strokeStyle) {
return `${text}_${font}_${fillStyle}_${strokeStyle}`;
}
getOrCreateText(text, font, fillStyle, strokeStyle = null) {
const key = this.getCacheKey(text, font, fillStyle, strokeStyle);
if (this.cache.has(key)) {
return this.cache.get(key);
}
// 创建离屏canvas渲染文本
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCtx.font = font;
const metrics = tempCtx.measureText(text);
tempCanvas.width = Math.ceil(metrics.width) + 4;
tempCanvas.height = Math.ceil(metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent) + 4;
tempCtx.font = font;
tempCtx.fillStyle = fillStyle;
tempCtx.textBaseline = 'top';
tempCtx.fillText(text, 2, 2);
if (strokeStyle) {
tempCtx.strokeStyle = strokeStyle;
tempCtx.strokeText(text, 2, 2);
}
const cachedText = {
canvas: tempCanvas,
width: tempCanvas.width,
height: tempCanvas.height
};
this.cache.set(key, cachedText);
return cachedText;
}
drawCachedText(text, x, y, font, fillStyle, strokeStyle = null) {
const cached = this.getOrCreateText(text, font, fillStyle, strokeStyle);
ctx.drawImage(cached.canvas, x - 2, y - 2);
}
clear() {
this.cache.clear();
}
}
// 使用示例
const textCache = new TextCache();
// 性能测试
function performanceTest() {
const startTime = performance.now();
// 不使用缓存
for (let i = 0; i < 100; i++) {
ctx.font = '16px Arial';
ctx.fillStyle = 'black';
ctx.fillText('Performance Test', 600 + (i % 10) * 2, 1100 + Math.floor(i / 10) * 2);
}
const noCacheTime = performance.now() - startTime;
const startTime2 = performance.now();
// 使用缓存
for (let i = 0; i < 100; i++) {
textCache.drawCachedText(
'Performance Test',
600 + (i % 10) * 2,
1150 + Math.floor(i / 10) * 2,
'16px Arial',
'black'
);
}
const cacheTime = performance.now() - startTime2;
console.log(`无缓存时间: ${noCacheTime.toFixed(2)}ms`);
console.log(`缓存时间: ${cacheTime.toFixed(2)}ms`);
console.log(`性能提升: ${((noCacheTime - cacheTime) / noCacheTime * 100).toFixed(1)}%`);
}
// 添加性能测试按钮
const perfButton = document.createElement('button');
perfButton.textContent = '运行性能测试';
perfButton.onclick = performanceTest;
document.body.appendChild(perfButton);
8. 小结
本章全面介绍了Canvas的文本渲染与样式控制:
- 文本绘制基础:fillText、strokeText方法和基本属性设置
- 字体管理:Web字体加载、字体回退机制
- 文本测量:measureText方法、文本换行、居中对齐
- 高级效果:阴影、渐变、描边、路径文本
- 文本动画:打字机效果、淡入淡出、波浪动画
- 文本交互:可点击文本、文本选择功能
- 性能优化:文本缓存技术
下一章我们将学习图像处理与像素操作,包括图像绘制、像素数据操作和图像滤镜效果。
9. 练习题
- 创建一个文本编辑器,支持字体、大小、颜色的实时调整
- 实现一个文本跑马灯效果
- 制作一个文字云生成器
- 创建一个支持多种动画效果的文本展示组件
- 实现一个简单的富文本渲染器