掌握Canvas API,创建令人惊叹的图形、动画和交互式游戏
Canvas 简介
HTML5 Canvas 是一个强大的图形绘制API,允许开发者使用JavaScript在网页上动态生成和操作图形、动画和交互式内容。与SVG不同,Canvas是基于位图的,这意味着它通过像素操作来绘制图形。
Canvas广泛应用于数据可视化、游戏开发、图像处理、广告创意和交互式教育内容等领域。它的性能优势使其成为复杂图形应用的理想选择。
基础用法与绘制
要使用Canvas,首先需要在HTML中创建canvas元素,然后通过JavaScript获取其上下文进行绘制。
创建Canvas元素
<canvas id="myCanvas" width="800" height="600">
您的浏览器不支持Canvas,请升级或更换浏览器。
</canvas>
获取上下文与基本绘制
// 获取Canvas元素和上下文
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 检查浏览器支持
if (!ctx) {
console.error('您的浏览器不支持Canvas');
}
// 绘制矩形
ctx.fillStyle = 'blue'; // 设置填充颜色
ctx.fillRect(50, 50, 200, 100); // 绘制填充矩形
ctx.strokeStyle = 'red'; // 设置描边颜色
ctx.lineWidth = 3; // 设置线宽
ctx.strokeRect(300, 50, 200, 100); // 绘制描边矩形
// 清除部分区域
ctx.clearRect(100, 75, 100, 50);
颜色与样式
// 使用颜色名称
ctx.fillStyle = 'red';
// 使用十六进制值
ctx.fillStyle = '#ff0000';
// 使用RGB
ctx.fillStyle = 'rgb(255, 0, 0)';
// 使用RGBA(带透明度)
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
// 使用渐变色
const gradient = ctx.createLinearGradient(0, 0, 200, 0);
gradient.addColorStop(0, 'red');
gradient.addColorStop(1, 'blue');
ctx.fillStyle = gradient;
ctx.fillRect(50, 50, 200, 100);
形状与路径绘制
Canvas提供了丰富的路径API,可以绘制各种复杂形状。
基本路径绘制
// 开始新路径
ctx.beginPath();
// 移动到起点
ctx.moveTo(50, 50);
// 绘制直线
ctx.lineTo(150, 50);
ctx.lineTo(100, 150);
// 闭合路径(可选)
ctx.closePath();
// 设置样式并绘制
ctx.fillStyle = 'green';
ctx.fill();
ctx.strokeStyle = 'darkgreen';
ctx.lineWidth = 2;
ctx.stroke();
绘制圆形与弧形
// 绘制完整圆形
ctx.beginPath();
ctx.arc(200, 200, 50, 0, Math.PI * 2);
ctx.fillStyle = 'orange';
ctx.fill();
// 绘制半圆
ctx.beginPath();
ctx.arc(350, 200, 50, 0, Math.PI);
ctx.fillStyle = 'purple';
ctx.fill();
// 绘制扇形
ctx.beginPath();
ctx.moveTo(500, 200);
ctx.arc(500, 200, 50, 0, Math.PI / 2);
ctx.closePath();
ctx.fillStyle = 'teal';
ctx.fill();
贝塞尔曲线
// 二次贝塞尔曲线
ctx.beginPath();
ctx.moveTo(50, 300);
ctx.quadraticCurveTo(150, 250, 250, 300);
ctx.strokeStyle = 'blue';
ctx.stroke();
// 三次贝塞尔曲线
ctx.beginPath();
ctx.moveTo(300, 300);
ctx.bezierCurveTo(350, 250, 450, 350, 500, 300);
ctx.strokeStyle = 'red';
ctx.stroke();
文本与图像处理
Canvas不仅可以绘制形状,还可以渲染文本和处理图像。
文本渲染
// 设置文本样式
ctx.font = '30px Arial';
ctx.fillStyle = 'navy';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 绘制填充文本
ctx.fillText('Hello Canvas', 400, 100);
// 绘制描边文本
ctx.strokeStyle = 'darkred';
ctx.lineWidth = 1;
ctx.strokeText('Hello Canvas', 400, 150);
// 测量文本宽度
const text = 'Hello Canvas';
const metrics = ctx.measureText(text);
console.log(`文本宽度: ${metrics.width}px`);
图像绘制与处理
// 创建图像对象
const img = new Image();
// 设置加载完成后的回调
img.onload = function() {
// 绘制原始图像
ctx.drawImage(img, 50, 300, 100, 100);
// 绘制裁剪后的图像
ctx.drawImage(
img,
50, 50, 100, 100, // 源图像裁剪区域
200, 300, 150, 150 // 目标绘制区域
);
};
// 设置图像源(必须在onload后设置)
img.src = 'image.jpg';
像素操作
// 获取像素数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// 修改像素数据(创建灰度效果)
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // 红色通道
data[i + 1] = avg; // 绿色通道
data[i + 2] = avg; // 蓝色通道
}
// 将修改后的数据放回Canvas
ctx.putImageData(imageData, 0, 0);
动画实现技术
Canvas动画基于连续重绘原理,通常使用requestAnimationFrame实现。
基本动画循环
let x = 50;
let speed = 2;
function animate() {
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新位置
x += speed;
// 边界检测
if (x > canvas.width - 50 || x < 50) {
speed = -speed;
}
// 绘制图形
ctx.beginPath();
ctx.arc(x, 100, 50, 0, Math.PI * 2);
ctx.fillStyle = 'coral';
ctx.fill();
// 继续动画循环
requestAnimationFrame(animate);
}
// 启动动画
animate();
复杂动画示例
const particles = [];
// 创建粒子
function createParticles(x, y, count) {
for (let i = 0; i < count; i++) {
particles.push({
x: x,
y: y,
size: Math.random() * 5 + 1,
speedX: Math.random() * 6 - 3,
speedY: Math.random() * 6 - 3,
color: `hsl(${Math.random() * 360}, 100%, 50%)`
});
}
}
// 粒子动画
function animateParticles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
// 更新位置
p.x += p.speedX;
p.y += p.speedY;
// 缩小粒子
p.size -= 0.1;
// 绘制粒子
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = p.color;
ctx.fill();
// 移除消失的粒子
if (p.size <= 0.2) {
particles.splice(i, 1);
i--;
}
}
// 添加新粒子
if (particles.length < 100) {
createParticles(canvas.width / 2, canvas.height / 2, 5);
}
requestAnimationFrame(animateParticles);
}
// 启动粒子动画
animateParticles();
交互与事件处理
Canvas本身不支持内部元素的事件监听,但可以通过数学计算实现交互效果。
鼠标交互
// 存储交互对象
const circles = [
{ x: 100, y: 100, radius: 30, color: 'red' },
{ x: 200, y: 200, radius: 40, color: 'blue' },
{ x: 300, y: 300, radius: 50, color: 'green' }
];
// 绘制所有圆形
function drawCircles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
circles.forEach(circle => {
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
ctx.fillStyle = circle.color;
ctx.fill();
});
}
// 检测鼠标点击
canvas.addEventListener('click', function(event) {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
// 检查是否点击了某个圆形
for (let i = 0; i < circles.length; i++) {
const circle = circles[i];
const distance = Math.sqrt(
Math.pow(mouseX - circle.x, 2) +
Math.pow(mouseY - circle.y, 2)
);
if (distance <= circle.radius) {
console.log(`点击了 ${circle.color} 圆形`);
circle.color = `hsl(${Math.random() * 360}, 100%, 50%)`;
drawCircles();
break;
}
}
});
// 初始绘制
drawCircles();
鼠标移动交互
let hoveredCircle = null;
canvas.addEventListener('mousemove', function(event) {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
// 检查鼠标是否在某个圆形上
let found = false;
for (let i = 0; i < circles.length; i++) {
const circle = circles[i];
const distance = Math.sqrt(
Math.pow(mouseX - circle.x, 2) +
Math.pow(mouseY - circle.y, 2)
);
if (distance <= circle.radius) {
document.body.style.cursor = 'pointer';
found = true;
// 高亮显示
if (hoveredCircle !== circle) {
hoveredCircle = circle;
drawCircles();
// 绘制高亮效果
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius + 5, 0, Math.PI * 2);
ctx.strokeStyle = 'yellow';
ctx.lineWidth = 3;
ctx.stroke();
}
break;
}
}
if (!found) {
document.body.style.cursor = 'default';
if (hoveredCircle) {
hoveredCircle = null;
drawCircles();
}
}
});
实战案例:太空射击游戏
下面通过一个完整的太空射击游戏示例展示Canvas的强大功能:
// 游戏主类
class SpaceShooter {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.canvas.width = 800;
this.canvas.height = 600;
this.player = {
x: this.canvas.width / 2,
y: this.canvas.height - 50,
width: 50,
height: 50,
speed: 5,
color: 'blue'
};
this.bullets = [];
this.enemies = [];
this.score = 0;
this.gameOver = false;
this.keys = {};
// 设置事件监听
this.setupEventListeners();
// 启动游戏
this.gameLoop();
this.spawnEnemies();
}
setupEventListeners() {
window.addEventListener('keydown', (e) => {
this.keys[e.key] = true;
// 发射子弹
if (e.key === ' ' && !this.gameOver) {
this.bullets.push({
x: this.player.x,
y: this.player.y,
width: 5,
height: 15,
speed: 10,
color: 'yellow'
});
}
// 重新开始游戏
if (e.key === 'r' && this.gameOver) {
this.resetGame();
}
});
window.addEventListener('keyup', (e) => {
this.keys[e.key] = false;
});
}
resetGame() {
this.player.x = this.canvas.width / 2;
this.bullets = [];
this.enemies = [];
this.score = 0;
this.gameOver = false;
this.gameLoop();
this.spawnEnemies();
}
spawnEnemies() {
if (this.gameOver) return;
// 创建敌人
this.enemies.push({
x: Math.random() * (this.canvas.width - 30),
y: -30,
width: 30,
height: 30,
speed: 2 + Math.random() * 2,
color: `hsl(${Math.random() * 360}, 100%, 50%)`
});
// 设置下一次生成时间
setTimeout(() => this.spawnEnemies(), 1000 - Math.min(this.score, 800));
}
update() {
// 移动玩家
if (this.keys['ArrowLeft'] && this.player.x > 0) {
this.player.x -= this.player.speed;
}
if (this.keys['ArrowRight'] && this.player.x = 0; i--) {
this.bullets[i].y -= this.bullets[i].speed;
// 移除超出屏幕的子弹
if (this.bullets[i].y = 0; i--) {
this.enemies[i].y += this.enemies[i].speed;
// 检查敌人是否超出屏幕
if (this.enemies[i].y > this.canvas.height) {
this.enemies.splice(i, 1);
this.score -= 5; // 扣分
}
// 检查敌人与玩家碰撞
if (this.checkCollision(this.player, this.enemies[i])) {
this.gameOver = true;
return;
}
// 检查子弹与敌人碰撞
for (let j = this.bullets.length - 1; j >= 0; j--) {
if (this.checkCollision(this.bullets[j], this.enemies[i])) {
this.enemies.splice(i, 1);
this.bullets.splice(j, 1);
this.score += 10;
break;
}
}
}
}
checkCollision(obj1, obj2) {
return obj1.x obj2.x &&
obj1.y obj2.y;
}
draw() {
// 清除画布
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制玩家
this.ctx.fillStyle = this.player.color;
this.ctx.fillRect(this.player.x, this.player.y, this.player.width, this.player.height);
// 绘制子弹
this.bullets.forEach(bullet => {
this.ctx.fillStyle = bullet.color;
this.ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
});
// 绘制敌人
this.enemies.forEach(enemy => {
this.ctx.fillStyle = enemy.color;
this.ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
});
// 绘制分数
this.ctx.fillStyle = 'white';
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 = 'white';
this.ctx.font = '40px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText('游戏结束', this.canvas.width / 2, this.canvas.height / 2 - 40);
this.ctx.font = '20px Arial';
this.ctx.fillText(`最终分数: ${this.score}`, this.canvas.width / 2, this.canvas.height / 2);
this.ctx.fillText('按 R 键重新开始', this.canvas.width / 2, this.canvas.height / 2 + 40);
this.ctx.textAlign = 'left';
}
}
gameLoop() {
if (this.gameOver) return;
this.update();
this.draw();
requestAnimationFrame(() => this.gameLoop());
}
}
// 启动游戏
const game = new SpaceShooter();
HTML结构
<!DOCTYPE html>
<html>
<head>
<title>太空射击游戏</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
canvas { display: block; margin: 0 auto; background: #001133; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>
// 上面的游戏代码放在这里
</script>
</body>
</html>
性能优化与最佳实践
为了提高Canvas应用的性能,可以遵循以下优化策略:
1. 减少重绘区域
// 只清除变化的部分,而不是整个画布
function optimizedDraw() {
// 清除需要更新的区域,而不是整个画布
ctx.clearRect(prevX - 5, prevY - 5, 60, 60);
ctx.clearRect(currentX - 5, currentY - 5, 60, 60);
// 绘制新内容
ctx.fillRect(currentX, currentY, 50, 50);
// 保存当前位置
prevX = currentX;
prevY = currentY;
}
2. 使用离屏Canvas
// 创建离屏Canvas
const offscreenCanvas = document.createElement('canvas');
const offscreenCtx = offscreenCanvas.getContext('2d');
offscreenCanvas.width = 100;
offscreenCanvas.height = 100;
// 在离屏Canvas上绘制复杂图形
offscreenCtx.beginPath();
offscreenCtx.arc(50, 50, 50, 0, Math.PI * 2);
offscreenCtx.fillStyle = 'red';
offscreenCtx.fill();
// 在主Canvas上重复使用
function drawMultiple() {
for (let i = 0; i < 10; i++) {
ctx.drawImage(offscreenCanvas, i * 60, 100);
}
}
3. 避免浮点数坐标
// 使用整数坐标可以提高性能
ctx.drawImage(image, Math.floor(x), Math.floor(y));
4. 合理使用图层
// 使用多个Canvas叠加实现图层效果
<div style="position: relative;">
<canvas id="bgCanvas" style="position: absolute; z-index: 1;"></canvas>
<canvas id="gameCanvas" style="position: absolute; z-index: 2;"></canvas>
<canvas id="uiCanvas" style="position: absolute; z-index: 3;"></canvas>
</div>
5. 使用WebGL进行3D渲染
// 获取WebGL上下文
const glCanvas = document.getElementById('glCanvas');
const gl = glCanvas.getContext('webgl') || glCanvas.getContext('experimental-webgl');
if (gl) {
// 使用WebGL进行高性能3D渲染
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
} else {
console.error('WebGL不支持');
}