深入理解 Canvas:从基础 API 到游戏开发实战
前言:Canvas 作为 HTML5 最强大的绘图技术之一,已经成为前端可视化、游戏开发、动画制作的核心技术栈。本文将从 Canvas 基础 API 入手,深入剖析动画原理,最终带你实现一个完整的飞机大战游戏。
一、Canvas 基础:认识你的画布
1.1 什么是 Canvas?
Canvas 是 HTML5 新增的元素,它提供了一个基于像素的绘图表面。你可以把它想象成一张白纸,通过 JavaScript 的画笔(API)在上面绘制任意图形、文字、图片。
<canvas id="myCanvas" width="800" height="600"></canvas>
关键特性:
- 位图(Bitmap):Canvas 是基于像素的,放大后会失真
- 即时绘制:一旦绘制完成,就变成像素数据,无法单独修改
- 高性能:直接由 GPU 加速,适合复杂动画和游戏
1.2 获取绘图上下文
Canvas 本身只是一个容器,真正的绑图能力来自上下文(Context):
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d'); // 2D 上下文
// 或 3D 上下文(WebGL)
// const gl = canvas.getContext('webgl');
getContext('2d') 返回一个 CanvasRenderingContext2D 对象,拥有 100+ 个绘图方法。
二、Canvas 绑图 API 深度解析
2.1 基础图形绘制
矩形(Rect)
矩形是 Canvas 中最基础的图形,也是其他复杂图形的基础:
// 填充矩形
ctx.fillStyle = '#3498db';
ctx.fillRect(10, 10, 150, 100); // x, y, width, height
// 描边矩形
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 2;
ctx.strokeRect(170, 10, 150, 100);
// 清除矩形区域(用于擦除)
ctx.clearRect(50, 50, 100, 50); // 清除指定区域
坐标系说明:
- 原点
(0, 0)在左上角 - X 轴向右增大
- Y 轴向下增大
圆形(Circle)
Canvas 没有直接画圆的方法,而是通过路径(Path) 来绘制:
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2); // x, y, radius, startAngle, endAngle
ctx.fillStyle = '#e74c3c';
ctx.fill();
ctx.strokeStyle = '#c0392b';
ctx.stroke();
参数详解:
startAngle:起始角度(弧度)endAngle:结束角度(弧度)Math.PI * 2:360度(完整的圆)
线条(Line)
ctx.beginPath();
ctx.moveTo(50, 50); // 移动到起点
ctx.lineTo(200, 50); // 画线到终点
ctx.lineTo(200, 200);
ctx.lineTo(50, 200);
ctx.closePath(); // 闭合路径
ctx.strokeStyle = '#9b59b6';
ctx.lineWidth = 3;
ctx.stroke();
2.2 颜色与样式
Canvas 提供了丰富的颜色控制:
// 填充颜色
ctx.fillStyle = '#ff6b6b'; // 十六进制
ctx.fillStyle = 'rgb(255, 107, 107)'; // RGB
ctx.fillStyle = 'rgba(255, 107, 107, 0.5)'; // RGBA(带透明度)
ctx.fillStyle = 'hsl(0, 100%, 70%)'; // HSL
// 描边颜色
ctx.strokeStyle = '#4ecdc4';
// 渐变
const gradient = ctx.createLinearGradient(0, 0, 200, 0);
gradient.addColorStop(0, '#ff6b6b');
gradient.addColorStop(1, '#4ecdc4');
ctx.fillStyle = gradient;
ctx.fillRect(10, 10, 200, 100);
2.3 文本绘制
ctx.font = 'bold 24px Arial';
ctx.fillStyle = '#2c3e50';
ctx.textAlign = 'center'; // left | center | right
ctx.textBaseline = 'middle'; // top | middle | bottom
ctx.fillText('Hello Canvas!', 400, 300); // 填充文本
ctx.strokeText('Hello Canvas!', 400, 350); // 描边文本
三、动画原理:Canvas 的心跳
3.1 为什么需要动画?
Canvas 的强大之处在于动态内容。游戏、数据可视化、交互式动画都需要持续更新画面。
动画的本质: 快速连续地绘制静态画面,利用人眼的视觉暂留形成运动错觉。
3.2 setInterval vs requestAnimationFrame
❌ setInterval 的问题
// 不推荐:时间可能和屏幕刷新率不同步
setInterval(() => {
update();
draw();
}, 16); // 约 60fps
问题:
- 时间不精确,可能和屏幕刷新率冲突
- 即使页面不可见,也会继续执行,浪费资源
- 多个定时器可能导致卡顿
✅ requestAnimationFrame(推荐)
function animate() {
// 1. 清除上一帧
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 2. 更新状态
update();
// 3. 绘制当前帧
draw();
// 4. 请求下一帧
requestAnimationFrame(animate);
}
// 启动动画
requestAnimationFrame(animate);
requestAnimationFrame 的优势:
- 自动同步屏幕刷新率(通常 60fps)
- 页面不可见时自动暂停,节省资源
- 浏览器优化,更流畅的动画效果
3.3 游戏循环(Game Loop)
游戏开发的核心是一个稳定的游戏循环:
class Game {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.lastTime = 0;
this.gameLoop = this.gameLoop.bind(this);
}
gameLoop(timestamp) {
// 计算时间差(deltaTime)
const deltaTime = timestamp - this.lastTime;
this.lastTime = timestamp;
// 1. 处理输入
this.handleInput();
// 2. 更新游戏状态(基于 deltaTime)
this.update(deltaTime);
// 3. 渲染画面
this.render();
// 4. 请求下一帧
requestAnimationFrame(this.gameLoop);
}
start() {
this.lastTime = performance.now();
requestAnimationFrame(this.gameLoop);
}
}
四、实战案例:飞机大战游戏
4.1 项目初始化
使用 Vite 快速搭建开发环境:
npm create vite@latest airplane -- --template vanilla
cd airplane
npm install
npm run dev
4.2 游戏架构设计
src/
├── main.js # 入口文件
├── game.js # 游戏主类
├── player.js # 玩家飞机
├── enemy.js # 敌机
├── bullet.js # 子弹
├── utils.js # 工具函数
└── style.css # 样式
4.3 核心代码实现
游戏主类(Game.js)
export class Game {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.player = null;
this.enemies = [];
this.bullets = [];
this.score = 0;
this.gameOver = false;
// 绑定事件
this.bindEvents();
}
bindEvents() {
// 键盘控制
window.addEventListener('keydown', (e) => {
this.player?.handleKeyDown(e.key);
});
window.addEventListener('keyup', (e) => {
this.player?.handleKeyUp(e.key);
});
}
init() {
this.player = new Player(this.canvas);
this.enemies = [];
this.bullets = [];
this.score = 0;
this.gameOver = false;
this.start();
}
update(deltaTime) {
if (this.gameOver) return;
// 更新玩家
this.player.update(deltaTime);
// 更新子弹
this.bullets.forEach(bullet => bullet.update(deltaTime));
this.bullets = this.bullets.filter(bullet => bullet.active);
// 更新敌机
this.enemies.forEach(enemy => enemy.update(deltaTime));
this.enemies = this.enemies.filter(enemy => enemy.active);
// 碰撞检测
this.checkCollisions();
// 生成敌机
this.spawnEnemies();
}
render() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制背景
this.drawBackground();
// 绘制游戏对象
this.player.draw(this.ctx);
this.bullets.forEach(bullet => bullet.draw(this.ctx));
this.enemies.forEach(enemy => enemy.draw(this.ctx));
// 绘制 UI
this.drawUI();
}
drawBackground() {
// 星空背景
this.ctx.fillStyle = '#0a0a2e';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
drawUI() {
this.ctx.fillStyle = '#fff';
this.ctx.font = '20px Arial';
this.ctx.fillText(`分数: ${this.score}`, 10, 30);
if (this.gameOver) {
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = '#e74c3c';
this.ctx.font = 'bold 48px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText('游戏结束', this.canvas.width / 2, this.canvas.height / 2);
this.ctx.fillStyle = '#fff';
this.ctx.font = '24px Arial';
this.ctx.fillText(`最终得分: ${this.score}`, this.canvas.width / 2, this.canvas.height / 2 + 50);
}
}
}
玩家飞机(Player.js)
export class Player {
constructor(canvas) {
this.canvas = canvas;
this.x = canvas.width / 2;
this.y = canvas.height - 80;
this.width = 60;
this.height = 60;
this.speed = 5;
this.keys = {};
}
handleKeyDown(key) {
this.keys[key] = true;
}
handleKeyUp(key) {
this.keys[key] = false;
}
update(deltaTime) {
// 移动控制
if (this.keys['ArrowLeft'] || this.keys['a']) {
this.x = Math.max(0, this.x - this.speed);
}
if (this.keys['ArrowRight'] || this.keys['d']) {
this.x = Math.min(this.canvas.width - this.width, this.x + this.speed);
}
if (this.keys['ArrowUp'] || this.keys['w']) {
this.y = Math.max(0, this.y - this.speed);
}
if (this.keys['ArrowDown'] || this.keys['s']) {
this.y = Math.min(this.canvas.height - this.height, this.y + this.speed);
}
}
draw(ctx) {
// 绘制飞机(简化版)
ctx.fillStyle = '#4ecdc4';
ctx.beginPath();
ctx.moveTo(this.x + this.width / 2, this.y);
ctx.lineTo(this.x + this.width, this.y + this.height);
ctx.lineTo(this.x, this.y + this.height);
ctx.closePath();
ctx.fill();
}
}
4.4 性能优化技巧
1. 对象池(Object Pool)
避免频繁创建和销毁对象:
class BulletPool {
constructor(maxSize) {
this.pool = [];
this.maxSize = maxSize;
}
get() {
if (this.pool.length > 0) {
return this.pool.pop();
}
return new Bullet();
}
release(bullet) {
if (this.pool.length < this.maxSize) {
bullet.reset();
this.pool.push(bullet);
}
}
}
2. 离屏渲染
将复杂图形预先绘制到离屏 Canvas:
// 创建离屏 Canvas
const offscreenCanvas = document.createElement('canvas');
const offscreenCtx = offscreenCanvas.getContext('2d');
// 在离屏 Canvas 上绘制复杂图形
function drawComplexShape(ctx) {
ctx.fillStyle = '#e74c3c';
ctx.beginPath();
ctx.arc(50, 50, 30, 0, Math.PI * 2);
ctx.fill();
// ... 更多复杂绘制
}
// 预先绘制一次
drawComplexShape(offscreenCtx);
// 在主 Canvas 上快速绘制
function render() {
ctx.drawImage(offscreenCanvas, 0, 0); // 快速复制
}
3. 局部更新
只重绘变化的区域,而不是整个画布:
function render() {
// 清除上一帧的位置
ctx.clearRect(lastX, lastY, width, height);
// 绘制新位置
draw(newX, newY);
// 保存当前位置
lastX = newX;
lastY = newY;
}
五、Canvas 高级应用
5.1 图像处理
Canvas 可以直接操作像素数据:
// 获取图像数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; // RGBA 数组
// 灰度滤镜
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // R
data[i + 1] = avg; // G
data[i + 2] = avg; // B
// data[i + 3] 是 Alpha
}
// 应用修改
ctx.putImageData(imageData, 0, 0);
5.2 物理模拟
结合物理引擎实现真实效果:
class PhysicsObject {
constructor(x, y) {
this.x = x;
this.y = y;
this.vx = 0; // 速度 x
this.vy = 0; // 速度 y
this.ax = 0; // 加速度 x
this.ay = 0; // 加速度 y
this.gravity = 0.5;
}
update(deltaTime) {
// 应用重力
this.ay += this.gravity;
// 更新速度
this.vx += this.ax * deltaTime;
this.vy += this.ay * deltaTime;
// 更新位置
this.x += this.vx * deltaTime;
this.y += this.vy * deltaTime;
// 边界检测
if (this.y > canvas.height - this.radius) {
this.y = canvas.height - this.radius;
this.vy *= -0.8; // 弹跳系数
}
}
}
5.3 数据可视化
Canvas 是实现自定义图表的利器:
function drawBarChart(data) {
const barWidth = 50;
const gap = 10;
const startX = 50;
const maxHeight = 300;
data.forEach((value, index) => {
const x = startX + (barWidth + gap) * index;
const height = (value / Math.max(...data)) * maxHeight;
const y = canvas.height - 50 - height;
// 渐变色
const gradient = ctx.createLinearGradient(x, y, x, canvas.height - 50);
gradient.addColorStop(0, '#3498db');
gradient.addColorStop(1, '#2c3e50');
ctx.fillStyle = gradient;
ctx.fillRect(x, y, barWidth, height);
// 数值标签
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.fillText(value, x + barWidth / 2, y - 10);
});
}
六、Canvas vs SVG vs WebGL
| 特性 | Canvas | SVG | WebGL |
|---|---|---|---|
| 类型 | 位图 | 矢量图 | 3D 位图 |
| 性能 | 高(适合复杂动画) | 中等 | 最高(GPU 加速) |
| 交互 | 需手动计算 | 内置事件 | 需手动计算 |
| 文件大小 | 小 | 大(XML) | 小 |
| 适用场景 | 游戏、动画 | 图标、图表 | 3D 游戏、VR |
选择建议:
- 游戏开发 → Canvas(2D)或 WebGL(3D)
- 数据可视化 → Canvas(大数据量)或 SVG(需要交互)
- UI 组件 → SVG(图标、动画)
七、调试与性能监控
7.1 性能监控
class PerformanceMonitor {
constructor() {
this.fps = 0;
this.frames = 0;
this.lastTime = performance.now();
}
update() {
this.frames++;
const currentTime = performance.now();
if (currentTime - this.lastTime >= 1000) {
this.fps = this.frames;
this.frames = 0;
this.lastTime = currentTime;
}
}
draw(ctx) {
ctx.fillStyle = '#0f0';
ctx.font = '16px Arial';
ctx.fillText(`FPS: ${this.fps}`, 10, 20);
}
}
7.2 常见性能问题
- 过度绘制:避免每帧清除整个画布
- 内存泄漏:及时释放不用的图像资源
- 计算密集:将复杂计算移到 Web Worker
八、总结
Canvas 是前端开发者必须掌握的核心技术之一。通过本文的学习,你已经掌握了:
- Canvas 基础 API:矩形、圆形、线条、文本
- 动画原理:requestAnimationFrame 和游戏循环
- 游戏开发:完整飞机大战的实现
- 性能优化:对象池、离屏渲染、局部更新
- 高级应用:图像处理、物理模拟、数据可视化
下一步学习建议:
- 尝试添加更多游戏功能(道具、关卡、音效)
- 学习 WebGL 进行 3D 开发
- 探索 Three.js、PixiJS 等游戏框架
Canvas 的世界无限广阔,期待你用它创造出更多精彩的作品!
参考资源:
如果你觉得这篇文章有帮助,欢迎点赞收藏!有问题可以在评论区讨论。