返回文章列表

HTML5 Canvas绘图与动画基础

HTML5 Canvas 是一种通过 JavaScript 在网页上绘制图形的强大技术。它可以用于绘制图表、制作游戏、创建动画效果、处理图像等。本文将带你从零开始学习 Canvas API,掌握基础绘图到动画实现的完整流程。

一、Canvas 元素介绍

Canvas 是 HTML5 新增的元素,它本身只是一个矩形的画布,所有的绘制工作都需要通过 JavaScript 来完成。Canvas 提供了一个坐标系统,左上角为原点 (0, 0),x 轴向右延伸,y 轴向下延伸。

在 HTML 中创建 Canvas 元素非常简单:

<!-- 基本语法 -->
<canvas id="myCanvas" width="800" height="600"></canvas>

<!-- 也可以通过 CSS 控制尺寸,但注意:CSS 尺寸和 Canvas 实际尺寸不同 -->
<canvas id="myCanvas2" width="800" height="600"
        style="width: 400px; height: 300px;">
    您的浏览器不支持 Canvas
</canvas>

重要提示:Canvas 的 width 和 height 属性定义的是绘图区域的实际像素大小,而 CSS 的 width 和 height 只是显示尺寸。如果两者不一致,Canvas 内容会被缩放,可能导致图形模糊。

二、获取 2D 绘图上下文

要在 Canvas 上绘图,首先需要获取绘图上下文(Context)。目前最常用的是 2D 上下文:

// 获取 Canvas 元素
const canvas = document.getElementById('myCanvas');

// 获取 2D 绘图上下文
const ctx = canvas.getContext('2d');

// 检查是否支持 Canvas
if (!ctx) {
    console.error('您的浏览器不支持 Canvas 2D');
}

// 也可以设置画布大小(会清空画布内容)
canvas.width = 800;
canvas.height = 600;

三、绘制基本图形

3.1 矩形

Canvas 提供了三个绘制矩形的方法:

const ctx = canvas.getContext('2d');

// 绘制填充矩形
ctx.fillStyle = '#ff6b6b';
ctx.fillRect(50, 50, 200, 100); // x, y, width, height

// 绘制矩形边框
ctx.strokeStyle = '#4ecdc4';
ctx.lineWidth = 3;
ctx.strokeRect(300, 50, 200, 100);

// 清除矩形区域(变为透明)
ctx.clearRect(100, 75, 50, 50); // 擦除填充矩形的一部分

3.2 圆形与弧线

使用 arc() 方法绘制圆形和弧线:

const ctx = canvas.getContext('2d');

// 绘制填充圆形
ctx.beginPath();
ctx.arc(150, 150, 80, 0, Math.PI * 2); // x, y, radius, startAngle, endAngle
ctx.fillStyle = '#ffe66d';
ctx.fill();
ctx.closePath();

// 绘制描边圆形
ctx.beginPath();
ctx.arc(350, 150, 80, 0, Math.PI * 2);
ctx.strokeStyle = '#1a535c';
ctx.lineWidth = 4;
ctx.stroke();
ctx.closePath();

// 绘制半圆
ctx.beginPath();
ctx.arc(550, 150, 80, 0, Math.PI); // 从 0 到 PI(180度)
ctx.fillStyle = '#ff6b6b';
ctx.fill();
ctx.closePath();

// 绘制扇形(饼图的一部分)
ctx.beginPath();
ctx.moveTo(250, 300); // 圆心
ctx.arc(250, 300, 80, 0, Math.PI / 3); // 60度的扇形
ctx.closePath();
ctx.fillStyle = '#4ecdc4';
ctx.fill();

3.3 路径绘制

路径是更灵活的绘图方式,可以绘制任意形状:

const ctx = canvas.getContext('2d');

// 绘制三角形
ctx.beginPath();
ctx.moveTo(100, 50);    // 起点
ctx.lineTo(200, 150);   // 第二个点
ctx.lineTo(0, 150);     // 第三个点
ctx.closePath();        // 闭合路径(连接回起点)
ctx.fillStyle = '#ff6b6b';
ctx.fill();
ctx.strokeStyle = '#c44569';
ctx.lineWidth = 2;
ctx.stroke();

