1. 项目概述:用Canvas实现会转的大风车
这个项目非常适合刚接触Canvas绘图的前端开发者。通过实现一个会旋转的大风车,可以快速掌握Canvas的基础绘图API和动画原理。我选择这个案例教学是因为它涵盖了从基础图形绘制到动画实现的完整流程,而且效果直观有趣。
大风车看似简单,但实现过程中会涉及Canvas坐标系、路径绘制、旋转变换、动画帧控制等核心知识点。完成这个项目后,你将掌握:
- 使用Canvas绘制基本图形
- 理解Canvas坐标系和变换
- 实现平滑动画效果
- 优化动画性能的技巧
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心实现步骤
2.1 初始化Canvas画布
首先需要在HTML中创建Canvas元素:
html复制<canvas id="windmillCanvas" width="400" height="400"></canvas>
然后在JavaScript中获取Canvas上下文:
javascript复制const canvas = document.getElementById('windmillCanvas');
const ctx = canvas.getContext('2d');
这里有几个关键点需要注意:
- Canvas的width和height属性应该通过HTML属性设置,而不是CSS
- 获取2D上下文是绘图的前提
- 画布大小建议设置为偶数,避免某些浏览器出现模糊问题
2.2 绘制风车底座
我们先从静态部分开始,绘制风车的底座:
javascript复制function drawBase() {
ctx.fillStyle = '#8B4513'; // 棕色
ctx.beginPath();
ctx.moveTo(180, 200);
ctx.lineTo(220, 200);
ctx.lineTo(210, 400);
ctx.lineTo(190, 400);
ctx.closePath();
ctx.fill();
}
这个梯形底座使用了Canvas的路径绘制API:
beginPath()开始新路径moveTo()和lineTo()定义路径点closePath()闭合路径fill()填充路径
2.3 绘制风车叶片
风车叶片是旋转的主体部分,我们需要先定义一个绘制单个叶片的函数:
javascript复制function drawBlade(rotation) {
ctx.save();
ctx.translate(200, 200); // 移动到中心点
ctx.rotate(rotation); // 应用旋转
ctx.fillStyle = '#FF6347'; // 番茄色
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(50, 10);
ctx.lineTo(100, 0);
ctx.lineTo(50, -40);
ctx.closePath();
ctx.fill();
ctx.restore();
}
这里有几个重要技巧:
- 使用
save()和restore()保存和恢复绘图状态 translate()将原点移动到旋转中心rotate()实现旋转效果- 旋转角度通过参数传入,便于动画控制
2.4 绘制完整风车
现在我们可以组合这些函数来绘制完整风车:
javascript复制function drawWindmill(rotation) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBase();
// 绘制4个叶片,每个间隔90度
for(let i = 0; i < 4; i++) {
drawBlade(rotation + i * Math.PI/2);
}
}
关键点:
- 每次绘制前清除画布
- 4个叶片均匀分布,间隔90度(π/2弧度)
- 共用一个旋转角度参数,保持同步旋转
3. 实现动画效果
3.1 使用requestAnimationFrame
要实现平滑的旋转动画,我们使用requestAnimationFrame:
javascript复制let angle = 0;
function animate() {
angle += 0.02; // 控制旋转速度
drawWindmill(angle);
requestAnimationFrame(animate);
}
animate();
requestAnimationFrame的优势:
- 与浏览器刷新率同步,通常60fps
- 页面不可见时会自动暂停,节省资源
- 比setInterval/setTimeout更平滑高效
3.2 控制动画速度
动画速度可以通过调整角度增量来控制:
javascript复制const speed = 0.02; // 速度系数
angle += speed;
如果需要根据时间计算角度,实现恒定转速:
javascript复制let lastTime = 0;
const rpm = 10; // 每分钟转数
function animate(timestamp) {
if(!lastTime) lastTime = timestamp;
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
angle += (rpm / 60) * Math.PI * 2 * deltaTime / 1000;
drawWindmill(angle);
requestAnimationFrame(animate);
}
这种方法可以确保在不同刷新率的设备上保持相同的实际转速。
4. 性能优化技巧
4.1 减少绘制区域
对于复杂场景,可以只重绘发生变化的部分:
javascript复制ctx.clearRect(0, 0, canvas.width, 200); // 只清除上半部分
4.2 使用离屏Canvas
对于复杂的静态背景,可以预先绘制到离屏Canvas:
javascript复制const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
const offscreenCtx = offscreenCanvas.getContext('2d');
// 预先绘制静态部分
function initBackground() {
offscreenCtx.fillStyle = '#87CEEB';
offscreenCtx.fillRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
// 绘制其他静态元素...
}
// 在动画循环中
ctx.drawImage(offscreenCanvas, 0, 0);
4.3 避免频繁的样式更改
将相同样式的绘制操作集中在一起,减少状态切换:
javascript复制// 不好的做法
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 50, 50);
ctx.fillStyle = 'blue';
ctx.fillRect(70, 10, 50, 50);
ctx.fillStyle = 'red';
ctx.fillRect(130, 10, 50, 50);
// 好的做法
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 50, 50);
ctx.fillRect(130, 10, 50, 50);
ctx.fillStyle = 'blue';
ctx.fillRect(70, 10, 50, 50);
5. 常见问题与解决方案
5.1 动画卡顿或闪烁
可能原因及解决方法:
- 确保在修改DOM后调用
requestAnimationFrame - 检查是否有其他高负载任务阻塞主线程
- 简化绘制操作或使用离屏缓冲
5.2 图形边缘模糊
解决方法:
- 确保Canvas的width/height属性与CSS大小匹配
- 坐标使用整数值,避免亚像素渲染
- 对于直线,将坐标偏移0.5像素:
javascript复制ctx.moveTo(100.5, 200.5);
5.3 移动端性能问题
优化建议:
- 减少同时动画的元素数量
- 降低帧率到30fps
- 使用CSS transform代替Canvas动画(适用于简单场景)
6. 扩展功能实现
6.1 添加交互控制
让用户控制风车转速:
javascript复制let speed = 0.02;
document.addEventListener('keydown', (e) => {
if(e.key === 'ArrowUp') speed += 0.005;
if(e.key === 'ArrowDown') speed -= 0.005;
});
6.2 响应式设计
使Canvas适应不同屏幕大小:
javascript复制function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
drawWindmill(angle); // 重绘
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
6.3 添加视觉效果
实现叶片阴影和渐变:
javascript复制function drawBlade(rotation) {
ctx.save();
ctx.translate(200, 200);
ctx.rotate(rotation);
// 添加阴影
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 5;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 3;
// 使用渐变填充
const gradient = ctx.createLinearGradient(0, -40, 100, 0);
gradient.addColorStop(0, '#FF4500');
gradient.addColorStop(1, '#FF6347');
ctx.fillStyle = gradient;
// 绘制叶片路径...
ctx.restore();
}
7. 完整代码示例
以下是完整的实现代码:
html复制<!DOCTYPE html>
<html>
<head>
<title>Canvas风车动画</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="windmillCanvas"></canvas>
<script>
const canvas = document.getElementById('windmillCanvas');
const ctx = canvas.getContext('2d');
let angle = 0;
let speed = 0.02;
function resizeCanvas() {
const size = Math.min(window.innerWidth, window.innerHeight) * 0.8;
canvas.width = size;
canvas.height = size;
}
function drawBase() {
const centerX = canvas.width / 2;
ctx.fillStyle = '#8B4513';
ctx.beginPath();
ctx.moveTo(centerX - 20, centerX);
ctx.lineTo(centerX + 20, centerX);
ctx.lineTo(centerX + 10, canvas.height);
ctx.lineTo(centerX - 10, canvas.height);
ctx.closePath();
ctx.fill();
}
function drawBlade(rotation) {
const centerX = canvas.width / 2;
ctx.save();
ctx.translate(centerX, centerX);
ctx.rotate(rotation);
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 5;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 3;
const gradient = ctx.createLinearGradient(0, -40, 100, 0);
gradient.addColorStop(0, '#FF4500');
gradient.addColorStop(1, '#FF6347');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(50, 10);
ctx.lineTo(100, 0);
ctx.lineTo(50, -40);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function drawWindmill(rotation) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制天空背景
const skyGradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
skyGradient.addColorStop(0, '#87CEEB');
skyGradient.addColorStop(1, '#E0F7FA');
ctx.fillStyle = skyGradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawBase();
for(let i = 0; i < 4; i++) {
drawBlade(rotation + i * Math.PI/2);
}
}
function animate(timestamp) {
angle += speed;
drawWindmill(angle);
requestAnimationFrame(animate);
}
// 交互控制
document.addEventListener('keydown', (e) => {
if(e.key === 'ArrowUp') speed = Math.min(speed + 0.005, 0.1);
if(e.key === 'ArrowDown') speed = Math.max(speed - 0.005, 0);
if(e.key === ' ') speed = 0; // 空格键暂停
});
// 触摸控制
canvas.addEventListener('touchstart', () => {
speed = speed > 0 ? -0.03 : 0.03;
});
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
animate();
</script>
</body>
</html>
这个完整示例包含了响应式设计、交互控制和视觉效果增强,可以直接保存为HTML文件运行。
