在前面的章节中,我们学习了基本的动画原理和交互事件处理。本章将深入探讨更高级的动画技术和物理效果,这些技术可以让你的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中实现高级动画和物理效果:

  1. 粒子系统:通过管理大量独立的粒子来创建复杂的视觉效果,如火焰、烟雾、雨滴等。

  2. 物理引擎:实现了一个简单的物理引擎,并学习了如何集成第三方物理引擎(如Matter.js)来模拟物理行为。

  3. 碰撞检测:学习了各种形状之间的碰撞检测算法,以及如何使用空间分区技术(四叉树)来优化大量物体的碰撞检测。

  4. 高级动画技术:实现了骨骼动画和布料模拟等高级动画效果。

这些技术可以帮助你创建更加生动、真实和引人入胜的Canvas应用。

9.6 练习

  1. 创建一个粒子系统,模拟雪花效果,包括风力影响和与地面的碰撞。

  2. 使用物理引擎实现一个简单的弹球游戏,包括障碍物和得分系统。

  3. 实现一个基于四叉树的碰撞检测系统,并创建一个包含数百个移动物体的演示。

  4. 创建一个简单的角色,使用骨骼动画实现走路、跑步和跳跃等动作。

  5. 实现一个旗帜飘动的效果,使用布料模拟技术,并添加风力影响。

在下一章中,我们将学习Canvas的性能优化和最佳实践,以确保你的应用在各种设备上都能流畅运行。