// 绘制五角星
function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius) {
    let rot = Math.PI / 2 * 3;
    let x = cx;
    let y = cy;
    let step = Math.PI / spikes;

    ctx.beginPath();
    ctx.moveTo(cx, cy - outerRadius);

    for (let i = 0; i < spikes; i++) {
        x = cx + Math.cos(rot) * outerRadius;
        y = cy + Math.sin(rot) * outerRadius;
        ctx.lineTo(x, y);
        rot += step;

        x = cx + Math.cos(rot) * innerRadius;
        y = cy + Math.sin(rot) * innerRadius;
        ctx.lineTo(x, y);
        rot += step;
    }

    ctx.lineTo(cx, cy - outerRadius);
    ctx.closePath();
    ctx.fillStyle = '#ffe66d';
    ctx.fill();
    ctx.strokeStyle = '#f9ca24';
    ctx.lineWidth = 2;
    ctx.stroke();
}

drawStar(ctx, 400, 300, 5, 80, 40);

四、颜色与渐变

4.1 纯色填充

const ctx = canvas.getContext('2d');

// 使用颜色名称
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 50);

// 使用十六进制颜色
ctx.fillStyle = '#ff6b6b';
ctx.fillRect(120, 10, 100, 50);

// 使用 RGB
ctx.fillStyle = 'rgb(255, 107, 107)';
ctx.fillRect(230, 10, 100, 50);

// 使用 RGBA(带透明度)
ctx.fillStyle = 'rgba(255, 107, 107, 0.5)';
ctx.fillRect(340, 10, 100, 50);

// 设置描边颜色
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.strokeRect(10, 10, 430, 50);

4.2 线性渐变

const ctx = canvas.getContext('2d');

// 创建线性渐变(从左上到右下)
const linearGradient = ctx.createLinearGradient(0, 0, 400, 0);
linearGradient.addColorStop(0, '#ff6b6b');    // 起始颜色
linearGradient.addColorStop(0.5, '#4ecdc4');  // 中间颜色
linearGradient.addColorStop(1, '#ffe66d');     // 结束颜色

ctx.fillStyle = linearGradient;
ctx.fillRect(50, 50, 400, 100);

// 对角线渐变
const diagGradient = ctx.createLinearGradient(50, 200, 450, 300);
diagGradient.addColorStop(0, '#667eea');
diagGradient.addColorStop(1, '#764ba2');
ctx.fillStyle = diagGradient;
ctx.fillRect(50, 200, 400, 100);

4.3 径向渐变

const ctx = canvas.getContext('2d');

// 创建径向渐变
const radialGradient = ctx.createRadialGradient(
    200, 150, 10,  // 内圆:x, y, radius
    200, 150, 100  // 外圆:x, y, radius
);
radialGradient.addColorStop(0, '#ffffff');    // 中心为白色
radialGradient.addColorStop(0.3, '#ffe66d');
radialGradient.addColorStop(1, '#ff6b6b');   // 边缘为红色

ctx.fillStyle = radialGradient;
ctx.beginPath();
ctx.arc(200, 150, 100, 0, Math.PI * 2);
ctx.fill();

五、文字绘制

const ctx = canvas.getContext('2d');

// 设置字体样式
ctx.font = 'bold 36px "Noto Sans SC", sans-serif';
ctx.fillStyle = '#333';
ctx.textAlign = 'center';       // 水平对齐:left, center, right
ctx.textBaseline = 'middle';    // 垂直对齐:top, middle, bottom

// 绘制填充文字
ctx.fillText('Hello Canvas!', 400, 50);

// 绘制描边文字
ctx.strokeStyle = '#ff6b6b';
ctx.lineWidth = 2;
ctx.strokeText('Hello Canvas!', 400, 100);

// 测量文字宽度(常用于居中对齐)
const text = 'Hello Canvas!';
const metrics = ctx.measureText(text);
console.log('文字宽度:', metrics.width);

// 设置文字阴影
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 3;
ctx.fillStyle = '#4ecdc4';
ctx.fillText('带阴影的文字', 400, 200);

// 清除阴影设置
ctx.shadowColor = 'transparent';

六、图像操作

const ctx = canvas.getContext('2d');

// 绘制图像
const img = new Image();
img.onload = function() {
    // 原始大小绘制
    ctx.drawImage(img, 50, 50);

    // 指定大小绘制(缩放)
    ctx.drawImage(img, 50, 250, 200, 150); // img, x, y, width, height

    // 裁剪绘制(从源图像中截取一部分)
    ctx.drawImage(img,
        0, 0, 100, 100,    // 源图像裁剪区域
        300, 50, 200, 200  // 目标绘制区域
    );
};
img.src = 'image.jpg';

