pin_drop当前位置:知识文库 ❯ 图文
多媒体:动画 - 完整教程与代码示例
一、教程简介
Canvas 动画是通过在每一帧重新绘制画面来实现的。requestAnimationFrame 是现代浏览器提供的动画 API,它能够在浏览器的下一次重绘之前调用指定的回调函数,从而实现流畅的 60fps 动画效果。本教程将详细讲解 Canvas 动画的基本原理、动画循环的构建、帧率控制以及常见动画效果的实现。
二、核心概念
动画原理
Canvas 动画基于"逐帧渲染"原理:
-
清除画布:擦除上一帧的内容
-
更新状态:计算动画对象的新位置、大小、颜色等
-
重新绘制:根据新状态绘制当前帧
-
请求下一帧:通过 requestAnimationFrame 递归调用
requestAnimationFrame
requestAnimationFrame 相比 setInterval/setTimeout 的优势:
-
与浏览器刷新率同步(通常 60fps)
-
页面不可见时自动暂停,节省资源
-
更平滑的动画效果
-
电池友好
时间差(Delta Time)
基于时间的动画而非基于帧的动画,确保不同帧率下动画速度一致:
代码示例
let lastTime = 0;
function animate(timestamp) {
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
// 使用 deltaTime 计算位移
requestAnimationFrame(animate);
}三、语法与用法
requestAnimationFrame
代码示例
const id = requestAnimationFrame(callback);cancelAnimationFrame
代码示例
cancelAnimationFrame(id);
取消之前通过 requestAnimationFrame 注册的回调。
基本动画循环
代码示例
function animate(timestamp) {
// 1. 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 2. 更新状态
update();
// 3. 绘制
draw();
// 4. 请求下一帧
requestAnimationFrame(animate);
}
// 启动动画
requestAnimationFrame(animate);四、代码示例
示例1:基本动画效果集合
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>基本动画效果集合</title>
<style>
body {
margin: 0;
padding: 20px;
background: #0d1117;
color: #c9d1d9;
font-family: 'Segoe UI', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
}
canvas {
border: 1px solid #30363d;
border-radius: 8px;
background: #161b22;
}
</style>
</head>
<body>
<h1>Canvas 动画效果集合</h1>
<canvas id="myCanvas" width="750" height="450"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
let time = 0;
// === 弹跳球 ===
const ball = {
x: 120, y: 100, vx: 3, vy: 2, r: 15,
color: '#e94560'
};
function updateBall() {
ball.x += ball.vx;
ball.y += ball.vy;
if (ball.x - ball.r < 10 || ball.x + ball.r > 230) ball.vx *= -1;
if (ball.y - ball.r < 50 || ball.y + ball.r > 210) ball.vy *= -1;
}
function drawBall() {
ctx.fillStyle = '#0f172a';
ctx.fillRect(10, 50, 220, 160);
ctx.strokeStyle = '#30363d';
ctx.lineWidth = 1;
ctx.strokeRect(10, 50, 220, 160);
ctx.fillStyle = '#c9d1d9';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('弹跳球', 120, 45);
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fillStyle = ball.color;
ctx.fill();
// 拖尾
ctx.beginPath();
ctx.arc(ball.x - ball.vx * 2, ball.y - ball.vy * 2, ball.r * 0.7, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(233, 69, 96, 0.3)';
ctx.fill();
}
// === 旋转方块 ===
function drawRotatingSquare() {
const cx = 370, cy = 130;
ctx.fillStyle = '#0f172a';
ctx.fillRect(260, 50, 220, 160);
ctx.strokeStyle = '#30363d';
ctx.strokeRect(260, 50, 220, 160);
ctx.fillStyle = '#c9d1d9';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('旋转方块', 370, 45);
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(time * 0.03);
ctx.fillStyle = '#58a6ff';
ctx.fillRect(-30, -30, 60, 60);
ctx.restore();
}
// === 脉冲圆 ===
function drawPulsingCircle() {
const cx = 620, cy = 130;
ctx.fillStyle = '#0f172a';
ctx.fillRect(510, 50, 220, 160);
ctx.strokeStyle = '#30363d';
ctx.strokeRect(510, 50, 220, 160);
ctx.fillStyle = '#c9d1d9';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('脉冲圆', 620, 45);
const pulse = Math.sin(time * 0.05) * 20 + 40;
ctx.beginPath();
ctx.arc(cx, cy, pulse, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(63, 185, 80, 0.3)';
ctx.fill();
ctx.strokeStyle = '#3fb950';
ctx.lineWidth = 2;
ctx.stroke();
}
// === 粒子系统 ===
const particles = [];
for (let i = 0; i < 50; i++) {
particles.push({
x: 375, y: 350,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4,
life: Math.random() * 100,
maxLife: 100 + Math.random() * 50,
size: 2 + Math.random() * 3,
hue: Math.random() * 60 + 200
});
}
function updateParticles() {
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.02; // 重力
p.life++;
if (p.life > p.maxLife) {
p.x = 375;
p.y = 350;
p.vx = (Math.random() - 0.5) * 4;
p.vy = (Math.random() - 0.5) * 4 - 2;
p.life = 0;
}
});
}
function drawParticles() {
ctx.fillStyle = '#0f172a';
ctx.fillRect(10, 230, 730, 210);
ctx.strokeStyle = '#30363d';
ctx.strokeRect(10, 230, 730, 210);
ctx.fillStyle = '#c9d1d9';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('粒子系统', 375, 225);
particles.forEach(p => {
const alpha = 1 - p.life / p.maxLife;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${p.hue}, 70%, 60%, ${alpha})`;
ctx.fill();
});
}
// === 正弦波 ===
function drawSineWave() {
ctx.fillStyle = '#c9d1d9';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('正弦波动画', 375, 225);
ctx.beginPath();
for (let x = 10; x < 740; x++) {
const y = 335 + Math.sin((x + time * 2) * 0.03) * 30
+ Math.sin((x + time * 3) * 0.02) * 15;
if (x === 10) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.strokeStyle = '#d2a8ff';
ctx.lineWidth = 2;
ctx.stroke();
}
// === 主动画循环 ===
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
updateBall();
drawBall();
drawRotatingSquare();
drawPulsingCircle();
updateParticles();
drawParticles();
drawSineWave();
time++;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
</script>
</body>
</html>由于代码较长,完整的示例2(基于时间的动画与帧率控制)请参考原始Markdown文件。
五、浏览器兼容性
六、注意事项与最佳实践
1. 始终使用 requestAnimationFrame
代码示例
// 推荐
function animate() {
// 动画逻辑
requestAnimationFrame(animate);
}
// 不推荐
// setInterval(animate, 16); // 不与浏览器刷新同步2. 基于时间的动画
代码示例
// 推荐:基于时间的动画
function animate(timestamp) {
if (!lastTime) lastTime = timestamp;
const dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
object.x += speed * dt; // 速度 * 时间 = 位移
requestAnimationFrame(animate);
}
// 不推荐:基于帧的动画
// object.x += speed; // 不同帧率下速度不同3. 暂停与恢复
代码示例
let isPaused = false;
let lastTime = 0;
function animate(timestamp) {
if (isPaused) return;
if (!lastTime) lastTime = timestamp;
const dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
update(dt);
draw();
requestAnimationFrame(animate);
}
function pause() {
isPaused = true;
}
function resume() {
isPaused = false;
lastTime = 0; // 重置时间避免跳帧
requestAnimationFrame(animate);
}4. 清除画布的选择
代码示例
// 完全清除(无拖尾)
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 半透明覆盖(拖尾效果)
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);5. 性能优化
-
减少状态切换(fillStyle、font 等)
-
合并相同样式的绘制操作
-
使用离屏 Canvas 缓存静态内容
-
避免在动画循环中创建对象
七、代码规范示例
代码示例
// 推荐:动画类封装
class CanvasAnimation {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.isRunning = false;
this.lastTime = 0;
this.animationId = null;
this.objects = [];
}
start() {
this.isRunning = true;
this.lastTime = 0;
this.animationId = requestAnimationFrame(this._loop.bind(this));
}
stop() {
this.isRunning = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
}
_loop(timestamp) {
if (!this.isRunning) return;
if (!this.lastTime) this.lastTime = timestamp;
const dt = Math.min((timestamp - this.lastTime) / 1000, 0.1); // 限制最大 dt
this.lastTime = timestamp;
this.update(dt);
this.draw();
this.animationId = requestAnimationFrame(this._loop.bind(this));
}
update(dt) {
// 子类重写
}
draw() {
// 子类重写
}
}
// 使用
class MyAnimation extends CanvasAnimation {
update(dt) {
this.objects.forEach(obj => {
obj.x += obj.vx * dt;
obj.y += obj.vy * dt;
});
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.objects.forEach(obj => {
this.ctx.beginPath();
this.ctx.arc(obj.x, obj.y, obj.r, 0, Math.PI * 2);
this.ctx.fill();
});
}
}八、常见问题与解决方案
常见问题
动画在不同设备上速度不同怎么办?
原因是使用基于帧的动画,不同帧率导致速度差异。解决方案是使用基于时间的动画,通过 deltaTime 计算位移,确保不同帧率下动画速度一致。
切换标签页后动画跳帧怎么解决?
原因是 requestAnimationFrame 在标签页不可见时暂停,恢复后 dt 值很大。解决方案是限制最大 dt 值,如 dt = Math.min((timestamp - lastTime) / 1000, 0.1)。
如何实现缓动效果?
可以使用缓动函数:linear(线性)、easeIn(缓入)、easeOut(缓出)、easeInOut(缓入缓出)。通过 progress = easeInOut(elapsed / duration) 计算进度,然后 object.x = startX + (endX - startX) * progress 实现平滑过渡。
如何实现拖尾效果?
有两种方案:方案1是使用半透明覆盖 ctx.fillStyle = 'rgba(0, 0, 0, 0.1)' 代替 clearRect;方案2是记录轨迹点数组,逐帧绘制历史位置并逐渐降低透明度。
如何控制动画帧率?
可以通过计算目标帧间隔来控制帧率。设置 targetFPS = 30,interval = 1000 / targetFPS,在动画循环中判断 if (timestamp - lastFrameTime < interval) return,从而实现降帧效果。
九、总结
Canvas 动画是创建交互式和动态内容的核心技术,关键要点:
-
requestAnimationFrame:浏览器原生动画 API,与刷新率同步
-
动画循环:清除 -> 更新 -> 绘制 -> 请求下一帧
-
基于时间:使用 deltaTime 确保不同帧率下动画速度一致
-
暂停恢复:暂停时取消动画,恢复时重置 lastTime
-
拖尾效果:半透明覆盖或记录轨迹点
-
缓动函数:easeIn、easeOut、easeInOut 实现自然运动
-
性能优化:减少状态切换、合并绘制、使用离屏 Canvas
小贴士
在开发动画时,建议先在低帧率(如 30fps)下调试,确保逻辑正确后再提升到 60fps。使用 Chrome DevTools 的 Performance 面板可以分析动画性能,找出瓶颈。对于复杂动画,考虑使用 WebGL 或 CSS 动画以获得更好的性能。
本文涉及AI创作
内容由AI创作,请仔细甄别