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:将静态内容绘制到离屏 Canvas,只在需要时更新
- 批量路径操作:多个路径可以放在同一个 beginPath/closePath 中
- 分层渲染:使用多个 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 元素的基本概念和坐标系统
- 获取 2D 绘图上下文的方法
- 绘制基本图形:矩形、圆形、路径
- 使用纯色、线性渐变和径向渐变
- 在 Canvas 上绘制和样式化文字
- 图像的加载、绘制和像素操作
- 使用 requestAnimationFrame 创建流畅动画
- 粒子动画系统的完整实现
- Canvas 状态管理和性能优化技巧
接下来,你可以尝试将 Canvas 与其他 Web 技术(如 WebGL、SVG)结合使用,或者探索 Canvas 库(如 Konva.js、Fabric.js)来简化复杂场景的开发。