// 将 Canvas 转为图片(可用于下载)
function downloadCanvas() {
    const link = document.createElement('a');
    link.download = 'my-drawing.png';
    link.href = canvas.toDataURL('image/png');
    link.click();
}

// 获取像素数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; // Uint8ClampedArray

// 修改像素(例如反色效果)
for (let i = 0; i < data.length; i += 4) {
    data[i]     = 255 - data[i];     // R
    data[i + 1] = 255 - data[i + 1]; // G
    data[i + 2] = 255 - data[i + 2]; // B
    // data[i + 3] 是 Alpha,保持不变
}
ctx.putImageData(imageData, 0, 0);

七、动画循环(requestAnimationFrame)

Canvas 动画的核心是使用 requestAnimationFrame 创建流畅的动画循环。与 setInterval 不同,requestAnimationFrame 会自动与浏览器的刷新率同步(通常为 60fps),并且在标签页不可见时会自动暂停,更加高效。

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;

let x = 50;
let y = 300;
let dx = 5; // 每帧移动的像素
let dy = 2;
let radius = 30;

function animate() {
    // 清除画布(清空上一帧的内容)
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 绘制背景
    ctx.fillStyle = '#1a1a2e';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // 绘制移动的球
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2);
    ctx.fillStyle = '#ff6b6b';
    ctx.fill();
    ctx.closePath();

    // 更新位置
    x += dx;
    y += dy;

    // 边界碰撞检测(反弹)
    if (x + radius > canvas.width || x - radius < 0) {
        dx = -dx; // 水平反弹
    }
    if (y + radius > canvas.height || y - radius < 0) {
        dy = -dy; // 垂直反弹
    }

    // 请求下一帧
    requestAnimationFrame(animate);
}

// 启动动画
animate();

更精确的动画应该基于时间差(delta time),这样动画速度不会受帧率影响:

let lastTime = 0;
const speed = 200; // 像素/秒

function animate(timestamp) {
    // 计算时间差(毫秒转为秒)
    const deltaTime = (timestamp - lastTime) / 1000;
    lastTime = timestamp;

    // 避免第一帧的时间差过大
    const dt = Math.min(deltaTime, 0.1);

    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 使用基于时间的移动
    x += dx * speed * dt;
    y += dy * speed * dt;

    // ... 绘制代码 ...

    requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

八、实战案例:粒子动画效果

下面是一个完整的粒子动画效果,包含粒子的生成、运动、连线效果和鼠标交互:

const canvas = document.getElementById('particleCanvas');
const ctx = canvas.getContext('2d');

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// 响应窗口大小变化
window.addEventListener('resize', function() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
});

// 粒子类
class Particle {
    constructor(x, y) {
        this.x = x || Math.random() * canvas.width;
        this.y = y || Math.random() * canvas.height;
        this.size = Math.random() * 3 + 1;
        this.speedX = (Math.random() - 0.5) * 2;
        this.speedY = (Math.random() - 0.5) * 2;
        this.color = `hsl(${Math.random() * 60 + 340}, 80%, 60%)`;
    }

    update() {
        this.x += this.speedX;
        this.y += this.speedY;

        // 边界反弹
        if (this.x > canvas.width || this.x < 0) {
            this.speedX = -this.speedX;
        }
        if (this.y > canvas.height || this.y < 0) {
            this.speedY = -this.speedY;
        }
    }

    draw() {
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
        ctx.fillStyle = this.color;
        ctx.fill();
    }
}

// 粒子数组
const particles = [];
const particleCount = 100;
const connectionDistance = 150;

// 初始化粒子
for (let i = 0; i < particleCount; i++) {
    particles.push(new Particle());
}

// 鼠标位置追踪
let mouse = { x: null, y: null, radius: 200 };

canvas.addEventListener('mousemove', function(event) {
    mouse.x = event.x;
    mouse.y = event.y;
});

canvas.addEventListener('mouseleave', function() {
    mouse.x = null;
    mouse.y = null;
});

