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的文本渲染与样式控制:

  1. 文本绘制基础:fillText、strokeText方法和基本属性设置
  2. 字体管理:Web字体加载、字体回退机制
  3. 文本测量:measureText方法、文本换行、居中对齐
  4. 高级效果:阴影、渐变、描边、路径文本
  5. 文本动画:打字机效果、淡入淡出、波浪动画
  6. 文本交互:可点击文本、文本选择功能
  7. 性能优化:文本缓存技术

下一章我们将学习图像处理与像素操作,包括图像绘制、像素数据操作和图像滤镜效果。

9. 练习题

  1. 创建一个文本编辑器,支持字体、大小、颜色的实时调整
  2. 实现一个文本跑马灯效果
  3. 制作一个文字云生成器
  4. 创建一个支持多种动画效果的文本展示组件
  5. 实现一个简单的富文本渲染器