在前面的章节中,我们学习了基本的动画原理和交互事件处理。本章将深入探讨更高级的动画技术和物理效果,这些技术可以让你的Canvas应用更加生动、真实和引人入胜。
9.1 粒子系统
9.1.1 粒子系统基础
粒子系统是模拟各种自然现象(如火焰、烟雾、水流、爆炸等)的强大工具。它通过管理大量独立的小粒子来创建复杂的视觉效果。
class Particle {
constructor(x, y, options = {}) {
// 位置
this.x = x;
this.y = y;
// 速度
this.vx = options.vx || (Math.random() * 2 - 1);
this.vy = options.vy || (Math.random() * 2 - 1);
// 加速度(可用于模拟重力)
this.ax = options.ax || 0;
this.ay = options.ay || 0.1;
// 生命周期
this.life = options.life || Math.random() * 100 + 50;
this.maxLife = this.life;
// 外观
this.radius = options.radius || Math.random() * 5 + 2;
this.color = options.color || '#FFF';
this.opacity = options.opacity || 1;
// 其他属性
this.fadeRate = options.fadeRate || 0.02;
this.shrinkRate = options.shrinkRate || 0.01;
}
update() {
// 更新速度
this.vx += this.ax;
this.vy += this.ay;
// 更新位置
this.x += this.vx;
this.y += this.vy;
// 减少生命值
this.life -= 1;
// 随着生命减少,改变外观
this.opacity = Math.max(0, this.opacity - this.fadeRate);
this.radius = Math.max(0, this.radius - this.shrinkRate);
// 返回粒子是否存活
return this.life > 0 && this.opacity > 0 && this.radius > 0;
}
draw(ctx) {
ctx.save();
ctx.globalAlpha = this.opacity;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
class ParticleSystem {
constructor(x, y, options = {}) {
this.x = x;
this.y = y;
this.particles = [];
this.emissionRate = options.emissionRate || 5;
this.particleOptions = options.particleOptions || {};
this.isEmitting = true;
}
update() {
// 如果正在发射,添加新粒子
if (this.isEmitting) {
for (let i = 0; i < this.emissionRate; i++) {
this.particles.push(new Particle(this.x, this.y, this.particleOptions));
}
}
// 更新所有粒子,并移除死亡的粒子
for (let i = this.particles.length - 1; i >= 0; i--) {
if (!this.particles[i].update()) {
this.particles.splice(i, 1);
}
}
}
draw(ctx) {
for (const particle of this.particles) {
particle.draw(ctx);
}
}
startEmitting() {
this.isEmitting = true;
}
stopEmitting() {
this.isEmitting = false;
}
setPosition(x, y) {
this.x = x;
this.y = y;
}
}
// 使用示例 - 创建火焰效果
const canvas = document.getElementById('particleCanvas');
const ctx = canvas.getContext('2d');
const fireOptions = {
emissionRate: 10,
particleOptions: {
vx: () => Math.random() * 2 - 1,
vy: () => -Math.random() * 3 - 1,
ax: 0,
ay: -0.05,
life: () => Math.random() * 50 + 30,
radius: () => Math.random() * 3 + 1,
color: () => {
const colors = ['#FF5500', '#FF8800', '#FFAA00', '#FFCC00'];
return colors[Math.floor(Math.random() * colors.length)];
},
fadeRate: 0.01,
shrinkRate: 0.01
}
};
const fireSystem = new ParticleSystem(canvas.width / 2, canvas.height - 50, fireOptions);
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
fireSystem.update();
fireSystem.draw(ctx);
requestAnimationFrame(animate);
}
animate();
9.1.2 高级粒子效果
烟花效果
class Firework {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.particles = [];
this.rockets = [];
// 绑定点击事件
this.canvas.addEventListener('click', this.launchRocket.bind(this));
// 自动发射烟花
setInterval(() => {
if (Math.random() < 0.3) {
this.launchRocket();
}
}, 1000);
}
launchRocket(event) {
let targetX, targetY;
if (event) {
// 如果是点击事件,发射到点击位置
const rect = this.canvas.getBoundingClientRect();
targetX = event.clientX - rect.left;
targetY = event.clientY - rect.top;
} else {
// 随机位置
targetX = Math.random() * this.canvas.width;
targetY = Math.random() * this.canvas.height / 2;
}
// 创建火箭
const rocket = {
x: Math.random() * this.canvas.width,
y: this.canvas.height,
targetX: targetX,
targetY: targetY,
speed: 5 + Math.random() * 5,
angle: Math.atan2(targetY - this.canvas.height, targetX - this.canvas.width / 2),
color: `hsl(${Math.random() * 360}, 100%, 50%)`,
radius: 2,
trail: []
};
this.rockets.push(rocket);
}
explode(rocket) {
const particleCount = 100 + Math.floor(Math.random() * 100);
const baseHue = parseInt(rocket.color.match(/\d+/)[0]);
for (let i = 0; i < particleCount; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 5 + 1;
this.particles.push({
x: rocket.x,
y: rocket.y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
color: `hsl(${baseHue + Math.random() * 30 - 15}, 100%, 50%)`,
alpha: 1,
radius: Math.random() * 2 + 1,
decay: Math.random() * 0.02 + 0.01
});
}
}
update() {
// 更新火箭
for (let i = this.rockets.length - 1; i >= 0; i--) {
const rocket = this.rockets[i];
// 移动火箭
const dx = rocket.targetX - rocket.x;
const dy = rocket.targetY - rocket.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 10 || rocket.y < rocket.targetY) {
// 到达目标位置,爆炸
this.explode(rocket);
this.rockets.splice(i, 1);
} else {
// 继续移动
const vx = dx / distance * rocket.speed;
const vy = dy / distance * rocket.speed;
rocket.x += vx;
rocket.y += vy;
// 记录轨迹
rocket.trail.push({ x: rocket.x, y: rocket.y, alpha: 1 });
// 限制轨迹长度
if (rocket.trail.length > 20) {
rocket.trail.shift();
}
// 减少轨迹点的透明度
rocket.trail.forEach(point => {
point.alpha *= 0.9;
});
}
}
// 更新粒子
for (let i = this.particles.length - 1; i >= 0; i--) {
const particle = this.particles[i];
// 添加重力
particle.vy += 0.1;
// 移动粒子
particle.x += particle.vx;
particle.y += particle.vy;
// 减少透明度
particle.alpha -= particle.decay;
// 移除消失的粒子
if (particle.alpha <= 0) {
this.particles.splice(i, 1);
}
}
}
draw() {
// 绘制火箭
for (const rocket of this.rockets) {
// 绘制轨迹
for (const point of rocket.trail) {
this.ctx.fillStyle = `hsla(${rocket.color.match(/\d+/)[0]}, 100%, 50%, ${point.alpha})`;
this.ctx.beginPath();
this.ctx.arc(point.x, point.y, 1, 0, Math.PI * 2);
this.ctx.fill();
}
// 绘制火箭
this.ctx.fillStyle = rocket.color;
this.ctx.beginPath();
this.ctx.arc(rocket.x, rocket.y, rocket.radius, 0, Math.PI * 2);
this.ctx.fill();
}
// 绘制粒子
for (const particle of this.particles) {
this.ctx.fillStyle = particle.color.replace(')', `, ${particle.alpha})`);
this.ctx.beginPath();
this.ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
this.ctx.fill();
}
}
animate() {
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.update();
this.draw();
requestAnimationFrame(this.animate.bind(this));
}
}
// 使用示例
const canvas = document.getElementById('fireworkCanvas');
const firework = new Firework(canvas);
firework.animate();
雨滴效果
class RainEffect {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.raindrops = [];
this.splashes = [];
this.lastTime = 0;
this.rainIntensity = 0.3; // 每帧产生的雨滴数量
// 初始化
this.resize();
window.addEventListener('resize', this.resize.bind(this));
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
createRaindrop() {
return {
x: Math.random() * this.canvas.width,
y: -20,
length: Math.random() * 10 + 10,
speed: Math.random() * 5 + 15,
thickness: Math.random() * 2 + 1
};
}
createSplash(x, y) {
const count = Math.floor(Math.random() * 3) + 2;
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI;
const speed = Math.random() * 3 + 1;
this.splashes.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: -Math.sin(angle) * speed - 2,
radius: Math.random() * 1.5 + 0.5,
alpha: 1
});
}
}
update(deltaTime) {
// 创建新雨滴
const newDrops = Math.floor(this.rainIntensity * deltaTime / 16);
for (let i = 0; i < newDrops; i++) {
this.raindrops.push(this.createRaindrop());
}
// 更新雨滴
for (let i = this.raindrops.length - 1; i >= 0; i--) {
const drop = this.raindrops[i];
drop.y += drop.speed * deltaTime / 16;
// 检查是否到达地面
if (drop.y > this.canvas.height) {
// 创建飞溅效果
this.createSplash(drop.x, this.canvas.height);
this.raindrops.splice(i, 1);
}
}
// 更新飞溅效果
for (let i = this.splashes.length - 1; i >= 0; i--) {
const splash = this.splashes[i];
splash.x += splash.vx;
splash.y += splash.vy;
splash.vy += 0.2; // 重力
splash.alpha -= 0.05;
if (splash.alpha <= 0) {
this.splashes.splice(i, 1);
}
}
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制背景
this.ctx.fillStyle = '#333';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制雨滴
this.ctx.strokeStyle = 'rgba(174, 194, 224, 0.6)';
this.ctx.lineCap = 'round';
for (const drop of this.raindrops) {
this.ctx.lineWidth = drop.thickness;
this.ctx.beginPath();
this.ctx.moveTo(drop.x, drop.y);
this.ctx.lineTo(drop.x, drop.y + drop.length);
this.ctx.stroke();
}
// 绘制飞溅效果
for (const splash of this.splashes) {
this.ctx.fillStyle = `rgba(174, 194, 224, ${splash.alpha})`;
this.ctx.beginPath();
this.ctx.arc(splash.x, splash.y, splash.radius, 0, Math.PI * 2);
this.ctx.fill();
}
}
animate(currentTime) {
if (!this.lastTime) this.lastTime = currentTime;
const deltaTime = currentTime - this.lastTime;
this.lastTime = currentTime;
this.update(deltaTime);
this.draw();
requestAnimationFrame(this.animate.bind(this));
}
}
// 使用示例
const canvas = document.getElementById('rainCanvas');
const rainEffect = new RainEffect(canvas);
requestAnimationFrame(rainEffect.animate.bind(rainEffect));
9.2 物理引擎集成
9.2.1 基础物理引擎
创建一个简单的物理引擎来模拟基本的物理行为:
class PhysicsBody {
constructor(x, y, options = {}) {
this.x = x;
this.y = y;
this.vx = options.vx || 0;
this.vy = options.vy || 0;
this.mass = options.mass || 1;
this.radius = options.radius || 20;
this.restitution = options.restitution || 0.8; // 弹性系数
this.friction = options.friction || 0.05; // 摩擦系数
this.gravity = options.gravity !== undefined ? options.gravity : true;
this.fixed = options.fixed || false; // 是否固定不动
this.color = options.color || '#3498db';
}
update(deltaTime, world) {
if (this.fixed) return;
// 应用重力
if (this.gravity) {
this.vy += world.gravity * this.mass * deltaTime / 1000;
}
// 应用摩擦力
this.vx *= (1 - this.friction);
this.vy *= (1 - this.friction);
// 更新位置
this.x += this.vx * deltaTime / 1000;
this.y += this.vy * deltaTime / 1000;
// 边界碰撞检测
this.handleBoundaryCollision(world);
}
handleBoundaryCollision(world) {
// 左右边界
if (this.x - this.radius < 0) {
this.x = this.radius;
this.vx = -this.vx * this.restitution;
} else if (this.x + this.radius > world.width) {
this.x = world.width - this.radius;
this.vx = -this.vx * this.restitution;
}
// 上下边界
if (this.y - this.radius < 0) {
this.y = this.radius;
this.vy = -this.vy * this.restitution;
} else if (this.y + this.radius > world.height) {
this.y = world.height - this.radius;
this.vy = -this.vy * this.restitution;
}
}
applyForce(fx, fy) {
if (this.fixed) return;
// F = ma, a = F/m
this.vx += fx / this.mass;
this.vy += fy / this.mass;
}
draw(ctx) {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
}
}
class PhysicsWorld {
constructor(width, height, options = {}) {
this.width = width;
this.height = height;
this.gravity = options.gravity !== undefined ? options.gravity : 9.8;
this.bodies = [];
}
addBody(body) {
this.bodies.push(body);
return body;
}
removeBody(body) {
const index = this.bodies.indexOf(body);
if (index !== -1) {
this.bodies.splice(index, 1);
}
}
update(deltaTime) {
// 更新所有物体
for (const body of this.bodies) {
body.update(deltaTime, this);
}
// 检测碰撞
this.detectCollisions();
}
detectCollisions() {
for (let i = 0; i < this.bodies.length; i++) {
for (let j = i + 1; j < this.bodies.length; j++) {
const bodyA = this.bodies[i];
const bodyB = this.bodies[j];
// 如果两个物体都是固定的,跳过碰撞检测
if (bodyA.fixed && bodyB.fixed) continue;
// 计算距离
const dx = bodyB.x - bodyA.x;
const dy = bodyB.y - bodyA.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 检测碰撞
const minDistance = bodyA.radius + bodyB.radius;
if (distance < minDistance) {
// 碰撞发生,计算碰撞响应
this.resolveCollision(bodyA, bodyB, dx, dy, distance);
}
}
}
}
resolveCollision(bodyA, bodyB, dx, dy, distance) {
// 计算碰撞法线
const nx = dx / distance;
const ny = dy / distance;
// 计算相对速度
const relativeVelocityX = bodyB.vx - bodyA.vx;
const relativeVelocityY = bodyB.vy - bodyA.vy;
// 计算相对速度在碰撞法线上的投影
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
// 如果物体正在分离,不处理碰撞
if (velocityAlongNormal > 0) return;
// 计算弹性系数(取两个物体的平均值)
const restitution = (bodyA.restitution + bodyB.restitution) / 2;
// 计算冲量
let j = -(1 + restitution) * velocityAlongNormal;
j /= (1 / bodyA.mass) + (1 / bodyB.mass);
// 应用冲量
const impulseX = j * nx;
const impulseY = j * ny;
if (!bodyA.fixed) {
bodyA.vx -= impulseX / bodyA.mass;
bodyA.vy -= impulseY / bodyA.mass;
}
if (!bodyB.fixed) {
bodyB.vx += impulseX / bodyB.mass;
bodyB.vy += impulseY / bodyB.mass;
}
// 防止物体重叠
const overlap = (bodyA.radius + bodyB.radius) - distance;
const correctionX = nx * overlap * 0.5;
const correctionY = ny * overlap * 0.5;
if (!bodyA.fixed) {
bodyA.x -= correctionX;
bodyA.y -= correctionY;
}
if (!bodyB.fixed) {
bodyB.x += correctionX;
bodyB.y += correctionY;
}
}
draw(ctx) {
ctx.clearRect(0, 0, this.width, this.height);
// 绘制所有物体
for (const body of this.bodies) {
body.draw(ctx);
}
}
}
// 使用示例
const canvas = document.getElementById('physicsCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;
const world = new PhysicsWorld(canvas.width, canvas.height, { gravity: 9.8 });
// 添加一些物体
for (let i = 0; i < 20; i++) {
world.addBody(new PhysicsBody(
Math.random() * canvas.width,
Math.random() * canvas.height / 2,
{
radius: Math.random() * 20 + 10,
mass: Math.random() * 5 + 1,
restitution: Math.random() * 0.4 + 0.6,
color: `hsl(${Math.random() * 360}, 70%, 50%)`
}
));
}
// 添加地面
world.addBody(new PhysicsBody(
canvas.width / 2,
canvas.height - 20,
{
radius: canvas.width / 2,
fixed: true,
color: '#2c3e50'
}
));
// 添加交互
let selectedBody = null;
let isDragging = false;
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// 检查是否点击了某个物体
for (const body of world.bodies) {
const dx = mouseX - body.x;
const dy = mouseY - body.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < body.radius && !body.fixed) {
selectedBody = body;
isDragging = true;
break;
}
}
});
canvas.addEventListener('mousemove', (e) => {
if (isDragging && selectedBody) {
const rect = canvas.getBoundingClientRect();
selectedBody.x = e.clientX - rect.left;
selectedBody.y = e.clientY - rect.top;
selectedBody.vx = 0;
selectedBody.vy = 0;
}
});
canvas.addEventListener('mouseup', () => {
isDragging = false;
selectedBody = null;
});
let lastTime = 0;
function animate(currentTime) {
if (!lastTime) lastTime = currentTime;
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
world.update(deltaTime);
world.draw(ctx);
requestAnimationFrame(animate);
}
animate();
9.2.2 集成第三方物理引擎
对于更复杂的物理模拟,可以集成第三方物理引擎,如Matter.js、Box2D或Planck.js。以下是使用Matter.js的示例:
// 首先需要引入Matter.js库
// <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
class MatterJSIntegration {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
// 解构Matter.js模块
const { Engine, Render, Runner, World, Bodies, Composite, Mouse, MouseConstraint } = Matter;
// 创建引擎
this.engine = Engine.create();
this.world = this.engine.world;
// 创建渲染器
this.render = Render.create({
canvas: this.canvas,
engine: this.engine,
options: {
width: this.canvas.width,
height: this.canvas.height,
wireframes: false,
background: '#f0f0f0'
}
});
// 创建运行器
this.runner = Runner.create();
// 添加鼠标控制
this.mouse = Mouse.create(this.canvas);
this.mouseConstraint = MouseConstraint.create(this.engine, {
mouse: this.mouse,
constraint: {
stiffness: 0.2,
render: {
visible: false
}
}
});
World.add(this.world, this.mouseConstraint);
// 创建边界
const wallThickness = 50;
const walls = [
// 底部
Bodies.rectangle(
this.canvas.width / 2,
this.canvas.height + wallThickness / 2,
this.canvas.width + wallThickness * 2,
wallThickness,
{ isStatic: true }
),
// 顶部
Bodies.rectangle(
this.canvas.width / 2,
-wallThickness / 2,
this.canvas.width + wallThickness * 2,
wallThickness,
{ isStatic: true }
),
// 左侧
Bodies.rectangle(
-wallThickness / 2,
this.canvas.height / 2,
wallThickness,
this.canvas.height,
{ isStatic: true }
),
// 右侧
Bodies.rectangle(
this.canvas.width + wallThickness / 2,
this.canvas.height / 2,
wallThickness,
this.canvas.height,
{ isStatic: true }
)
];
World.add(this.world, walls);
// 添加一些物体
this.addBodies();
// 启动
Render.run(this.render);
Runner.run(this.runner, this.engine);
}
addBodies() {
const { Bodies, World, Composite } = Matter;
// 添加一些矩形
for (let i = 0; i < 10; i++) {
const rect = Bodies.rectangle(
Math.random() * this.canvas.width,
Math.random() * this.canvas.height / 2,
Math.random() * 50 + 30,
Math.random() * 50 + 30,
{
density: Math.random() * 0.005 + 0.001,
frictionAir: Math.random() * 0.01,
restitution: Math.random() * 0.3 + 0.6,
render: {
fillStyle: `hsl(${Math.random() * 360}, 70%, 50%)`
}
}
);
World.add(this.world, rect);
}
// 添加一些圆形
for (let i = 0; i < 10; i++) {
const circle = Bodies.circle(
Math.random() * this.canvas.width,
Math.random() * this.canvas.height / 2,
Math.random() * 25 + 15,
{
density: Math.random() * 0.005 + 0.001,
frictionAir: Math.random() * 0.01,
restitution: Math.random() * 0.3 + 0.6,
render: {
fillStyle: `hsl(${Math.random() * 360}, 70%, 50%)`
}
}
);
World.add(this.world, circle);
}
// 添加一个复合物体(汽车)
const car = this.createCar(400, 300, 100, 40);
World.add(this.world, car);
}
createCar(x, y, width, height) {
const { Bodies, Body, Composite } = Matter;
// 创建车身
const body = Bodies.rectangle(x, y, width, height, {
density: 0.002,
frictionAir: 0.01,
render: {
fillStyle: '#3498db'
}
});
// 创建车轮
const wheelRadius = height / 2.5;
const wheelOffset = width / 2.5;
const wheelA = Bodies.circle(x - wheelOffset, y + height / 2, wheelRadius, {
density: 0.002,
frictionAir: 0.01,
friction: 1,
render: {
fillStyle: '#2c3e50'
}
});
const wheelB = Bodies.circle(x + wheelOffset, y + height / 2, wheelRadius, {
density: 0.002,
frictionAir: 0.01,
friction: 1,
render: {
fillStyle: '#2c3e50'
}
});
// 创建约束(车轴)
const axelA = Matter.Constraint.create({
bodyA: body,
bodyB: wheelA,
pointA: { x: -wheelOffset, y: height / 2 },
pointB: { x: 0, y: 0 },
stiffness: 0.5
});
const axelB = Matter.Constraint.create({
bodyA: body,
bodyB: wheelB,
pointA: { x: wheelOffset, y: height / 2 },
pointB: { x: 0, y: 0 },
stiffness: 0.5
});
return Composite.create({ bodies: [body, wheelA, wheelB], constraints: [axelA, axelB] });
}
applyForce(body, force) {
Matter.Body.applyForce(body, body.position, force);
}
}
// 使用示例
const canvas = document.getElementById('matterCanvas');
canvas.width = 800;
canvas.height = 600;
const matterIntegration = new MatterJSIntegration(canvas);
9.3 碰撞检测与响应
9.3.1 基础碰撞检测
实现不同形状之间的碰撞检测:
class CollisionDetector {
// 点与圆碰撞检测
static pointCircle(px, py, cx, cy, radius) {
const dx = px - cx;
const dy = py - cy;
return dx * dx + dy * dy <= radius * radius;
}
// 点与矩形碰撞检测
static pointRect(px, py, rx, ry, rw, rh) {
return px >= rx && px <= rx + rw && py >= ry && py <= ry + rh;
}
// 圆与圆碰撞检测
static circleCircle(c1x, c1y, r1, c2x, c2y, r2) {
const dx = c1x - c2x;
const dy = c1y - c2y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance <= r1 + r2;
}
// 圆与矩形碰撞检测
static circleRect(cx, cy, radius, rx, ry, rw, rh) {
// 找到矩形上离圆心最近的点
const closestX = Math.max(rx, Math.min(cx, rx + rw));
const closestY = Math.max(ry, Math.min(cy, ry + rh));
// 计算圆心到最近点的距离
const dx = cx - closestX;
const dy = cy - closestY;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance <= radius;
}
// 矩形与矩形碰撞检测
static rectRect(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) {
return r1x < r2x + r2w && r1x + r1w > r2x && r1y < r2y + r2h && r1y + r1h > r2y;
}
// 线段与线段碰撞检测
static lineLine(x1, y1, x2, y2, x3, y3, x4, y4) {
// 计算两条线段的方向向量
const uA = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
const uB = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
// 如果uA和uB都在[0,1]范围内,则线段相交
return uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1;
}
// 线段与圆碰撞检测
static lineCircle(x1, y1, x2, y2, cx, cy, radius) {
// 检查线段的端点是否在圆内
if (this.pointCircle(x1, y1, cx, cy, radius) || this.pointCircle(x2, y2, cx, cy, radius)) {
return true;
}
// 计算线段的长度
const lineLength = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
// 计算点积
const dot = (((cx - x1) * (x2 - x1)) + ((cy - y1) * (y2 - y1))) / (lineLength * lineLength);
// 找到线段上离圆心最近的点
const closestX = x1 + (dot * (x2 - x1));
const closestY = y1 + (dot * (y2 - y1));
// 检查这个点是否在线段上
if (!this.pointLine(closestX, closestY, x1, y1, x2, y2)) {
return false;
}
// 检查最近点到圆心的距离是否小于半径
const dx = closestX - cx;
const dy = closestY - cy;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance <= radius;
}
// 点是否在线段上
static pointLine(px, py, x1, y1, x2, y2) {
// 计算线段长度的平方
const lineLength = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
if (lineLength === 0) {
// 线段实际上是一个点
return (px === x1 && py === y1);
}
// 计算点到线段端点的距离之和
const d1 = Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1));
const d2 = Math.sqrt((px - x2) * (px - x2) + (py - y2) * (py - y2));
// 允许一个小的误差
const buffer = 0.1;
// 检查点到线段端点的距离之和是否等于线段长度
return Math.abs(d1 + d2 - Math.sqrt(lineLength)) <= buffer;
}
// 多边形与点碰撞检测
static polygonPoint(vertices, px, py) {
let collision = false;
// 使用射线法检测点是否在多边形内
for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++) {
const vi = vertices[i];
const vj = vertices[j];
if (((vi.y > py) !== (vj.y > py)) &&
(px < (vj.x - vi.x) * (py - vi.y) / (vj.y - vi.y) + vi.x)) {
collision = !collision;
}
}
return collision;
}
// 多边形与圆碰撞检测
static polygonCircle(vertices, cx, cy, radius) {
// 检查圆心是否在多边形内
if (this.polygonPoint(vertices, cx, cy)) {
return true;
}
// 检查多边形的每条边是否与圆相交
for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++) {
const vi = vertices[i];
const vj = vertices[j];
if (this.lineCircle(vi.x, vi.y, vj.x, vj.y, cx, cy, radius)) {
return true;
}
}
return false;
}
// 多边形与多边形碰撞检测(使用分离轴定理)
static polygonPolygon(vertices1, vertices2) {
// 检查第一个多边形的每条边
for (let i = 0, j = vertices1.length - 1; i < vertices1.length; j = i++) {
const vi = vertices1[i];
const vj = vertices1[j];
// 获取法向量
const normal = { x: vj.y - vi.y, y: vi.x - vj.x };
// 归一化法向量
const length = Math.sqrt(normal.x * normal.x + normal.y * normal.y);
normal.x /= length;
normal.y /= length;
// 找到两个多边形在法向量上的投影
let min1 = Infinity, max1 = -Infinity;
for (const vertex of vertices1) {
const projection = normal.x * vertex.x + normal.y * vertex.y;
min1 = Math.min(min1, projection);
max1 = Math.max(max1, projection);
}
let min2 = Infinity, max2 = -Infinity;
for (const vertex of vertices2) {
const projection = normal.x * vertex.x + normal.y * vertex.y;
min2 = Math.min(min2, projection);
max2 = Math.max(max2, projection);
}
// 检查投影是否重叠
if (max1 < min2 || max2 < min1) {
// 找到了一个分离轴,多边形不相交
return false;
}
}
// 检查第二个多边形的每条边
for (let i = 0, j = vertices2.length - 1; i < vertices2.length; j = i++) {
const vi = vertices2[i];
const vj = vertices2[j];
// 获取法向量
const normal = { x: vj.y - vi.y, y: vi.x - vj.x };
// 归一化法向量
const length = Math.sqrt(normal.x * normal.x + normal.y * normal.y);
normal.x /= length;
normal.y /= length;
// 找到两个多边形在法向量上的投影
let min1 = Infinity, max1 = -Infinity;
for (const vertex of vertices1) {
const projection = normal.x * vertex.x + normal.y * vertex.y;
min1 = Math.min(min1, projection);
max1 = Math.max(max1, projection);
}
let min2 = Infinity, max2 = -Infinity;
for (const vertex of vertices2) {
const projection = normal.x * vertex.x + normal.y * vertex.y;
min2 = Math.min(min2, projection);
max2 = Math.max(max2, projection);
}
// 检查投影是否重叠
if (max1 < min2 || max2 < min1) {
// 找到了一个分离轴,多边形不相交
return false;
}
}
// 没有找到分离轴,多边形相交
return true;
}
}
// 使用示例
const canvas = document.getElementById('collisionCanvas');
const ctx = canvas.getContext('2d');
// 创建一些形状
const circle = { x: 200, y: 200, radius: 50 };
const rect = { x: 400, y: 200, width: 100, height: 80 };
const polygon = {
vertices: [
{ x: 600, y: 150 },
{ x: 650, y: 200 },
{ x: 600, y: 250 },
{ x: 550, y: 200 }
]
};
// 绘制函数
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 获取鼠标位置
const mouseX = mouse.x;
const mouseY = mouse.y;
// 检测碰撞
const circleCollision = CollisionDetector.pointCircle(mouseX, mouseY, circle.x, circle.y, circle.radius);
const rectCollision = CollisionDetector.pointRect(mouseX, mouseY, rect.x, rect.y, rect.width, rect.height);
const polygonCollision = CollisionDetector.polygonPoint(polygon.vertices, mouseX, mouseY);
// 绘制圆
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
ctx.fillStyle = circleCollision ? 'rgba(52, 152, 219, 0.7)' : 'rgba(52, 152, 219, 0.3)';
ctx.fill();
// 绘制矩形
ctx.fillStyle = rectCollision ? 'rgba(231, 76, 60, 0.7)' : 'rgba(231, 76, 60, 0.3)';
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
// 绘制多边形
ctx.beginPath();
ctx.moveTo(polygon.vertices[0].x, polygon.vertices[0].y);
for (let i = 1; i < polygon.vertices.length; i++) {
ctx.lineTo(polygon.vertices[i].x, polygon.vertices[i].y);
}
ctx.closePath();
ctx.fillStyle = polygonCollision ? 'rgba(46, 204, 113, 0.7)' : 'rgba(46, 204, 113, 0.3)';
ctx.fill();
// 绘制鼠标位置
ctx.beginPath();
ctx.arc(mouseX, mouseY, 5, 0, Math.PI * 2);
ctx.fillStyle = '#2c3e50';
ctx.fill();
requestAnimationFrame(draw);
}
// 跟踪鼠标位置
const mouse = { x: 0, y: 0 };
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
});
draw();
9.3.2 空间分区与优化
对于大量物体的碰撞检测,可以使用空间分区技术来优化性能:
class QuadTree {
constructor(boundary, capacity) {
this.boundary = boundary; // { x, y, width, height }
this.capacity = capacity; // 每个节点的最大容量
this.objects = [];
this.divided = false;
this.northwest = null;
this.northeast = null;
this.southwest = null;
this.southeast = null;
}
// 插入对象
insert(object) {
// 如果对象不在边界内,不插入
if (!this.contains(object)) {
return false;
}
// 如果当前节点未满,直接插入
if (this.objects.length < this.capacity && !this.divided) {
this.objects.push(object);
return true;
}
// 如果当前节点已满,分割节点
if (!this.divided) {
this.subdivide();
}
// 尝试将对象插入到子节点
return (
this.northwest.insert(object) ||
this.northeast.insert(object) ||
this.southwest.insert(object) ||
this.southeast.insert(object)
);
}
// 分割节点
subdivide() {
const x = this.boundary.x;
const y = this.boundary.y;
const w = this.boundary.width / 2;
const h = this.boundary.height / 2;
const nw = { x: x, y: y, width: w, height: h };
const ne = { x: x + w, y: y, width: w, height: h };
const sw = { x: x, y: y + h, width: w, height: h };
const se = { x: x + w, y: y + h, width: w, height: h };
this.northwest = new QuadTree(nw, this.capacity);
this.northeast = new QuadTree(ne, this.capacity);
this.southwest = new QuadTree(sw, this.capacity);
this.southeast = new QuadTree(se, this.capacity);
this.divided = true;
// 将当前节点的对象重新分配到子节点
for (const object of this.objects) {
this.northwest.insert(object);
this.northeast.insert(object);
this.southwest.insert(object);
this.southeast.insert(object);
}
this.objects = [];
}
// 检查对象是否在边界内
contains(object) {
return (
object.x >= this.boundary.x &&
object.x < this.boundary.x + this.boundary.width &&
object.y >= this.boundary.y &&
object.y < this.boundary.y + this.boundary.height
);
}
// 查询范围内的所有对象
query(range, found = []) {
// 如果范围与边界不相交,返回空数组
if (!this.intersects(range)) {
return found;
}
// 检查当前节点中的对象
for (const object of this.objects) {
if (this.objectInRange(object, range)) {
found.push(object);
}
}
// 如果已分割,递归查询子节点
if (this.divided) {
this.northwest.query(range, found);
this.northeast.query(range, found);
this.southwest.query(range, found);
this.southeast.query(range, found);
}
return found;
}
// 检查范围是否与边界相交
intersects(range) {
return !(
range.x > this.boundary.x + this.boundary.width ||
range.x + range.width < this.boundary.x ||
range.y > this.boundary.y + this.boundary.height ||
range.y + range.height < this.boundary.y
);
}
// 检查对象是否在范围内
objectInRange(object, range) {
return (
object.x >= range.x &&
object.x < range.x + range.width &&
object.y >= range.y &&
object.y < range.y + range.height
);
}
// 清空四叉树
clear() {
this.objects = [];
if (this.divided) {
this.northwest.clear();
this.northeast.clear();
this.southwest.clear();
this.southeast.clear();
this.northwest = null;
this.northeast = null;
this.southwest = null;
this.southeast = null;
this.divided = false;
}
}
// 绘制四叉树(用于调试)
draw(ctx) {
ctx.strokeStyle = '#aaa';
ctx.strokeRect(
this.boundary.x,
this.boundary.y,
this.boundary.width,
this.boundary.height
);
if (this.divided) {
this.northwest.draw(ctx);
this.northeast.draw(ctx);
this.southwest.draw(ctx);
this.southeast.draw(ctx);
}
}
}
// 使用示例 - 优化大量粒子的碰撞检测
class OptimizedParticleSystem {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.particles = [];
this.quadTree = new QuadTree(
{ x: 0, y: 0, width: canvas.width, height: canvas.height },
4
);
// 创建粒子
for (let i = 0; i < 500; i++) {
this.particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
radius: Math.random() * 5 + 2,
vx: Math.random() * 2 - 1,
vy: Math.random() * 2 - 1,
color: `hsl(${Math.random() * 360}, 70%, 50%)`
});
}
}
update() {
// 清空四叉树
this.quadTree.clear();
// 更新粒子位置并插入到四叉树
for (const particle of this.particles) {
particle.x += particle.vx;
particle.y += particle.vy;
// 边界碰撞
if (particle.x < particle.radius) {
particle.x = particle.radius;
particle.vx *= -1;
} else if (particle.x > this.canvas.width - particle.radius) {
particle.x = this.canvas.width - particle.radius;
particle.vx *= -1;
}
if (particle.y < particle.radius) {
particle.y = particle.radius;
particle.vy *= -1;
} else if (particle.y > this.canvas.height - particle.radius) {
particle.y = this.canvas.height - particle.radius;
particle.vy *= -1;
}
// 插入到四叉树
this.quadTree.insert(particle);
}
// 检测碰撞
for (const particle of this.particles) {
// 创建一个查询范围(粒子周围的区域)
const range = {
x: particle.x - particle.radius * 2,
y: particle.y - particle.radius * 2,
width: particle.radius * 4,
height: particle.radius * 4
};
// 查询范围内的粒子
const potentialCollisions = this.quadTree.query(range);
// 检测碰撞
for (const other of potentialCollisions) {
// 避免自身碰撞
if (particle === other) continue;
// 计算距离
const dx = other.x - particle.x;
const dy = other.y - particle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 检测碰撞
if (distance < particle.radius + other.radius) {
// 简单的碰撞响应
const angle = Math.atan2(dy, dx);
const sin = Math.sin(angle);
const cos = Math.cos(angle);
// 旋转粒子位置
const pos1 = { x: 0, y: 0 };
const pos2 = { x: dx * cos + dy * sin, y: dy * cos - dx * sin };
// 旋转粒子速度
const vel1 = { x: particle.vx * cos + particle.vy * sin, y: particle.vy * cos - particle.vx * sin };
const vel2 = { x: other.vx * cos + other.vy * sin, y: other.vy * cos - other.vx * sin };
// 碰撞后的速度
const vxTotal = vel1.x - vel2.x;
vel1.x = vel2.x;
vel2.x = vxTotal + vel1.x;
// 更新位置,防止粒子重叠
const absV = Math.abs(vel1.x) + Math.abs(vel2.x);
const overlap = (particle.radius + other.radius) - Math.abs(pos1.x - pos2.x);
pos1.x += vel1.x / absV * overlap;
pos2.x += vel2.x / absV * overlap;
// 旋转回原来的坐标系
const pos1F = { x: pos1.x * cos - pos1.y * sin, y: pos1.y * cos + pos1.x * sin };
const pos2F = { x: pos2.x * cos - pos2.y * sin, y: pos2.y * cos + pos2.x * sin };
// 调整粒子位置
other.x = particle.x + pos2F.x;
other.y = particle.y + pos2F.y;
particle.x = particle.x + pos1F.x;
particle.y = particle.y + pos1F.y;
// 旋转速度向量
particle.vx = vel1.x * cos - vel1.y * sin;
particle.vy = vel1.y * cos + vel1.x * sin;
other.vx = vel2.x * cos - vel2.y * sin;
other.vy = vel2.y * cos + vel2.x * sin;
}
}
}
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制粒子
for (const particle of this.particles) {
this.ctx.fillStyle = particle.color;
this.ctx.beginPath();
this.ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
this.ctx.fill();
}
// 绘制四叉树(用于调试)
// this.quadTree.draw(this.ctx);
}
animate() {
this.update();
this.draw();
requestAnimationFrame(this.animate.bind(this));
}
}
// 使用示例
const canvas = document.getElementById('optimizedCanvas');
canvas.width = 800;
canvas.height = 600;
const particleSystem = new OptimizedParticleSystem(canvas);
particleSystem.animate();
9.4 高级动画技术
9.4.1 骨骼动画
骨骼动画是一种高级动画技术,它通过模拟角色的骨骼结构来创建更自然的动画:
class Bone {
constructor(x, y, length, angle, parent = null) {
this.x = x;
this.y = y;
this.length = length;
this.angle = angle;
this.parent = parent;
this.children = [];
// 计算末端点
this.endX = this.x + Math.cos(this.angle) * this.length;
this.endY = this.y + Math.sin(this.angle) * this.length;
}
addChild(bone) {
bone.parent = this;
bone.x = this.endX;
bone.y = this.endY;
this.children.push(bone);
bone.updateEndPoint();
return bone;
}
updateEndPoint() {
this.endX = this.x + Math.cos(this.angle) * this.length;
this.endY = this.y + Math.sin(this.angle) * this.length;
// 更新所有子骨骼
for (const child of this.children) {
child.x = this.endX;
child.y = this.endY;
child.updateEndPoint();
}
}
setAngle(angle) {
this.angle = angle;
this.updateEndPoint();
}
draw(ctx) {
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.lineTo(this.endX, this.endY);
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.strokeStyle = '#3498db';
ctx.stroke();
// 绘制关节
ctx.beginPath();
ctx.arc(this.x, this.y, 3, 0, Math.PI * 2);
ctx.fillStyle = '#e74c3c';
ctx.fill();
// 绘制子骨骼
for (const child of this.children) {
child.draw(ctx);
}
}
}
class SkeletonAnimation {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
// 创建骨骼结构
this.createSkeleton();
// 创建动画关键帧
this.createAnimations();
// 动画状态
this.currentAnimation = 'walk';
this.animationTime = 0;
this.animationSpeed = 0.05;
// 绑定事件
this.canvas.addEventListener('click', this.toggleAnimation.bind(this));
}
createSkeleton() {
// 创建根骨骼(躯干)
this.rootBone = new Bone(this.canvas.width / 2, 200, 60, Math.PI / 2);
// 创建头部
const head = new Bone(this.rootBone.x, this.rootBone.y, 30, -Math.PI / 2);
// 创建左臂
const leftShoulder = new Bone(this.rootBone.x, this.rootBone.y + 20, 40, Math.PI + Math.PI / 4);
const leftElbow = new Bone(leftShoulder.endX, leftShoulder.endY, 40, Math.PI + Math.PI / 4);
leftShoulder.addChild(leftElbow);
// 创建右臂
const rightShoulder = new Bone(this.rootBone.x, this.rootBone.y + 20, 40, -Math.PI / 4);
const rightElbow = new Bone(rightShoulder.endX, rightShoulder.endY, 40, -Math.PI / 4);
rightShoulder.addChild(rightElbow);
// 创建左腿
const leftHip = new Bone(this.rootBone.endX, this.rootBone.endY, 50, Math.PI / 2 + Math.PI / 8);
const leftKnee = new Bone(leftHip.endX, leftHip.endY, 50, Math.PI / 2 - Math.PI / 8);
leftHip.addChild(leftKnee);
// 创建右腿
const rightHip = new Bone(this.rootBone.endX, this.rootBone.endY, 50, Math.PI / 2 - Math.PI / 8);
const rightKnee = new Bone(rightHip.endX, rightHip.endY, 50, Math.PI / 2 + Math.PI / 8);
rightHip.addChild(rightKnee);
// 存储所有骨骼的引用
this.bones = {
root: this.rootBone,
head: head,
leftShoulder: leftShoulder,
leftElbow: leftElbow,
rightShoulder: rightShoulder,
rightElbow: rightElbow,
leftHip: leftHip,
leftKnee: leftKnee,
rightHip: rightHip,
rightKnee: rightKnee
};
}
createAnimations() {
// 走路动画
this.animations = {
idle: {
duration: 1,
keyframes: [
{
time: 0,
bones: {
leftShoulder: { angle: Math.PI + Math.PI / 8 },
leftElbow: { angle: Math.PI + Math.PI / 8 },
rightShoulder: { angle: -Math.PI / 8 },
rightElbow: { angle: -Math.PI / 8 },
leftHip: { angle: Math.PI / 2 },
leftKnee: { angle: Math.PI / 2 },
rightHip: { angle: Math.PI / 2 },
rightKnee: { angle: Math.PI / 2 }
}
},
{
time: 0.5,
bones: {
leftShoulder: { angle: Math.PI + Math.PI / 10 },
leftElbow: { angle: Math.PI + Math.PI / 10 },
rightShoulder: { angle: -Math.PI / 10 },
rightElbow: { angle: -Math.PI / 10 },
leftHip: { angle: Math.PI / 2 + Math.PI / 50 },
leftKnee: { angle: Math.PI / 2 - Math.PI / 50 },
rightHip: { angle: Math.PI / 2 - Math.PI / 50 },
rightKnee: { angle: Math.PI / 2 + Math.PI / 50 }
}
},
{
time: 1,
bones: {
leftShoulder: { angle: Math.PI + Math.PI / 8 },
leftElbow: { angle: Math.PI + Math.PI / 8 },
rightShoulder: { angle: -Math.PI / 8 },
rightElbow: { angle: -Math.PI / 8 },
leftHip: { angle: Math.PI / 2 },
leftKnee: { angle: Math.PI / 2 },
rightHip: { angle: Math.PI / 2 },
rightKnee: { angle: Math.PI / 2 }
}
}
]
},
walk: {
duration: 1,
keyframes: [
{
time: 0,
bones: {
leftShoulder: { angle: Math.PI + Math.PI / 4 },
leftElbow: { angle: Math.PI + Math.PI / 8 },
rightShoulder: { angle: -Math.PI / 4 },
rightElbow: { angle: -Math.PI / 8 },
leftHip: { angle: Math.PI / 2 + Math.PI / 4 },
leftKnee: { angle: Math.PI / 2 - Math.PI / 8 },
rightHip: { angle: Math.PI / 2 - Math.PI / 4 },
rightKnee: { angle: Math.PI / 2 + Math.PI / 8 }
}
},
{
time: 0.5,
bones: {
leftShoulder: { angle: Math.PI - Math.PI / 4 },
leftElbow: { angle: Math.PI - Math.PI / 8 },
rightShoulder: { angle: Math.PI / 4 },
rightElbow: { angle: Math.PI / 8 },
leftHip: { angle: Math.PI / 2 - Math.PI / 4 },
leftKnee: { angle: Math.PI / 2 + Math.PI / 8 },
rightHip: { angle: Math.PI / 2 + Math.PI / 4 },
rightKnee: { angle: Math.PI / 2 - Math.PI / 8 }
}
},
{
time: 1,
bones: {
leftShoulder: { angle: Math.PI + Math.PI / 4 },
leftElbow: { angle: Math.PI + Math.PI / 8 },
rightShoulder: { angle: -Math.PI / 4 },
rightElbow: { angle: -Math.PI / 8 },
leftHip: { angle: Math.PI / 2 + Math.PI / 4 },
leftKnee: { angle: Math.PI / 2 - Math.PI / 8 },
rightHip: { angle: Math.PI / 2 - Math.PI / 4 },
rightKnee: { angle: Math.PI / 2 + Math.PI / 8 }
}
}
]
}
};
}
toggleAnimation() {
this.currentAnimation = this.currentAnimation === 'walk' ? 'idle' : 'walk';
}
update() {
// 更新动画时间
const animation = this.animations[this.currentAnimation];
this.animationTime = (this.animationTime + this.animationSpeed) % animation.duration;
// 找到当前时间对应的关键帧
let keyframe1 = null;
let keyframe2 = null;
let t = 0;
for (let i = 0; i < animation.keyframes.length; i++) {
if (animation.keyframes[i].time <= this.animationTime) {
keyframe1 = animation.keyframes[i];
keyframe2 = animation.keyframes[(i + 1) % animation.keyframes.length];
// 计算插值因子
const t1 = keyframe1.time;
let t2 = keyframe2.time;
if (t2 < t1) t2 += animation.duration;
t = (this.animationTime - t1) / (t2 - t1);
}
}
if (!keyframe1 || !keyframe2) return;
// 插值骨骼角度
for (const boneName in keyframe1.bones) {
const bone1 = keyframe1.bones[boneName];
const bone2 = keyframe2.bones[boneName];
// 计算角度插值
let angle1 = bone1.angle;
let angle2 = bone2.angle;
// 确保角度差在[-PI, PI]范围内
while (angle2 - angle1 > Math.PI) angle1 += Math.PI * 2;
while (angle1 - angle2 > Math.PI) angle2 += Math.PI * 2;
const angle = angle1 + (angle2 - angle1) * t;
// 应用到骨骼
this.bones[boneName].setAngle(angle);
}
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制骨骼
this.rootBone.draw(this.ctx);
this.bones.head.draw(this.ctx);
// 绘制动画信息
this.ctx.fillStyle = '#333';
this.ctx.font = '16px Arial';
this.ctx.fillText(`Animation: ${this.currentAnimation}`, 20, 30);
this.ctx.fillText('Click to toggle animation', 20, 50);
}
animate() {
this.update();
this.draw();
requestAnimationFrame(this.animate.bind(this));
}
}
// 使用示例
const canvas = document.getElementById('skeletonCanvas');
canvas.width = 800;
canvas.height = 600;
const skeletonAnimation = new SkeletonAnimation(canvas);
skeletonAnimation.animate();
9.4.2 布料模拟
布料模拟是一种高级物理效果,它通过模拟布料的物理特性来创建逼真的布料动画:
class ClothSimulation {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
// 布料参数
this.width = 20; // 布料宽度(点数)
this.height = 20; // 布料高度(点数)
this.spacing = 10; // 点之间的间距
this.stiffness = 0.1; // 弹簧刚度
this.damping = 0.95; // 阻尼
this.gravity = 0.2; // 重力
// 创建布料
this.createCloth();
// 鼠标交互
this.mouse = { x: 0, y: 0, down: false };
this.selectedPoint = null;
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
}
createCloth() {
// 创建点
this.points = [];
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const point = {
x: 100 + x * this.spacing,
y: 100 + y * this.spacing,
oldX: 100 + x * this.spacing,
oldY: 100 + y * this.spacing,
pinned: y === 0 && (x === 0 || x === this.width - 1), // 固定顶部两角
mass: 1
};
this.points.push(point);
}
}
// 创建约束(弹簧)
this.constraints = [];
// 水平约束
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width - 1; x++) {
const p1 = this.points[y * this.width + x];
const p2 = this.points[y * this.width + x + 1];
this.constraints.push({
p1: p1,
p2: p2,
length: this.spacing
});
}
}
// 垂直约束
for (let y = 0; y < this.height - 1; y++) {
for (let x = 0; x < this.width; x++) {
const p1 = this.points[y * this.width + x];
const p2 = this.points[(y + 1) * this.width + x];
this.constraints.push({
p1: p1,
p2: p2,
length: this.spacing
});
}
}
}
handleMouseDown(e) {
const rect = this.canvas.getBoundingClientRect();
this.mouse.x = e.clientX - rect.left;
this.mouse.y = e.clientY - rect.top;
this.mouse.down = true;
// 查找最近的点
let minDist = Infinity;
for (const point of this.points) {
const dx = point.x - this.mouse.x;
const dy = point.y - this.mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < minDist && dist < 20) {
minDist = dist;
this.selectedPoint = point;
}
}
}
handleMouseMove(e) {
const rect = this.canvas.getBoundingClientRect();
this.mouse.x = e.clientX - rect.left;
this.mouse.y = e.clientY - rect.top;
}
handleMouseUp() {
this.mouse.down = false;
this.selectedPoint = null;
}
update() {
// 处理鼠标拖拽
if (this.mouse.down && this.selectedPoint) {
this.selectedPoint.x = this.mouse.x;
this.selectedPoint.y = this.mouse.y;
}
// 更新所有点
for (const point of this.points) {
if (point.pinned || point === this.selectedPoint) continue;
// 保存当前位置
const vx = (point.x - point.oldX) * this.damping;
const vy = (point.y - point.oldY) * this.damping;
// 更新旧位置
point.oldX = point.x;
point.oldY = point.y;
// 应用速度和重力
point.x += vx;
point.y += vy + this.gravity;
}
// 应用约束
for (let i = 0; i < 5; i++) { // 多次迭代以提高稳定性
for (const constraint of this.constraints) {
const dx = constraint.p2.x - constraint.p1.x;
const dy = constraint.p2.y - constraint.p1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const difference = constraint.length - distance;
const percent = (difference / distance) * 0.5 * this.stiffness;
const offsetX = dx * percent;
const offsetY = dy * percent;
// 如果两个点都没有固定,则平均分配位移
if (!constraint.p1.pinned && constraint.p1 !== this.selectedPoint) {
constraint.p1.x -= offsetX;
constraint.p1.y -= offsetY;
}
if (!constraint.p2.pinned && constraint.p2 !== this.selectedPoint) {
constraint.p2.x += offsetX;
constraint.p2.y += offsetY;
}
}
}
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制布料
this.ctx.beginPath();
// 绘制三角形
for (let y = 0; y < this.height - 1; y++) {
for (let x = 0; x < this.width - 1; x++) {
const p1 = this.points[y * this.width + x];
const p2 = this.points[y * this.width + x + 1];
const p3 = this.points[(y + 1) * this.width + x];
const p4 = this.points[(y + 1) * this.width + x + 1];
// 计算颜色(基于位置的渐变)
const hue = (x / this.width * 180 + y / this.height * 180) % 360;
this.ctx.fillStyle = `hsl(${hue}, 70%, 60%)`;
// 绘制两个三角形
this.ctx.beginPath();
this.ctx.moveTo(p1.x, p1.y);
this.ctx.lineTo(p2.x, p2.y);
this.ctx.lineTo(p3.x, p3.y);
this.ctx.fill();
this.ctx.beginPath();
this.ctx.moveTo(p2.x, p2.y);
this.ctx.lineTo(p4.x, p4.y);
this.ctx.lineTo(p3.x, p3.y);
this.ctx.fill();
}
}
// 绘制约束(可选,用于调试)
/*
this.ctx.strokeStyle = '#aaa';
this.ctx.lineWidth = 1;
for (const constraint of this.constraints) {
this.ctx.beginPath();
this.ctx.moveTo(constraint.p1.x, constraint.p1.y);
this.ctx.lineTo(constraint.p2.x, constraint.p2.y);
this.ctx.stroke();
}
*/
// 绘制点(可选,用于调试)
/*
for (const point of this.points) {
this.ctx.fillStyle = point.pinned ? '#e74c3c' : '#3498db';
this.ctx.beginPath();
this.ctx.arc(point.x, point.y, 3, 0, Math.PI * 2);
this.ctx.fill();
}
*/
// 绘制说明
this.ctx.fillStyle = '#333';
this.ctx.font = '16px Arial';
this.ctx.fillText('拖拽布料的任意部分', 20, 30);
}
animate() {
this.update();
this.draw();
requestAnimationFrame(this.animate.bind(this));
}
}
// 使用示例
const canvas = document.getElementById('clothCanvas');
canvas.width = 800;
canvas.height = 600;
const clothSimulation = new ClothSimulation(canvas);
clothSimulation.animate();
9.5 小结
在本章中,我们学习了如何在Canvas中实现高级动画和物理效果:
粒子系统:通过管理大量独立的粒子来创建复杂的视觉效果,如火焰、烟雾、雨滴等。
物理引擎:实现了一个简单的物理引擎,并学习了如何集成第三方物理引擎(如Matter.js)来模拟物理行为。
碰撞检测:学习了各种形状之间的碰撞检测算法,以及如何使用空间分区技术(四叉树)来优化大量物体的碰撞检测。
高级动画技术:实现了骨骼动画和布料模拟等高级动画效果。
这些技术可以帮助你创建更加生动、真实和引人入胜的Canvas应用。
9.6 练习
创建一个粒子系统,模拟雪花效果,包括风力影响和与地面的碰撞。
使用物理引擎实现一个简单的弹球游戏,包括障碍物和得分系统。
实现一个基于四叉树的碰撞检测系统,并创建一个包含数百个移动物体的演示。
创建一个简单的角色,使用骨骼动画实现走路、跑步和跳跃等动作。
实现一个旗帜飘动的效果,使用布料模拟技术,并添加风力影响。
在下一章中,我们将学习Canvas的性能优化和最佳实践,以确保你的应用在各种设备上都能流畅运行。