// 绘制粒子之间的连线
function drawConnections() {
    for (let i = 0; i < particles.length; i++) {
        for (let j = i + 1; j < particles.length; j++) {
            const dx = particles[i].x - particles[j].x;
            const dy = particles[i].y - particles[j].y;
            const distance = Math.sqrt(dx * dx + dy * dy);

            if (distance < connectionDistance) {
                const opacity = 1 - distance / connectionDistance;
                ctx.beginPath();
                ctx.strokeStyle = `rgba(255, 107, 107, ${opacity * 0.5})`;
                ctx.lineWidth = 1;
                ctx.moveTo(particles[i].x, particles[i].y);
                ctx.lineTo(particles[j].x, particles[j].y);
                ctx.stroke();
            }
        }
    }
}

// 鼠标与粒子的交互连线
function drawMouseConnections() {
    if (mouse.x === null) return;

    particles.forEach(function(particle) {
        const dx = mouse.x - particle.x;
        const dy = mouse.y - particle.y;
        const distance = Math.sqrt(dx * dx + dy * dy);

        if (distance < mouse.radius) {
            const opacity = 1 - distance / mouse.radius;
            ctx.beginPath();
            ctx.strokeStyle = `rgba(78, 205, 196, ${opacity})`;
            ctx.lineWidth = 1.5;
            ctx.moveTo(particle.x, particle.y);
            ctx.lineTo(mouse.x, mouse.y);
            ctx.stroke();

            // 鼠标靠近时粒子会被轻微推开
            particle.x -= dx * 0.02;
            particle.y -= dy * 0.02;
        }
    });
}

// 主动画循环
function animate() {
    // 半透明清除(产生拖尾效果)
    ctx.fillStyle = 'rgba(15, 23, 42, 0.15)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    particles.forEach(function(particle) {
        particle.update();
        particle.draw();
    });

    drawConnections();
    drawMouseConnections();

    requestAnimationFrame(animate);
}

animate();

对应的 HTML 结构:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>粒子动画</title>
    <style>
        * { margin: 0; padding: 0; }
        body { overflow: hidden; background: #0f172a; }
        canvas { display: block; }
    </style>
</head>
<body>
    <canvas id="particleCanvas"></canvas>
    <script>
        /* 上面的 JavaScript 代码 */
    </script>
</body>
</html>

九、Canvas 常用技巧与注意事项

9.1 保存与恢复状态

const ctx = canvas.getContext('2d');

// 保存当前状态(包括变换矩阵、裁剪区域、样式属性等)
ctx.save();

// 进行变换或样式修改
ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);
ctx.fillStyle = '#ff6b6b';
ctx.fillRect(-25, -25, 50, 50);

// 恢复之前保存的状态
ctx.restore();

// 此时坐标原点和样式已经恢复
ctx.fillStyle = '#4ecdc4';
ctx.fillRect(200, 50, 50, 50);

9.2 Canvas 性能优化

// 离屏 Canvas 示例:绘制静态背景
const offscreen = document.createElement('canvas');
offscreen.width = canvas.width;
offscreen.height = canvas.height;
const offCtx = offscreen.getContext('2d');

// 在离屏 Canvas 上绘制复杂静态内容(只需绘制一次)
offCtx.fillStyle = '#1a1a2e';
offCtx.fillRect(0, 0, offscreen.width, offscreen.height);
offCtx.font = 'bold 48px sans-serif';
offCtx.fillStyle = '#ffffff';
offCtx.fillText('Static Background', 100, 300);

// 在动画循环中直接绘制离屏 Canvas(高效)
function animate() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(offscreen, 0, 0); // 一次性绘制整个背景

    // 绘制动态内容
    // ...

    requestAnimationFrame(animate);
}

十、总结

HTML5 Canvas 是一个功能强大的 2D 绘图工具,适用于数据可视化、游戏开发、图像处理等多种场景。通过本文的学习,你应该掌握了以下内容:

接下来,你可以尝试将 Canvas 与其他 Web 技术(如 WebGL、SVG)结合使用,或者探索 Canvas 库(如 Konva.js、Fabric.js)来简化复杂场景的开发。

动手挑战

学到这里,不妨动手试一试以下练习,巩固你的理解:

  1. 基础练习:回顾本文核心概念,用自己的话总结关键知识点。
  2. 进阶实践:将文中的示例代码运行一遍,尝试修改参数观察变化。
  3. 拓展思考:想一想这个技术/方法还能应用在哪些场景中?

小贴士:遇到问题时,先独立思考,再查阅资料,最后请教他人——这是成长最快的学习方式。

赞赏支持

本文更新于 2026-08-22,环境 Python 3.12