第9章:动画与帧循环
9.1 章节概述
前面的章节中,我们学习了如何绑制静态图形。但 Canvas 真正的魅力在于动画——让图形动起来,创建交互式体验。从简单的移动、旋转,到复杂的物理模拟、粒子系统,动画是 Canvas 应用的核心。
本章将深入讲解:
- 动画原理:帧、刷新率、视觉暂留
- requestAnimationFrame:高效的动画循环
- 时间控制:帧率无关的动画
- 缓动函数:自然流畅的运动效果
- 动画管理:多对象动画系统
- 实战应用:粒子系统、物理动画
9.2 动画基础原理
9.2.1 什么是动画?
动画的本质是快速连续显示略有不同的静态图像,利用人眼的"视觉暂留"效应产生运动的错觉。
帧 1 帧 2 帧 3 帧 4
● ● ● ●
→ →→ →→→
当这些帧快速连续播放时,看起来圆在移动9.2.2 关键概念
| 概念 | 说明 |
|---|---|
| 帧(Frame) | 单张静态画面 |
| 帧率(FPS) | 每秒显示的帧数 |
| 刷新率 | 显示器每秒刷新次数(通常 60Hz) |
| 渲染循环 | 不断重复的 清空→更新→绘制 过程 |
9.2.3 帧率与流畅度
| 帧率 | 体验 |
|---|---|
| 60 FPS | 非常流畅(目标值) |
| 30 FPS | 可接受,略有卡顿 |
| 24 FPS | 电影标准,明显帧感 |
| < 20 FPS | 明显卡顿 |
9.2.4 基本动画循环
javascript
// 动画的基本结构
function animate() {
// 1. 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 2. 更新状态(位置、角度等)
update();
// 3. 绘制
draw();
// 4. 请求下一帧
requestAnimationFrame(animate);
}
// 启动动画
animate();9.3 requestAnimationFrame
9.3.1 为什么使用 requestAnimationFrame?
不推荐使用 setInterval/setTimeout:
javascript
// ❌ 不推荐
setInterval(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
update();
draw();
}, 1000 / 60); // 尝试 60fps
// 问题:
// 1. 不与显示器刷新同步,可能导致画面撕裂
// 2. 浏览器标签不可见时仍然运行,浪费资源
// 3. 无法保证精确的时间间隔requestAnimationFrame 的优势:
javascript
// ✅ 推荐
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
update();
draw();
requestAnimationFrame(animate);
}
animate();
// 优势:
// 1. 与显示器刷新同步(通常 60fps)
// 2. 标签不可见时自动暂停
// 3. 浏览器优化,省电节能
// 4. 提供高精度时间戳9.3.2 基本用法
javascript
// 基础用法
let animationId;
function animate() {
// 动画逻辑...
animationId = requestAnimationFrame(animate);
}
// 启动
animationId = requestAnimationFrame(animate);
// 停止
cancelAnimationFrame(animationId);9.3.3 使用时间戳
requestAnimationFrame 会传入一个高精度时间戳:
javascript
function animate(timestamp) {
// timestamp 是从页面加载开始的毫秒数
console.log(`当前时间: ${timestamp}ms`);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);9.3.4 计算实际帧率
javascript
let lastTime = 0;
let frameCount = 0;
let fps = 0;
function animate(timestamp) {
// 计算与上一帧的时间差
const deltaTime = timestamp - lastTime;
frameCount++;
// 每秒更新一次 FPS 显示
if (timestamp - lastFpsUpdate >= 1000) {
fps = frameCount;
frameCount = 0;
lastFpsUpdate = timestamp;
console.log(`FPS: ${fps}`);
}
lastTime = timestamp;
requestAnimationFrame(animate);
}
let lastFpsUpdate = 0;
requestAnimationFrame(animate);9.4 帧率无关的动画
9.4.1 问题:帧率依赖
javascript
// ❌ 帧率依赖的动画
let x = 0;
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
x += 5; // 每帧移动 5 像素
ctx.fillRect(x, 100, 50, 50);
requestAnimationFrame(animate);
}
// 问题:
// - 60fps 时:每秒移动 300 像素
// - 30fps 时:每秒移动 150 像素
// 不同设备上速度不一致!9.4.2 解决方案:基于时间的动画
javascript
// ✅ 帧率无关的动画
let x = 0;
const speed = 200; // 每秒 200 像素
let lastTime = 0;
function animate(timestamp) {
// 计算时间增量(秒)
const deltaTime = (timestamp - lastTime) / 1000;
lastTime = timestamp;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 基于时间计算位移
x += speed * deltaTime; // 速度 × 时间 = 位移
ctx.fillRect(x, 100, 50, 50);
requestAnimationFrame(animate);
}
requestAnimationFrame((timestamp) => {
lastTime = timestamp;
requestAnimationFrame(animate);
});9.4.3 封装动画循环
javascript
/**
* 动画循环管理器
*/
class AnimationLoop {
constructor(updateCallback) {
this.updateCallback = updateCallback;
this.lastTime = 0;
this.isRunning = false;
this.animationId = null;
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.lastTime = performance.now();
this.loop(this.lastTime);
}
stop() {
if (!this.isRunning) return;
this.isRunning = false;
cancelAnimationFrame(this.animationId);
}
loop(timestamp) {
if (!this.isRunning) return;
const deltaTime = (timestamp - this.lastTime) / 1000;
this.lastTime = timestamp;
// 限制 deltaTime 防止长时间暂停后的跳跃
const clampedDelta = Math.min(deltaTime, 0.1);
this.updateCallback(clampedDelta, timestamp);
this.animationId = requestAnimationFrame(this.loop.bind(this));
}
}
// 使用
const loop = new AnimationLoop((deltaTime, timestamp) => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新和绘制...
x += speed * deltaTime;
ctx.fillRect(x, 100, 50, 50);
});
loop.start();
// loop.stop();9.5 缓动函数(Easing)
9.5.1 什么是缓动?
缓动使动画更加自然,而不是匀速运动。
匀速运动(线性):
时间 → 0% 25% 50% 75% 100%
位置 → 0% 25% 50% 75% 100%
感觉:机械、不自然
缓入缓出:
时间 → 0% 25% 50% 75% 100%
位置 → 0% 10% 50% 90% 100%
感觉:自然、舒适9.5.2 常用缓动函数
javascript
/**
* 缓动函数集合
* 参数 t: 0 到 1 之间的进度值
* 返回值: 0 到 1 之间的缓动后的值
*/
const Easing = {
// 线性(无缓动)
linear: t => t,
// 缓入(慢开始)
easeInQuad: t => t * t,
easeInCubic: t => t * t * t,
easeInQuart: t => t * t * t * t,
// 缓出(慢结束)
easeOutQuad: t => t * (2 - t),
easeOutCubic: t => 1 - Math.pow(1 - t, 3),
easeOutQuart: t => 1 - Math.pow(1 - t, 4),
// 缓入缓出
easeInOutQuad: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
// 弹性
easeOutElastic: t => {
if (t === 0 || t === 1) return t;
return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI) / 3) + 1;
},
// 回弹
easeOutBounce: t => {
if (t < 1 / 2.75) {
return 7.5625 * t * t;
} else if (t < 2 / 2.75) {
return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75;
} else if (t < 2.5 / 2.75) {
return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375;
} else {
return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375;
}
},
// 过冲
easeOutBack: t => {
const c1 = 1.70158;
const c3 = c1 + 1;
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
}
};9.5.3 使用缓动函数
javascript
/**
* 缓动动画类
*/
class TweenAnimation {
constructor(options) {
this.from = options.from;
this.to = options.to;
this.duration = options.duration || 1000;
this.easing = options.easing || Easing.linear;
this.onUpdate = options.onUpdate;
this.onComplete = options.onComplete;
this.startTime = null;
this.isRunning = false;
}
start() {
this.startTime = performance.now();
this.isRunning = true;
this.animate();
}
animate() {
if (!this.isRunning) return;
const now = performance.now();
const elapsed = now - this.startTime;
const progress = Math.min(elapsed / this.duration, 1);
// 应用缓动
const easedProgress = this.easing(progress);
// 计算当前值
const currentValue = this.from + (this.to - this.from) * easedProgress;
this.onUpdate?.(currentValue, progress);
if (progress < 1) {
requestAnimationFrame(() => this.animate());
} else {
this.isRunning = false;
this.onComplete?.();
}
}
stop() {
this.isRunning = false;
}
}
// 使用
let ballX = 50;
const tween = new TweenAnimation({
from: 50,
to: 500,
duration: 2000,
easing: Easing.easeOutBounce,
onUpdate: (value) => {
ballX = value;
// 重绘
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(ballX, 200, 30, 0, Math.PI * 2);
ctx.fillStyle = '#4D7CFF';
ctx.fill();
},
onComplete: () => {
console.log('动画完成!');
}
});
tween.start();9.5.4 缓动函数可视化
javascript
/**
* 可视化缓动函数曲线
*/
function visualizeEasing(ctx, easingFunc, x, y, width, height) {
ctx.save();
ctx.translate(x, y);
// 绘制边框
ctx.strokeStyle = '#ddd';
ctx.strokeRect(0, 0, width, height);
// 绘制对角线(线性参考)
ctx.strokeStyle = '#ccc';
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(0, height);
ctx.lineTo(width, 0);
ctx.stroke();
ctx.setLineDash([]);
// 绘制缓动曲线
ctx.strokeStyle = '#4D7CFF';
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i <= width; i++) {
const t = i / width;
const eased = easingFunc(t);
const px = i;
const py = height - eased * height;
if (i === 0) {
ctx.moveTo(px, py);
} else {
ctx.lineTo(px, py);
}
}
ctx.stroke();
ctx.restore();
}
// 绘制多个缓动函数对比
const easings = [
{ name: 'linear', func: Easing.linear },
{ name: 'easeInQuad', func: Easing.easeInQuad },
{ name: 'easeOutQuad', func: Easing.easeOutQuad },
{ name: 'easeInOutQuad', func: Easing.easeInOutQuad },
{ name: 'easeOutElastic', func: Easing.easeOutElastic },
{ name: 'easeOutBounce', func: Easing.easeOutBounce }
];
easings.forEach((easing, index) => {
const col = index % 3;
const row = Math.floor(index / 3);
const x = 50 + col * 200;
const y = 50 + row * 180;
visualizeEasing(ctx, easing.func, x, y, 150, 120);
ctx.fillStyle = '#333';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(easing.name, x + 75, y + 140);
});9.6 多对象动画管理
9.6.1 动画对象基类
javascript
/**
* 可动画对象基类
*/
class AnimatedObject {
constructor(x, y) {
this.x = x;
this.y = y;
this.vx = 0; // X 方向速度
this.vy = 0; // Y 方向速度
this.rotation = 0;
this.rotationSpeed = 0;
this.scale = 1;
this.alpha = 1;
this.isAlive = true;
}
update(deltaTime) {
// 更新位置
this.x += this.vx * deltaTime;
this.y += this.vy * deltaTime;
// 更新旋转
this.rotation += this.rotationSpeed * deltaTime;
}
draw(ctx) {
// 子类实现
}
}9.6.2 动画管理器
javascript
/**
* 动画场景管理器
*/
class AnimationScene {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.objects = [];
this.isRunning = false;
this.lastTime = 0;
}
add(object) {
this.objects.push(object);
}
remove(object) {
const index = this.objects.indexOf(object);
if (index > -1) {
this.objects.splice(index, 1);
}
}
start() {
this.isRunning = true;
this.lastTime = performance.now();
this.loop();
}
stop() {
this.isRunning = false;
}
loop() {
if (!this.isRunning) return;
const now = performance.now();
const deltaTime = Math.min((now - this.lastTime) / 1000, 0.1);
this.lastTime = now;
// 清空画布
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 更新和绘制所有对象
this.objects.forEach(obj => {
if (obj.isAlive) {
obj.update(deltaTime);
obj.draw(this.ctx);
}
});
// 移除死亡对象
this.objects = this.objects.filter(obj => obj.isAlive);
requestAnimationFrame(() => this.loop());
}
}9.6.3 使用示例
javascript
// 弹跳球
class BouncingBall extends AnimatedObject {
constructor(x, y, radius, color) {
super(x, y);
this.radius = radius;
this.color = color;
this.vx = (Math.random() - 0.5) * 400;
this.vy = (Math.random() - 0.5) * 400;
}
update(deltaTime) {
super.update(deltaTime);
// 边界碰撞
if (this.x - this.radius < 0 || this.x + this.radius > canvas.width) {
this.vx *= -1;
this.x = Math.max(this.radius, Math.min(canvas.width - this.radius, this.x));
}
if (this.y - this.radius < 0 || this.y + this.radius > canvas.height) {
this.vy *= -1;
this.y = Math.max(this.radius, Math.min(canvas.height - this.radius, this.y));
}
}
draw(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
}
}
// 创建场景
const scene = new AnimationScene(canvas);
// 添加多个球
for (let i = 0; i < 10; i++) {
const ball = new BouncingBall(
Math.random() * canvas.width,
Math.random() * canvas.height,
20 + Math.random() * 30,
`hsl(${Math.random() * 360}, 70%, 50%)`
);
scene.add(ball);
}
scene.start();9.7 粒子系统
9.7.1 粒子类
javascript
/**
* 粒子
*/
class Particle extends AnimatedObject {
constructor(x, y, options = {}) {
super(x, y);
// 随机速度
const angle = options.angle ?? Math.random() * Math.PI * 2;
const speed = options.speed ?? (50 + Math.random() * 100);
this.vx = Math.cos(angle) * speed;
this.vy = Math.sin(angle) * speed;
// 属性
this.size = options.size ?? (2 + Math.random() * 4);
this.color = options.color ?? '#fff';
this.life = options.life ?? (0.5 + Math.random() * 1.5);
this.maxLife = this.life;
// 物理
this.gravity = options.gravity ?? 0;
this.friction = options.friction ?? 1;
}
update(deltaTime) {
// 应用重力
this.vy += this.gravity * deltaTime;
// 应用摩擦力
this.vx *= this.friction;
this.vy *= this.friction;
super.update(deltaTime);
// 生命衰减
this.life -= deltaTime;
if (this.life <= 0) {
this.isAlive = false;
}
// 根据生命计算透明度
this.alpha = this.life / this.maxLife;
}
draw(ctx) {
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.restore();
}
}9.7.2 粒子发射器
javascript
/**
* 粒子发射器
*/
class ParticleEmitter {
constructor(x, y, options = {}) {
this.x = x;
this.y = y;
this.particles = [];
// 发射配置
this.emitRate = options.emitRate ?? 50; // 每秒发射数量
this.emitAccumulator = 0;
// 粒子配置
this.particleOptions = options.particleOptions ?? {};
this.isEmitting = true;
}
update(deltaTime) {
// 发射新粒子
if (this.isEmitting) {
this.emitAccumulator += this.emitRate * deltaTime;
while (this.emitAccumulator >= 1) {
this.emit();
this.emitAccumulator--;
}
}
// 更新所有粒子
this.particles.forEach(p => p.update(deltaTime));
// 移除死亡粒子
this.particles = this.particles.filter(p => p.isAlive);
}
emit() {
const particle = new Particle(this.x, this.y, this.particleOptions);
this.particles.push(particle);
}
draw(ctx) {
this.particles.forEach(p => p.draw(ctx));
}
start() {
this.isEmitting = true;
}
stop() {
this.isEmitting = false;
}
}9.7.3 烟花效果
javascript
/**
* 烟花粒子系统
*/
class Firework {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.particles = [];
this.isRunning = false;
}
explode(x, y) {
const color = `hsl(${Math.random() * 360}, 70%, 60%)`;
const particleCount = 100 + Math.random() * 50;
for (let i = 0; i < particleCount; i++) {
const angle = (i / particleCount) * Math.PI * 2;
const speed = 100 + Math.random() * 200;
this.particles.push(new Particle(x, y, {
angle,
speed,
size: 2 + Math.random() * 3,
color,
life: 1 + Math.random() * 1,
gravity: 200,
friction: 0.98
}));
}
}
start() {
this.isRunning = true;
this.lastTime = performance.now();
this.loop();
// 定时发射烟花
this.launchInterval = setInterval(() => {
this.explode(
100 + Math.random() * (this.canvas.width - 200),
100 + Math.random() * (this.canvas.height / 2)
);
}, 1000);
}
stop() {
this.isRunning = false;
clearInterval(this.launchInterval);
}
loop() {
if (!this.isRunning) return;
const now = performance.now();
const deltaTime = Math.min((now - this.lastTime) / 1000, 0.1);
this.lastTime = now;
// 半透明黑色覆盖,产生拖尾效果
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 更新和绘制
this.particles.forEach(p => {
p.update(deltaTime);
p.draw(this.ctx);
});
// 移除死亡粒子
this.particles = this.particles.filter(p => p.isAlive);
requestAnimationFrame(() => this.loop());
}
}
// 使用
const firework = new Firework(canvas);
firework.start();9.7.4 鼠标跟随粒子
javascript
/**
* 鼠标跟随粒子效果
*/
class MouseTrailParticles {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.particles = [];
this.mouseX = 0;
this.mouseY = 0;
this.bindEvents();
}
bindEvents() {
this.canvas.addEventListener('mousemove', (e) => {
const rect = this.canvas.getBoundingClientRect();
this.mouseX = e.clientX - rect.left;
this.mouseY = e.clientY - rect.top;
// 每次移动发射粒子
this.emit(3);
});
}
emit(count) {
for (let i = 0; i < count; i++) {
this.particles.push(new Particle(this.mouseX, this.mouseY, {
speed: 20 + Math.random() * 30,
size: 3 + Math.random() * 5,
color: `hsl(${Date.now() / 20 % 360}, 70%, 60%)`,
life: 0.5 + Math.random() * 0.5,
gravity: 50
}));
}
}
start() {
this.isRunning = true;
this.lastTime = performance.now();
this.loop();
}
loop() {
if (!this.isRunning) return;
const now = performance.now();
const deltaTime = Math.min((now - this.lastTime) / 1000, 0.1);
this.lastTime = now;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.particles.forEach(p => {
p.update(deltaTime);
p.draw(this.ctx);
});
this.particles = this.particles.filter(p => p.isAlive);
requestAnimationFrame(() => this.loop());
}
}9.8 简单物理动画
9.8.1 重力与弹跳
javascript
/**
* 带重力和弹跳的球
*/
class GravityBall extends AnimatedObject {
constructor(x, y, radius) {
super(x, y);
this.radius = radius;
this.vy = 0;
this.gravity = 980; // 像素/秒²
this.bounciness = 0.7; // 弹性系数
this.friction = 0.99;
}
update(deltaTime) {
// 应用重力
this.vy += this.gravity * deltaTime;
// 更新位置
this.y += this.vy * deltaTime;
// 地面碰撞
const ground = canvas.height - this.radius;
if (this.y > ground) {
this.y = ground;
this.vy *= -this.bounciness;
// 速度太小时停止
if (Math.abs(this.vy) < 10) {
this.vy = 0;
}
}
// 应用摩擦
this.vy *= this.friction;
}
draw(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = '#4D7CFF';
ctx.fill();
// 阴影
ctx.beginPath();
const shadowY = canvas.height - 5;
const shadowScale = 1 - (shadowY - this.y) / canvas.height;
ctx.ellipse(this.x, shadowY, this.radius * shadowScale, 5 * shadowScale, 0, 0, Math.PI * 2);
ctx.fillStyle = `rgba(0, 0, 0, ${0.3 * shadowScale})`;
ctx.fill();
}
}9.8.2 弹簧效果
javascript
/**
* 弹簧连接的两个点
*/
class Spring {
constructor(pointA, pointB, restLength, stiffness, damping) {
this.pointA = pointA;
this.pointB = pointB;
this.restLength = restLength;
this.stiffness = stiffness;
this.damping = damping;
}
update() {
const dx = this.pointB.x - this.pointA.x;
const dy = this.pointB.y - this.pointA.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const displacement = distance - this.restLength;
// 弹簧力 = -k * x
const force = this.stiffness * displacement;
// 方向
const nx = dx / distance;
const ny = dy / distance;
// 应用力
if (!this.pointA.fixed) {
this.pointA.vx += force * nx;
this.pointA.vy += force * ny;
this.pointA.vx *= this.damping;
this.pointA.vy *= this.damping;
}
if (!this.pointB.fixed) {
this.pointB.vx -= force * nx;
this.pointB.vy -= force * ny;
this.pointB.vx *= this.damping;
this.pointB.vy *= this.damping;
}
}
draw(ctx) {
ctx.beginPath();
ctx.moveTo(this.pointA.x, this.pointA.y);
ctx.lineTo(this.pointB.x, this.pointB.y);
ctx.strokeStyle = '#999';
ctx.lineWidth = 2;
ctx.stroke();
}
}
/**
* 可移动的点
*/
class SpringPoint {
constructor(x, y, fixed = false) {
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.fixed = fixed;
}
update(deltaTime) {
if (this.fixed) return;
this.x += this.vx * deltaTime;
this.y += this.vy * deltaTime;
}
draw(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, 8, 0, Math.PI * 2);
ctx.fillStyle = this.fixed ? '#FF6B6B' : '#4D7CFF';
ctx.fill();
}
}9.8.3 简单碰撞检测
javascript
/**
* 圆形碰撞检测
*/
function circleCollision(circle1, circle2) {
const dx = circle2.x - circle1.x;
const dy = circle2.y - circle1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance < circle1.radius + circle2.radius;
}
/**
* 解决圆形碰撞(弹性碰撞)
*/
function resolveCircleCollision(circle1, circle2) {
const dx = circle2.x - circle1.x;
const dy = circle2.y - circle1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance >= circle1.radius + circle2.radius) return;
// 法向量
const nx = dx / distance;
const ny = dy / distance;
// 相对速度
const dvx = circle1.vx - circle2.vx;
const dvy = circle1.vy - circle2.vy;
// 沿法向的相对速度
const dvn = dvx * nx + dvy * ny;
// 只处理相向运动
if (dvn > 0) return;
// 简单情况:质量相等
circle1.vx -= dvn * nx;
circle1.vy -= dvn * ny;
circle2.vx += dvn * nx;
circle2.vy += dvn * ny;
// 分离重叠
const overlap = (circle1.radius + circle2.radius - distance) / 2;
circle1.x -= overlap * nx;
circle1.y -= overlap * ny;
circle2.x += overlap * nx;
circle2.y += overlap * ny;
}9.9 动画状态机
9.9.1 精灵动画状态机
javascript
/**
* 动画状态机
*/
class AnimationStateMachine {
constructor() {
this.states = {};
this.currentState = null;
this.currentAnimation = null;
}
addState(name, animation, transitions = {}) {
this.states[name] = {
animation,
transitions
};
}
setState(name) {
if (name === this.currentState) return;
const state = this.states[name];
if (!state) {
console.warn(`状态 ${name} 不存在`);
return;
}
this.currentState = name;
this.currentAnimation = state.animation;
this.currentAnimation.reset();
}
trigger(event) {
if (!this.currentState) return;
const state = this.states[this.currentState];
const nextState = state.transitions[event];
if (nextState) {
this.setState(nextState);
}
}
update(deltaTime) {
this.currentAnimation?.update(deltaTime);
}
draw(ctx, x, y) {
this.currentAnimation?.draw(ctx, x, y);
}
}
// 使用示例
const character = new AnimationStateMachine();
character.addState('idle', idleAnimation, {
'walk': 'walking',
'jump': 'jumping'
});
character.addState('walking', walkAnimation, {
'stop': 'idle',
'jump': 'jumping'
});
character.addState('jumping', jumpAnimation, {
'land': 'idle'
});
character.setState('idle');
// 在游戏循环中
function gameLoop(deltaTime) {
// 处理输入
if (keys.ArrowRight) {
character.trigger('walk');
} else {
character.trigger('stop');
}
if (keys.Space) {
character.trigger('jump');
}
character.update(deltaTime);
character.draw(ctx, playerX, playerY);
}9.10 本章小结
本章深入介绍了 Canvas 动画系统:
核心概念
| 概念 | 说明 |
|---|---|
| requestAnimationFrame | 高效的动画循环 |
| deltaTime | 帧间时间差 |
| 帧率无关动画 | 基于时间而非帧数 |
| 缓动函数 | 自然的运动效果 |
动画循环模式
javascript
let lastTime = 0;
function animate(timestamp) {
const deltaTime = (timestamp - lastTime) / 1000;
lastTime = timestamp;
ctx.clearRect(0, 0, canvas.width, canvas.height);
update(deltaTime);
draw();
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);常用缓动类型
| 类型 | 效果 |
|---|---|
| linear | 匀速 |
| easeIn | 慢入 |
| easeOut | 慢出 |
| easeInOut | 慢入慢出 |
| elastic | 弹性 |
| bounce | 弹跳 |
9.11 练习题
基础练习
创建一个来回移动的方块动画
实现一个缓动效果选择器
创建多个随机运动的圆形
进阶练习
实现完整的粒子系统
创建带物理效果的弹球游戏
挑战练习
- 构建一个带状态机的游戏角色:
- 支持多种动画状态
- 平滑的状态过渡
- 键盘控制
下一章预告:在第10章中,我们将学习离屏渲染与性能优化。
文档版本:v1.0
字数统计:约 15,000 字
代码示例:50+ 个
