在前面的章节中,我们学习了Canvas的基础知识、绘图API、动画技术和交互事件处理等内容。随着Canvas应用的复杂度增加,性能问题变得越来越重要。本章将介绍Canvas性能优化的关键技术和最佳实践,帮助你创建高效、流畅的Canvas应用。

10.1 性能分析与测量

在进行优化之前,我们需要了解应用的性能瓶颈在哪里。这一节将介绍如何测量和分析Canvas应用的性能。

10.1.1 性能指标

以下是评估Canvas应用性能的关键指标:

  • 帧率(FPS):每秒渲染的帧数,通常60FPS被认为是流畅的。
  • CPU使用率:Canvas操作主要在CPU上执行,高CPU使用率可能导致应用卡顿。
  • 内存使用:过高的内存使用可能导致垃圾回收频繁,影响性能。
  • 渲染时间:每帧的渲染时间,理想情况下应小于16.67ms(60FPS)。

10.1.2 性能测量工具

浏览器开发者工具

现代浏览器提供了强大的性能分析工具:

// 使用Performance API测量代码执行时间
const startTime = performance.now();
// 执行需要测量的代码
drawComplexScene();
const endTime = performance.now();
console.log(`绘制场景耗时: ${endTime - startTime} ms`);

自定义FPS计数器

class FPSCounter {
  constructor() {
    this.fps = 0;
    this.frames = 0;
    this.lastTime = performance.now();
  }

  update() {
    this.frames++;
    const now = performance.now();
    const delta = now - this.lastTime;
    
    if (delta >= 1000) {
      this.fps = Math.round((this.frames * 1000) / delta);
      this.lastTime = now;
      this.frames = 0;
    }
    
    return this.fps;
  }

  draw(ctx, x, y) {
    const fps = this.update();
    ctx.font = '16px Arial';
    ctx.fillStyle = fps > 30 ? 'green' : (fps > 15 ? 'yellow' : 'red');
    ctx.fillText(`FPS: ${fps}`, x, y);
  }
}

// 使用方法
const fpsCounter = new FPSCounter();

function animate() {
  requestAnimationFrame(animate);
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // 绘制场景
  drawScene();
  
  // 显示FPS
  fpsCounter.draw(ctx, 10, 20);
}

animate();

10.1.3 性能分析示例

以下是一个完整的性能分析示例,包括测量渲染时间和内存使用:

class PerformanceMonitor {
  constructor() {
    this.fpsCounter = new FPSCounter();
    this.renderTimes = [];
    this.memoryUsage = [];
    this.maxSamples = 60; // 保存最近60帧的数据
  }

  startFrame() {
    this.frameStartTime = performance.now();
  }

  endFrame(ctx, x, y) {
    const renderTime = performance.now() - this.frameStartTime;
    this.renderTimes.push(renderTime);
    
    // 如果浏览器支持内存信息API
    if (performance.memory) {
      this.memoryUsage.push(performance.memory.usedJSHeapSize / (1024 * 1024));
    }
    
    // 保持数组长度
    if (this.renderTimes.length > this.maxSamples) {
      this.renderTimes.shift();
    }
    if (this.memoryUsage.length > this.maxSamples) {
      this.memoryUsage.shift();
    }
    
    // 计算平均渲染时间
    const avgRenderTime = this.renderTimes.reduce((a, b) => a + b, 0) / this.renderTimes.length;
    
    // 显示性能信息
    ctx.font = '14px Arial';
    ctx.fillStyle = 'black';
    ctx.fillText(`FPS: ${this.fpsCounter.update()}`, x, y);
    ctx.fillText(`平均渲染时间: ${avgRenderTime.toFixed(2)} ms`, x, y + 20);
    
    if (performance.memory) {
      const avgMemory = this.memoryUsage.reduce((a, b) => a + b, 0) / this.memoryUsage.length;
      ctx.fillText(`内存使用: ${avgMemory.toFixed(2)} MB`, x, y + 40);
    }
  }

  // 绘制性能图表
  drawChart(ctx, x, y, width, height) {
    // 绘制渲染时间图表
    ctx.strokeStyle = 'blue';
    ctx.beginPath();
    
    const maxTime = Math.max(...this.renderTimes, 16.67) * 1.2; // 最大值,至少包含16.67ms
    
    for (let i = 0; i < this.renderTimes.length; i++) {
      const xPos = x + (i / this.maxSamples) * width;
      const yPos = y + height - (this.renderTimes[i] / maxTime) * height;
      
      if (i === 0) {
        ctx.moveTo(xPos, yPos);
      } else {
        ctx.lineTo(xPos, yPos);
      }
    }
    
    ctx.stroke();
    
    // 绘制16.67ms参考线(60FPS)
    const refLineY = y + height - (16.67 / maxTime) * height;
    ctx.strokeStyle = 'red';
    ctx.beginPath();
    ctx.moveTo(x, refLineY);
    ctx.lineTo(x + width, refLineY);
    ctx.stroke();
    
    ctx.fillStyle = 'red';
    ctx.fillText('16.67ms (60FPS)', x + width + 5, refLineY + 5);
  }
}

// 使用方法
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const perfMonitor = new PerformanceMonitor();

function animate() {
  requestAnimationFrame(animate);
  
  perfMonitor.startFrame();
  
  // 清除画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // 绘制场景
  drawScene();
  
  // 结束帧并显示性能信息
  perfMonitor.endFrame(ctx, 10, 20);
  
  // 绘制性能图表
  perfMonitor.drawChart(ctx, 10, 70, 300, 100);
}

animate();

10.2 渲染优化策略

10.2.1 分层渲染与缓存

一个重要的优化策略是使用多个Canvas层和离屏渲染来减少重绘开销。

多层Canvas

<div class="canvas-container" style="position: relative;">
  <canvas id="background" style="position: absolute; left: 0; top: 0;"></canvas>
  <canvas id="main" style="position: absolute; left: 0; top: 0;"></canvas>
  <canvas id="ui" style="position: absolute; left: 0; top: 0;"></canvas>
</div>
const backgroundCanvas = document.getElementById('background');
const mainCanvas = document.getElementById('main');
const uiCanvas = document.getElementById('ui');

const bgCtx = backgroundCanvas.getContext('2d');
const mainCtx = mainCanvas.getContext('2d');
const uiCtx = uiCanvas.getContext('2d');

// 设置所有Canvas的尺寸
function resizeCanvases() {
  const width = window.innerWidth;
  const height = window.innerHeight;
  
  [backgroundCanvas, mainCanvas, uiCanvas].forEach(canvas => {
    canvas.width = width;
    canvas.height = height;
  });
  
  // 重新绘制背景(只需在尺寸变化或背景变化时绘制)
  drawBackground();
}

// 绘制静态背景
function drawBackground() {
  // 绘制不经常变化的背景元素
  bgCtx.fillStyle = 'skyblue';
  bgCtx.fillRect(0, 0, backgroundCanvas.width, backgroundCanvas.height);
  
  // 绘制山脉、树木等静态元素
  drawMountains(bgCtx);
  drawTrees(bgCtx);
}

// 主循环只更新需要动画的元素
function animate() {
  requestAnimationFrame(animate);
  
  // 清除动态内容的Canvas
  mainCtx.clearRect(0, 0, mainCanvas.width, mainCanvas.height);
  
  // 绘制动态游戏元素
  drawGameObjects(mainCtx);
  
  // 更新UI(如果需要)
  updateUI(uiCtx);
}

// 初始化
window.addEventListener('resize', resizeCanvases);
resizeCanvases();
animate();

离屏渲染与缓存

对于不经常变化的复杂图形,可以使用离屏Canvas进行预渲染:

class CachedDrawing {
  constructor(drawFunction, width, height) {
    this.cacheCanvas = document.createElement('canvas');
    this.cacheCanvas.width = width;
    this.cacheCanvas.height = height;
    this.cacheCtx = this.cacheCanvas.getContext('2d');
    
    // 执行绘制函数,将结果缓存到离屏Canvas
    drawFunction(this.cacheCtx);
  }
  
  // 在目标Canvas上绘制缓存的图像
  draw(ctx, x, y, width, height) {
    width = width || this.cacheCanvas.width;
    height = height || this.cacheCanvas.height;
    ctx.drawImage(this.cacheCanvas, x, y, width, height);
  }
  
  // 更新缓存(当需要改变时)
  update(drawFunction) {
    this.cacheCtx.clearRect(0, 0, this.cacheCanvas.width, this.cacheCanvas.height);
    drawFunction(this.cacheCtx);
  }
}

// 使用示例:缓存一个复杂的树
const treeCache = new CachedDrawing((ctx) => {
  // 绘制复杂的树
  drawDetailedTree(ctx, 0, 0, 100, 200);
}, 100, 200);

// 在主循环中使用缓存的树
function drawScene() {
  // 绘制50棵相同的树,但只需渲染一次
  for (let i = 0; i < 50; i++) {
    const x = Math.random() * canvas.width;
    const y = Math.random() * canvas.height;
    treeCache.draw(ctx, x, y);
  }
}

10.2.2 批量处理与减少状态更改

每次更改Canvas状态(如填充颜色、线宽等)都会产生开销。通过批量处理相似的绘制操作,可以减少状态更改:

// 低效方式:频繁切换状态
function drawCirclesInefficient(ctx, circles) {
  circles.forEach(circle => {
    ctx.beginPath();
    ctx.fillStyle = circle.color;
    ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
    ctx.fill();
  });
}

// 优化方式:按颜色分组批量处理
function drawCirclesOptimized(ctx, circles) {
  // 按颜色分组
  const circlesByColor = {};
  
  circles.forEach(circle => {
    if (!circlesByColor[circle.color]) {
      circlesByColor[circle.color] = [];
    }
    circlesByColor[circle.color].push(circle);
  });
  
  // 按颜色批量绘制
  Object.keys(circlesByColor).forEach(color => {
    ctx.fillStyle = color;
    ctx.beginPath();
    
    circlesByColor[color].forEach(circle => {
      ctx.moveTo(circle.x + circle.radius, circle.y);
      ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
    });
    
    ctx.fill();
  });
}

10.2.3 使用适当的绘图API

选择合适的绘图API可以显著提高性能:

// 绘制大量矩形的性能比较
function drawRectanglesComparison() {
  const count = 1000;
  const rectangles = [];
  
  // 生成随机矩形
  for (let i = 0; i < count; i++) {
    rectangles.push({
      x: Math.random() * canvas.width,
      y: Math.random() * canvas.height,
      width: 10 + Math.random() * 40,
      height: 10 + Math.random() * 40,
      color: `rgb(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)})`
    });
  }
  
  // 方法1: fillRect API
  function drawWithFillRect() {
    rectangles.forEach(rect => {
      ctx.fillStyle = rect.color;
      ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
    });
  }
  
  // 方法2: rect + fill API
  function drawWithRectAndFill() {
    rectangles.forEach(rect => {
      ctx.fillStyle = rect.color;
      ctx.beginPath();
      ctx.rect(rect.x, rect.y, rect.width, rect.height);
      ctx.fill();
    });
  }
  
  // 方法3: 路径API
  function drawWithPath() {
    rectangles.forEach(rect => {
      ctx.fillStyle = rect.color;
      ctx.beginPath();
      ctx.moveTo(rect.x, rect.y);
      ctx.lineTo(rect.x + rect.width, rect.y);
      ctx.lineTo(rect.x + rect.width, rect.y + rect.height);
      ctx.lineTo(rect.x, rect.y + rect.height);
      ctx.closePath();
      ctx.fill();
    });
  }
  
  // 方法4: 批量处理(按颜色分组)
  function drawWithBatching() {
    const rectsByColor = {};
    
    rectangles.forEach(rect => {
      if (!rectsByColor[rect.color]) {
        rectsByColor[rect.color] = [];
      }
      rectsByColor[rect.color].push(rect);
    });
    
    Object.keys(rectsByColor).forEach(color => {
      ctx.fillStyle = color;
      rectsByColor[color].forEach(rect => {
        ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
      });
    });
  }
  
  // 性能测试
  console.time('fillRect');
  drawWithFillRect();
  console.timeEnd('fillRect');
  
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  console.time('rect+fill');
  drawWithRectAndFill();
  console.timeEnd('rect+fill');
  
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  console.time('path');
  drawWithPath();
  console.timeEnd('path');
  
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  console.time('batching');
  drawWithBatching();
  console.timeEnd('batching');
}

10.3 内存管理与垃圾回收优化

10.3.1 对象池模式

对象池模式可以减少垃圾回收的频率,特别是在需要频繁创建和销毁对象的场景中:

class ParticlePool {
  constructor(size) {
    this.pool = [];
    this.activeParticles = [];
    
    // 预创建粒子对象
    for (let i = 0; i < size; i++) {
      this.pool.push(this.createParticle());
    }
  }
  
  createParticle() {
    return {
      x: 0,
      y: 0,
      vx: 0,
      vy: 0,
      color: 'white',
      size: 2,
      life: 0,
      maxLife: 60,
      active: false,
      reset: function() {
        this.x = 0;
        this.y = 0;
        this.vx = 0;
        this.vy = 0;
        this.color = 'white';
        this.size = 2;
        this.life = 0;
        this.maxLife = 60;
        this.active = false;
      }
    };
  }
  
  // 获取一个粒子
  get() {
    // 如果池中有可用粒子,则使用它
    if (this.pool.length > 0) {
      const particle = this.pool.pop();
      particle.active = true;
      this.activeParticles.push(particle);
      return particle;
    }
    
    // 如果没有可用粒子,创建一个新的
    console.warn('Particle pool depleted, creating new particle');
    const particle = this.createParticle();
    particle.active = true;
    this.activeParticles.push(particle);
    return particle;
  }
  
  // 释放一个粒子回池
  release(particle) {
    const index = this.activeParticles.indexOf(particle);
    if (index !== -1) {
      this.activeParticles.splice(index, 1);
      particle.reset();
      this.pool.push(particle);
    }
  }
  
  // 更新所有活动粒子
  update() {
    for (let i = this.activeParticles.length - 1; i >= 0; i--) {
      const particle = this.activeParticles[i];
      
      particle.life++;
      
      // 如果粒子寿命结束,释放回池
      if (particle.life >= particle.maxLife) {
        this.release(particle);
        continue;
      }
      
      // 更新粒子位置
      particle.x += particle.vx;
      particle.y += particle.vy;
    }
  }
  
  // 绘制所有活动粒子
  draw(ctx) {
    ctx.save();
    
    this.activeParticles.forEach(particle => {
      const alpha = 1 - (particle.life / particle.maxLife);
      
      ctx.globalAlpha = alpha;
      ctx.fillStyle = particle.color;
      ctx.beginPath();
      ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
      ctx.fill();
    });
    
    ctx.restore();
  }
}

// 使用示例
const particlePool = new ParticlePool(1000);

// 创建爆炸效果
function createExplosion(x, y, count) {
  for (let i = 0; i < count; i++) {
    const particle = particlePool.get();
    
    particle.x = x;
    particle.y = y;
    particle.vx = (Math.random() - 0.5) * 5;
    particle.vy = (Math.random() - 0.5) * 5;
    particle.size = 1 + Math.random() * 3;
    particle.color = `hsl(${Math.random() * 360}, 100%, 50%)`;
    particle.maxLife = 30 + Math.random() * 60;
  }
}

// 在动画循环中
function animate() {
  requestAnimationFrame(animate);
  
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // 更新和绘制粒子
  particlePool.update();
  particlePool.draw(ctx);
  
  // 随机创建爆炸
  if (Math.random() < 0.05) {
    createExplosion(
      Math.random() * canvas.width,
      Math.random() * canvas.height,
      50
    );
  }
}

10.3.2 避免闭包和临时对象

在动画循环中,应避免创建临时对象和闭包,以减少垃圾回收:

// 低效方式:每帧创建新对象
function updateInefficient() {
  entities.forEach(entity => {
    // 每次调用都创建新对象
    const velocity = { x: entity.speed * Math.cos(entity.angle), y: entity.speed * Math.sin(entity.angle) };
    entity.x += velocity.x;
    entity.y += velocity.y;
  });
}

// 优化方式:重用对象
const tempVelocity = { x: 0, y: 0 };

function updateOptimized() {
  entities.forEach(entity => {
    // 重用同一个对象
    tempVelocity.x = entity.speed * Math.cos(entity.angle);
    tempVelocity.y = entity.speed * Math.sin(entity.angle);
    entity.x += tempVelocity.x;
    entity.y += tempVelocity.y;
  });
}

10.3.3 使用TypedArray和二进制操作

对于大量数据处理,使用TypedArray可以提高性能:

// 使用TypedArray存储粒子数据
class OptimizedParticleSystem {
  constructor(maxParticles) {
    this.maxParticles = maxParticles;
    this.count = 0;
    
    // 使用TypedArray存储粒子属性
    this.positions = new Float32Array(maxParticles * 2); // x, y
    this.velocities = new Float32Array(maxParticles * 2); // vx, vy
    this.colors = new Uint32Array(maxParticles); // ARGB格式
    this.sizes = new Float32Array(maxParticles);
    this.lives = new Float32Array(maxParticles * 2); // currentLife, maxLife
  }
  
  addParticle(x, y, vx, vy, color, size, maxLife) {
    if (this.count >= this.maxParticles) return false;
    
    const i = this.count++;
    
    // 设置粒子属性
    this.positions[i * 2] = x;
    this.positions[i * 2 + 1] = y;
    
    this.velocities[i * 2] = vx;
    this.velocities[i * 2 + 1] = vy;
    
    this.colors[i] = color;
    this.sizes[i] = size;
    
    this.lives[i * 2] = 0; // currentLife
    this.lives[i * 2 + 1] = maxLife; // maxLife
    
    return true;
  }
  
  update() {
    let aliveCount = 0;
    
    for (let i = 0; i < this.count; i++) {
      const currentLifeIdx = i * 2;
      const maxLifeIdx = currentLifeIdx + 1;
      
      // 增加生命值
      this.lives[currentLifeIdx] += 1;
      
      // 检查粒子是否存活
      if (this.lives[currentLifeIdx] >= this.lives[maxLifeIdx]) {
        continue;
      }
      
      // 更新位置
      const posIdx = i * 2;
      const velIdx = i * 2;
      
      this.positions[posIdx] += this.velocities[velIdx];
      this.positions[posIdx + 1] += this.velocities[velIdx + 1];
      
      // 如果粒子仍然存活,将其移动到活动数组的前部
      if (i !== aliveCount) {
        // 交换位置
        this.positions[aliveCount * 2] = this.positions[posIdx];
        this.positions[aliveCount * 2 + 1] = this.positions[posIdx + 1];
        
        // 交换速度
        this.velocities[aliveCount * 2] = this.velocities[velIdx];
        this.velocities[aliveCount * 2 + 1] = this.velocities[velIdx + 1];
        
        // 交换颜色和大小
        this.colors[aliveCount] = this.colors[i];
        this.sizes[aliveCount] = this.sizes[i];
        
        // 交换生命值
        this.lives[aliveCount * 2] = this.lives[currentLifeIdx];
        this.lives[aliveCount * 2 + 1] = this.lives[maxLifeIdx];
      }
      
      aliveCount++;
    }
    
    this.count = aliveCount;
  }
  
  draw(ctx) {
    for (let i = 0; i < this.count; i++) {
      const posIdx = i * 2;
      const x = this.positions[posIdx];
      const y = this.positions[posIdx + 1];
      
      const size = this.sizes[i];
      
      // 从ARGB格式解析颜色
      const color = this.colors[i];
      const r = (color >> 16) & 0xFF;
      const g = (color >> 8) & 0xFF;
      const b = color & 0xFF;
      const a = ((color >> 24) & 0xFF) / 255;
      
      // 计算透明度衰减
      const lifeRatio = this.lives[i * 2] / this.lives[i * 2 + 1];
      const alpha = 1 - lifeRatio;
      
      ctx.globalAlpha = alpha * a;
      ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
      ctx.beginPath();
      ctx.arc(x, y, size, 0, Math.PI * 2);
      ctx.fill();
    }
    
    ctx.globalAlpha = 1;
  }
  
  // 创建爆炸效果
  createExplosion(x, y, count, minSpeed, maxSpeed) {
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const speed = minSpeed + Math.random() * (maxSpeed - minSpeed);
      
      const vx = Math.cos(angle) * speed;
      const vy = Math.sin(angle) * speed;
      
      // 生成随机颜色(ARGB格式)
      const r = Math.floor(Math.random() * 255);
      const g = Math.floor(Math.random() * 255);
      const b = Math.floor(Math.random() * 255);
      const color = (255 << 24) | (r << 16) | (g << 8) | b;
      
      const size = 1 + Math.random() * 3;
      const maxLife = 30 + Math.random() * 30;
      
      this.addParticle(x, y, vx, vy, color, size, maxLife);
    }
  }
}

// 使用示例
const particleSystem = new OptimizedParticleSystem(10000);

function animate() {
  requestAnimationFrame(animate);
  
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // 更新和绘制粒子
  particleSystem.update();
  particleSystem.draw(ctx);
  
  // 随机创建爆炸
  if (Math.random() < 0.05) {
    particleSystem.createExplosion(
      Math.random() * canvas.width,
      Math.random() * canvas.height,
      100,
      1,
      5
    );
  }
}

10.4 视口优化与可见性检测

10.4.1 视口裁剪

只渲染视口内可见的对象可以大幅提高性能:

class Viewport {
  constructor(canvas, worldWidth, worldHeight) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    
    // 世界尺寸
    this.worldWidth = worldWidth;
    this.worldHeight = worldHeight;
    
    // 视口位置和尺寸
    this.x = 0;
    this.y = 0;
    this.width = canvas.width;
    this.height = canvas.height;
    
    // 缩放级别
    this.scale = 1;
  }
  
  // 移动视口
  move(dx, dy) {
    this.x = Math.max(0, Math.min(this.x + dx, this.worldWidth - this.width / this.scale));
    this.y = Math.max(0, Math.min(this.y + dy, this.worldHeight - this.height / this.scale));
  }
  
  // 缩放视口
  zoom(factor, centerX, centerY) {
    // 保存缩放前的世界坐标
    const worldX = this.screenToWorldX(centerX);
    const worldY = this.screenToWorldY(centerY);
    
    // 应用缩放
    this.scale = Math.max(0.1, Math.min(5, this.scale * factor));
    
    // 调整视口位置,使缩放中心点保持在相同的屏幕位置
    this.x = worldX - centerX / this.scale;
    this.y = worldY - centerY / this.scale;
    
    // 确保视口不超出世界边界
    this.x = Math.max(0, Math.min(this.x, this.worldWidth - this.width / this.scale));
    this.y = Math.max(0, Math.min(this.y, this.worldHeight - this.height / this.scale));
  }
  
  // 屏幕坐标转世界坐标
  screenToWorldX(screenX) {
    return this.x + screenX / this.scale;
  }
  
  screenToWorldY(screenY) {
    return this.y + screenY / this.scale;
  }
  
  // 世界坐标转屏幕坐标
  worldToScreenX(worldX) {
    return (worldX - this.x) * this.scale;
  }
  
  worldToScreenY(worldY) {
    return (worldY - this.y) * this.scale;
  }
  
  // 检查对象是否在视口内
  isVisible(x, y, width, height) {
    return (
      x + width >= this.x &&
      x <= this.x + this.width / this.scale &&
      y + height >= this.y &&
      y <= this.y + this.height / this.scale
    );
  }
  
  // 开始绘制(应用视口变换)
  begin() {
    this.ctx.save();
    this.ctx.scale(this.scale, this.scale);
    this.ctx.translate(-this.x, -this.y);
  }
  
  // 结束绘制(恢复上下文状态)
  end() {
    this.ctx.restore();
  }
}

// 使用示例
const canvas = document.getElementById('canvas');
const worldWidth = 10000;
const worldHeight = 10000;
const viewport = new Viewport(canvas, worldWidth, worldHeight);

// 创建一些游戏对象
const gameObjects = [];
for (let i = 0; i < 10000; i++) {
  gameObjects.push({
    x: Math.random() * worldWidth,
    y: Math.random() * worldHeight,
    width: 20 + Math.random() * 50,
    height: 20 + Math.random() * 50,
    color: `hsl(${Math.random() * 360}, 70%, 50%)`
  });
}

// 渲染函数
function render() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  
  viewport.width = canvas.width;
  viewport.height = canvas.height;
  
  // 清除画布
  viewport.ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // 开始应用视口变换
  viewport.begin();
  
  // 绘制世界边界
  viewport.ctx.strokeStyle = 'black';
  viewport.ctx.strokeRect(0, 0, worldWidth, worldHeight);
  
  // 只绘制视口内的对象
  let visibleCount = 0;
  let totalCount = gameObjects.length;
  
  gameObjects.forEach(obj => {
    if (viewport.isVisible(obj.x, obj.y, obj.width, obj.height)) {
      viewport.ctx.fillStyle = obj.color;
      viewport.ctx.fillRect(obj.x, obj.y, obj.width, obj.height);
      visibleCount++;
    }
  });
  
  // 结束视口变换
  viewport.end();
  
  // 绘制UI(不受视口影响)
  viewport.ctx.fillStyle = 'black';
  viewport.ctx.font = '14px Arial';
  viewport.ctx.fillText(`可见对象: ${visibleCount}/${totalCount}`, 10, 20);
  viewport.ctx.fillText(`视口位置: (${viewport.x.toFixed(0)}, ${viewport.y.toFixed(0)})`, 10, 40);
  viewport.ctx.fillText(`缩放: ${viewport.scale.toFixed(2)}x`, 10, 60);
  
  requestAnimationFrame(render);
}

// 处理输入
let isDragging = false;
let lastMouseX = 0;
let lastMouseY = 0;

canvas.addEventListener('mousedown', (e) => {
  isDragging = true;
  lastMouseX = e.clientX;
  lastMouseY = e.clientY;
});

canvas.addEventListener('mousemove', (e) => {
  if (isDragging) {
    const dx = (lastMouseX - e.clientX) / viewport.scale;
    const dy = (lastMouseY - e.clientY) / viewport.scale;
    viewport.move(dx, dy);
    lastMouseX = e.clientX;
    lastMouseY = e.clientY;
  }
});

window.addEventListener('mouseup', () => {
  isDragging = false;
});

canvas.addEventListener('wheel', (e) => {
  e.preventDefault();
  const factor = e.deltaY < 0 ? 1.1 : 0.9;
  viewport.zoom(factor, e.clientX, e.clientY);
});

// 开始渲染
render();

10.4.2 四叉树空间分区

对于大型场景,使用四叉树可以加速碰撞检测和可见性检测:

class QuadTree {
  constructor(boundary, capacity) {
    this.boundary = boundary; // {x, y, width, height}
    this.capacity = capacity; // 每个节点的最大对象数
    this.objects = [];
    this.divided = false;
    this.children = [];
  }
  
  // 细分四叉树
  subdivide() {
    const x = this.boundary.x;
    const y = this.boundary.y;
    const w = this.boundary.width / 2;
    const h = this.boundary.height / 2;
    
    // 创建四个子节点
    this.children = [
      new QuadTree({x: x,     y: y,     width: w, height: h}, this.capacity), // 左上
      new QuadTree({x: x + w, y: y,     width: w, height: h}, this.capacity), // 右上
      new QuadTree({x: x,     y: y + h, width: w, height: h}, this.capacity), // 左下
      new QuadTree({x: x + w, y: y + h, width: w, height: h}, this.capacity)  // 右下
    ];
    
    this.divided = true;
    
    // 将现有对象重新分配到子节点
    for (let obj of this.objects) {
      this.insertToChildren(obj);
    }
    
    this.objects = [];
  }
  
  // 将对象插入到子节点
  insertToChildren(obj) {
    for (let child of this.children) {
      if (this.intersects(obj, child.boundary)) {
        child.insert(obj);
      }
    }
  }
  
  // 插入对象
  insert(obj) {
    // 如果对象不在边界内,不插入
    if (!this.intersects(obj, this.boundary)) {
      return false;
    }
    
    // 如果当前节点未满且未细分,直接添加
    if (this.objects.length < this.capacity && !this.divided) {
      this.objects.push(obj);
      return true;
    }
    
    // 如果未细分,则细分
    if (!this.divided) {
      this.subdivide();
    }
    
    // 将对象插入到子节点
    return this.insertToChildren(obj);
  }
  
  // 查询给定范围内的所有对象
  query(range, found = []) {
    // 如果查询范围与当前节点不相交,返回空数组
    if (!this.intersects(range, this.boundary)) {
      return found;
    }
    
    // 添加当前节点中的对象
    for (let obj of this.objects) {
      if (this.intersects(obj, range)) {
        found.push(obj);
      }
    }
    
    // 如果已细分,递归查询子节点
    if (this.divided) {
      for (let child of this.children) {
        child.query(range, found);
      }
    }
    
    return found;
  }
  
  // 检查两个矩形是否相交
  intersects(rectA, rectB) {
    return (
      rectA.x < rectB.x + rectB.width &&
      rectA.x + rectA.width > rectB.x &&
      rectA.y < rectB.y + rectB.height &&
      rectA.y + rectA.height > rectB.y
    );
  }
  
  // 清空四叉树
  clear() {
    this.objects = [];
    
    if (this.divided) {
      for (let child of this.children) {
        child.clear();
      }
      this.children = [];
      this.divided = false;
    }
  }
  
  // 绘制四叉树(用于调试)
  draw(ctx) {
    ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
    ctx.strokeRect(
      this.boundary.x,
      this.boundary.y,
      this.boundary.width,
      this.boundary.height
    );
    
    if (this.divided) {
      for (let child of this.children) {
        child.draw(ctx);
      }
    }
  }
}

// 使用示例
const worldWidth = 2000;
const worldHeight = 2000;
const quadTree = new QuadTree({x: 0, y: 0, width: worldWidth, height: worldHeight}, 4);

// 创建一些游戏对象
const gameObjects = [];
for (let i = 0; i < 1000; i++) {
  const obj = {
    id: i,
    x: Math.random() * worldWidth,
    y: Math.random() * worldHeight,
    width: 10 + Math.random() * 40,
    height: 10 + Math.random() * 40,
    color: `hsl(${Math.random() * 360}, 70%, 50%)`
  };
  gameObjects.push(obj);
}

// 在每一帧重建四叉树
function updateQuadTree() {
  quadTree.clear();
  
  for (let obj of gameObjects) {
    quadTree.insert(obj);
  }
}

// 使用四叉树进行可见性检测
function getVisibleObjects(viewport) {
  const viewportBounds = {
    x: viewport.x,
    y: viewport.y,
    width: viewport.width / viewport.scale,
    height: viewport.height / viewport.scale
  };
  
  return quadTree.query(viewportBounds);
}

// 渲染函数
function render() {
  // 更新四叉树
  updateQuadTree();
  
  // 获取可见对象
  const visibleObjects = getVisibleObjects(viewport);
  
  // 清除画布
  viewport.ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // 开始应用视口变换
  viewport.begin();
  
  // 绘制世界边界
  viewport.ctx.strokeStyle = 'black';
  viewport.ctx.strokeRect(0, 0, worldWidth, worldHeight);
  
  // 绘制四叉树(调试用)
  quadTree.draw(viewport.ctx);
  
  // 绘制可见对象
  for (let obj of visibleObjects) {
    viewport.ctx.fillStyle = obj.color;
    viewport.ctx.fillRect(obj.x, obj.y, obj.width, obj.height);
  }
  
  // 结束视口变换
  viewport.end();
  
  // 绘制UI
  viewport.ctx.fillStyle = 'black';
  viewport.ctx.font = '14px Arial';
  viewport.ctx.fillText(`可见对象: ${visibleObjects.length}/${gameObjects.length}`, 10, 20);
  
  requestAnimationFrame(render);
}

10.5 Canvas应用最佳实践

10.5.1 响应式Canvas

创建适应不同屏幕尺寸的Canvas应用:

class ResponsiveCanvas {
  constructor(canvasId) {
    this.canvas = document.getElementById(canvasId);
    this.ctx = this.canvas.getContext('2d');
    
    // 设置初始尺寸
    this.resize();
    
    // 监听窗口大小变化
    window.addEventListener('resize', this.resize.bind(this));
    
    // 设备像素比
    this.pixelRatio = window.devicePixelRatio || 1;
    
    // 应用高DPI支持
    this.applyHighDPI();
  }
  
  // 调整Canvas尺寸
  resize() {
    const parent = this.canvas.parentElement;
    
    // 如果父元素存在,使用父元素尺寸,否则使用窗口尺寸
    if (parent) {
      this.canvas.width = parent.clientWidth;
      this.canvas.height = parent.clientHeight;
    } else {
      this.canvas.width = window.innerWidth;
      this.canvas.height = window.innerHeight;
    }
    
    // 重新应用高DPI支持
    this.applyHighDPI();
    
    // 触发重绘
    this.draw();
  }
  
  // 应用高DPI支持
  applyHighDPI() {
    if (this.pixelRatio > 1) {
      const width = this.canvas.width;
      const height = this.canvas.height;
      
      // 设置Canvas元素的显示尺寸
      this.canvas.style.width = width + 'px';
      this.canvas.style.height = height + 'px';
      
      // 增加Canvas的实际像素数
      this.canvas.width = width * this.pixelRatio;
      this.canvas.height = height * this.pixelRatio;
      
      // 缩放上下文以匹配设备像素比
      this.ctx.scale(this.pixelRatio, this.pixelRatio);
    }
  }
  
  // 绘制方法(由子类实现)
  draw() {
    // 默认实现:清除画布
    this.ctx.clearRect(0, 0, this.canvas.width / this.pixelRatio, this.canvas.height / this.pixelRatio);
  }
  
  // 获取Canvas中心点
  getCenter() {
    return {
      x: this.canvas.width / this.pixelRatio / 2,
      y: this.canvas.height / this.pixelRatio / 2
    };
  }
  
  // 获取Canvas尺寸
  getSize() {
    return {
      width: this.canvas.width / this.pixelRatio,
      height: this.canvas.height / this.pixelRatio
    };
  }
}

// 使用示例
class MyCanvasApp extends ResponsiveCanvas {
  constructor(canvasId) {
    super(canvasId);
    
    // 初始化应用状态
    this.circles = [];
    for (let i = 0; i < 50; i++) {
      this.circles.push({
        x: Math.random() * this.getSize().width,
        y: Math.random() * this.getSize().height,
        radius: 10 + Math.random() * 40,
        color: `hsl(${Math.random() * 360}, 70%, 50%)`
      });
    }
    
    // 开始动画循环
    this.animate();
  }
  
  // 动画循环
  animate() {
    requestAnimationFrame(this.animate.bind(this));
    this.draw();
  }
  
  // 重写绘制方法
  draw() {
    // 清除画布
    super.draw();
    
    // 获取当前Canvas尺寸
    const size = this.getSize();
    
    // 绘制背景
    const gradient = this.ctx.createLinearGradient(0, 0, 0, size.height);
    gradient.addColorStop(0, '#f5f7fa');
    gradient.addColorStop(1, '#c3cfe2');
    
    this.ctx.fillStyle = gradient;
    this.ctx.fillRect(0, 0, size.width, size.height);
    
    // 绘制圆形
    this.circles.forEach(circle => {
      // 确保圆形在Canvas范围内
      if (circle.x < -circle.radius) circle.x = size.width + circle.radius;
      if (circle.x > size.width + circle.radius) circle.x = -circle.radius;
      if (circle.y < -circle.radius) circle.y = size.height + circle.radius;
      if (circle.y > size.height + circle.radius) circle.y = -circle.radius;
      
      // 移动圆形
      circle.x += (Math.random() - 0.5) * 2;
      circle.y += (Math.random() - 0.5) * 2;
      
      // 绘制圆形
      this.ctx.beginPath();
      this.ctx.fillStyle = circle.color;
      this.ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
      this.ctx.fill();
    });
    
    // 绘制文本
    const center = this.getCenter();
    this.ctx.font = 'bold 24px Arial';
    this.ctx.textAlign = 'center';
    this.ctx.textBaseline = 'middle';
    this.ctx.fillStyle = 'white';
    this.ctx.fillText('响应式Canvas示例', center.x, center.y);
  }
}

// 创建应用实例
const app = new MyCanvasApp('canvas');

10.5.2 模块化与架构设计

对于复杂的Canvas应用,采用模块化架构可以提高代码的可维护性:

// 游戏引擎架构示例

// 基础游戏对象类
class GameObject {
  constructor(x, y, width, height) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.rotation = 0;
    this.scale = { x: 1, y: 1 };
    this.visible = true;
    this.children = [];
  }
  
  update(deltaTime) {
    // 更新子对象
    this.children.forEach(child => child.update(deltaTime));
  }
  
  draw(ctx) {
    if (!this.visible) return;
    
    ctx.save();
    
    // 应用变换
    ctx.translate(this.x, this.y);
    ctx.rotate(this.rotation);
    ctx.scale(this.scale.x, this.scale.y);
    
    // 绘制自身
    this.drawSelf(ctx);
    
    // 绘制子对象
    this.children.forEach(child => child.draw(ctx));
    
    ctx.restore();
  }
  
  drawSelf(ctx) {
    // 由子类实现
  }
  
  addChild(gameObject) {
    this.children.push(gameObject);
  }
  
  removeChild(gameObject) {
    const index = this.children.indexOf(gameObject);
    if (index !== -1) {
      this.children.splice(index, 1);
    }
  }
}

// 场景类
class Scene extends GameObject {
  constructor() {
    super(0, 0, 0, 0);
    this.layers = {};
  }
  
  addToLayer(layerName, gameObject) {
    if (!this.layers[layerName]) {
      this.layers[layerName] = [];
    }
    
    this.layers[layerName].push(gameObject);
  }
  
  update(deltaTime) {
    // 更新所有层中的对象
    Object.values(this.layers).forEach(layer => {
      layer.forEach(obj => obj.update(deltaTime));
    });
  }
  
  draw(ctx) {
    // 按层绘制对象
    Object.keys(this.layers).forEach(layerName => {
      this.layers[layerName].forEach(obj => obj.draw(ctx));
    });
  }
}

// 游戏引擎类
class GameEngine {
  constructor(canvasId) {
    this.canvas = document.getElementById(canvasId);
    this.ctx = this.canvas.getContext('2d');
    
    // 设置Canvas尺寸
    this.resizeCanvas();
    window.addEventListener('resize', this.resizeCanvas.bind(this));
    
    // 游戏状态
    this.scenes = {};
    this.currentScene = null;
    
    // 时间管理
    this.lastTime = 0;
    this.deltaTime = 0;
    
    // 输入管理
    this.input = new InputManager(this.canvas);
    
    // 资源管理
    this.resources = new ResourceManager();
    
    // 性能监控
    this.perfMonitor = new PerformanceMonitor();
  }
  
  resizeCanvas() {
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;
  }
  
  addScene(name, scene) {
    this.scenes[name] = scene;
  }
  
  setCurrentScene(name) {
    if (this.scenes[name]) {
      this.currentScene = this.scenes[name];
    }
  }
  
  start() {
    // 开始游戏循环
    requestAnimationFrame(this.gameLoop.bind(this));
  }
  
  gameLoop(timestamp) {
    // 计算时间增量
    this.deltaTime = (timestamp - this.lastTime) / 1000; // 转换为秒
    this.lastTime = timestamp;
    
    // 开始性能监控
    this.perfMonitor.startFrame();
    
    // 更新输入状态
    this.input.update();
    
    // 更新当前场景
    if (this.currentScene) {
      this.currentScene.update(this.deltaTime);
    }
    
    // 清除画布
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    
    // 绘制当前场景
    if (this.currentScene) {
      this.currentScene.draw(this.ctx);
    }
    
    // 结束性能监控并显示
    this.perfMonitor.endFrame(this.ctx, 10, 20);
    
    // 继续游戏循环
    requestAnimationFrame(this.gameLoop.bind(this));
  }
}

// 输入管理器
class InputManager {
  constructor(canvas) {
    this.canvas = canvas;
    
    // 键盘状态
    this.keys = {};
    
    // 鼠标状态
    this.mousePosition = { x: 0, y: 0 };
    this.mouseButtons = { left: false, middle: false, right: false };
    
    // 触摸状态
    this.touches = [];
    
    // 注册事件监听器
    this.registerEventListeners();
  }
  
  registerEventListeners() {
    // 键盘事件
    window.addEventListener('keydown', this.handleKeyDown.bind(this));
    window.addEventListener('keyup', this.handleKeyUp.bind(this));
    
    // 鼠标事件
    this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
    this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
    this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
    
    // 触摸事件
    this.canvas.addEventListener('touchstart', this.handleTouchStart.bind(this));
    this.canvas.addEventListener('touchmove', this.handleTouchMove.bind(this));
    this.canvas.addEventListener('touchend', this.handleTouchEnd.bind(this));
  }
  
  // 键盘事件处理
  handleKeyDown(e) {
    this.keys[e.code] = true;
  }
  
  handleKeyUp(e) {
    this.keys[e.code] = false;
  }
  
  // 鼠标事件处理
  handleMouseMove(e) {
    const rect = this.canvas.getBoundingClientRect();
    this.mousePosition = {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    };
  }
  
  handleMouseDown(e) {
    switch (e.button) {
      case 0: this.mouseButtons.left = true; break;
      case 1: this.mouseButtons.middle = true; break;
      case 2: this.mouseButtons.right = true; break;
    }
  }
  
  handleMouseUp(e) {
    switch (e.button) {
      case 0: this.mouseButtons.left = false; break;
      case 1: this.mouseButtons.middle = false; break;
      case 2: this.mouseButtons.right = false; break;
    }
  }
  
  // 触摸事件处理
  handleTouchStart(e) {
    e.preventDefault();
    const rect = this.canvas.getBoundingClientRect();
    
    this.touches = Array.from(e.touches).map(touch => ({
      id: touch.identifier,
      x: touch.clientX - rect.left,
      y: touch.clientY - rect.top
    }));
    
    // 模拟鼠标左键按下
    if (this.touches.length > 0) {
      this.mouseButtons.left = true;
      this.mousePosition.x = this.touches[0].x;
      this.mousePosition.y = this.touches[0].y;
    }
  }
  
  handleTouchMove(e) {
    e.preventDefault();
    const rect = this.canvas.getBoundingClientRect();
    
    this.touches = Array.from(e.touches).map(touch => ({
      id: touch.identifier,
      x: touch.clientX - rect.left,
      y: touch.clientY - rect.top
    }));
    
    // 更新鼠标位置
    if (this.touches.length > 0) {
      this.mousePosition.x = this.touches[0].x;
      this.mousePosition.y = this.touches[0].y;
    }
  }
  
  handleTouchEnd(e) {
    e.preventDefault();
    
    // 更新触摸列表
    const activeTouchIds = Array.from(e.touches).map(touch => touch.identifier);
    this.touches = this.touches.filter(touch => activeTouchIds.includes(touch.id));
    
    // 如果没有触摸点,模拟鼠标左键释放
    if (this.touches.length === 0) {
      this.mouseButtons.left = false;
    }
  }
  
  // 检查按键是否按下
  isKeyDown(keyCode) {
    return this.keys[keyCode] === true;
  }
  
  // 检查鼠标按钮是否按下
  isMouseButtonDown(button) {
    return this.mouseButtons[button] === true;
  }
  
  // 获取鼠标位置
  getMousePosition() {
    return { ...this.mousePosition };
  }
  
  // 获取触摸点
  getTouches() {
    return [...this.touches];
  }
  
  // 更新方法(每帧调用)
  update() {
    // 可以在这里添加额外的输入处理逻辑
  }
}

// 资源管理器
class ResourceManager {
  constructor() {
    this.resources = {
      images: {},
      audio: {},
      json: {}
    };
    
    this.loadingPromises = [];
  }
  
  // 加载图像
  loadImage(key, url) {
    const promise = new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => {
        this.resources.images[key] = img;
        resolve(img);
      };
      img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
      img.src = url;
    });
    
    this.loadingPromises.push(promise);
    return promise;
  }
  
  // 加载音频
  loadAudio(key, url) {
    const promise = new Promise((resolve, reject) => {
      const audio = new Audio();
      audio.oncanplaythrough = () => {
        this.resources.audio[key] = audio;
        resolve(audio);
      };
      audio.onerror = () => reject(new Error(`Failed to load audio: ${url}`));
      audio.src = url;
    });
    
    this.loadingPromises.push(promise);
    return promise;
  }
  
  // 加载JSON
  loadJSON(key, url) {
    const promise = fetch(url)
      .then(response => response.json())
      .then(data => {
        this.resources.json[key] = data;
        return data;
      });
    
    this.loadingPromises.push(promise);
    return promise;
  }
  
  // 等待所有资源加载完成
  waitForAll() {
    return Promise.all(this.loadingPromises);
  }
  
  // 获取资源
  getImage(key) {
    return this.resources.images[key];
  }
  
  getAudio(key) {
    return this.resources.audio[key];
  }
  
  getJSON(key) {
    return this.resources.json[key];
  }
}

// 使用示例:创建一个简单的游戏
class SimpleGame {
  constructor(canvasId) {
    // 创建游戏引擎
    this.engine = new GameEngine(canvasId);
    
    // 加载资源
    this.loadResources();
  }
  
  async loadResources() {
    // 加载游戏资源
    this.engine.resources.loadImage('player', 'assets/player.png');
    this.engine.resources.loadImage('enemy', 'assets/enemy.png');
    this.engine.resources.loadImage('background', 'assets/background.png');
    
    try {
      // 等待所有资源加载完成
      await this.engine.resources.waitForAll();
      
      // 初始化游戏
      this.initGame();
    } catch (error) {
      console.error('资源加载失败:', error);
    }
  }
  
  initGame() {
    // 创建游戏场景
    const mainScene = new Scene();
    
    // 添加背景
    const background = new BackgroundObject(
      0, 0,
      this.engine.canvas.width,
      this.engine.canvas.height,
      this.engine.resources.getImage('background')
    );
    mainScene.addToLayer('background', background);
    
    // 添加玩家
    const player = new PlayerObject(
      this.engine.canvas.width / 2,
      this.engine.canvas.height / 2,
      50, 50,
      this.engine.resources.getImage('player')
    );
    mainScene.addToLayer('entities', player);
    
    // 添加敌人
    for (let i = 0; i < 10; i++) {
      const enemy = new EnemyObject(
        Math.random() * this.engine.canvas.width,
        Math.random() * this.engine.canvas.height,
        40, 40,
        this.engine.resources.getImage('enemy')
      );
      mainScene.addToLayer('entities', enemy);
    }
    
    // 添加UI
    const ui = new UIObject(10, 10);
    mainScene.addToLayer('ui', ui);
    
    // 将场景添加到引擎
    this.engine.addScene('main', mainScene);
    this.engine.setCurrentScene('main');
    
    // 开始游戏循环
    this.engine.start();
  }
}

// 背景对象
class BackgroundObject extends GameObject {
  constructor(x, y, width, height, image) {
    super(x, y, width, height);
    this.image = image;
  }
  
  drawSelf(ctx) {
    if (this.image) {
      ctx.drawImage(this.image, 0, 0, this.width, this.height);
    } else {
      // 如果图像未加载,绘制一个简单的背景
      const gradient = ctx.createLinearGradient(0, 0, 0, this.height);
      gradient.addColorStop(0, '#87CEEB');
      gradient.addColorStop(1, '#E0F7FA');
      
      ctx.fillStyle = gradient;
      ctx.fillRect(0, 0, this.width, this.height);
    }
  }
}

// 玩家对象
class PlayerObject extends GameObject {
  constructor(x, y, width, height, image) {
    super(x, y, width, height);
    this.image = image;
    this.speed = 200; // 像素/秒
  }
  
  update(deltaTime) {
    // 获取输入管理器
    const input = this.scene.engine.input;
    
    // 处理键盘输入
    let dx = 0;
    let dy = 0;
    
    if (input.isKeyDown('ArrowUp') || input.isKeyDown('KeyW')) {
      dy -= this.speed * deltaTime;
    }
    if (input.isKeyDown('ArrowDown') || input.isKeyDown('KeyS')) {
      dy += this.speed * deltaTime;
    }
    if (input.isKeyDown('ArrowLeft') || input.isKeyDown('KeyA')) {
      dx -= this.speed * deltaTime;
    }
    if (input.isKeyDown('ArrowRight') || input.isKeyDown('KeyD')) {
      dx += this.speed * deltaTime;
    }
    
    // 更新位置
    this.x += dx;
    this.y += dy;
    
    // 限制在画布范围内
    const canvas = this.scene.engine.canvas;
    this.x = Math.max(0, Math.min(this.x, canvas.width - this.width));
    this.y = Math.max(0, Math.min(this.y, canvas.height - this.height));
    
    super.update(deltaTime);
  }
  
  drawSelf(ctx) {
    if (this.image) {
      ctx.drawImage(this.image, -this.width / 2, -this.height / 2, this.width, this.height);
    } else {
      // 如果图像未加载,绘制一个简单的形状
      ctx.fillStyle = 'blue';
      ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
    }
  }
}

// 敌人对象
class EnemyObject extends GameObject {
  constructor(x, y, width, height, image) {
    super(x, y, width, height);
    this.image = image;
    this.speed = 100; // 像素/秒
    this.rotationSpeed = Math.PI; // 弧度/秒
  }
  
  update(deltaTime) {
    // 旋转
    this.rotation += this.rotationSpeed * deltaTime;
    
    // 简单的移动模式
    this.x += Math.cos(this.rotation) * this.speed * deltaTime;
    this.y += Math.sin(this.rotation) * this.speed * deltaTime;
    
    // 如果超出边界,反弹
    const canvas = this.scene.engine.canvas;
    
    if (this.x < 0 || this.x > canvas.width - this.width) {
      this.speed *= -1;
    }
    if (this.y < 0 || this.y > canvas.height - this.height) {
      this.speed *= -1;
    }
    
    super.update(deltaTime);
  }
  
  drawSelf(ctx) {
    if (this.image) {
      ctx.drawImage(this.image, -this.width / 2, -this.height / 2, this.width, this.height);
    } else {
      // 如果图像未加载,绘制一个简单的形状
      ctx.fillStyle = 'red';
      ctx.beginPath();
      ctx.moveTo(0, -this.height / 2);
      ctx.lineTo(this.width / 2, this.height / 2);
      ctx.lineTo(-this.width / 2, this.height / 2);
      ctx.closePath();
      ctx.fill();
    }
  }
}

// UI对象
class UIObject extends GameObject {
  constructor(x, y) {
    super(x, y, 0, 0);
    this.fps = 0;
    this.frameCount = 0;
    this.lastFpsUpdate = 0;
  }
  
  update(deltaTime) {
    // 更新FPS计数
    this.frameCount++;
    
    const now = performance.now();
    if (now - this.lastFpsUpdate >= 1000) {
      this.fps = Math.round((this.frameCount * 1000) / (now - this.lastFpsUpdate));
      this.frameCount = 0;
      this.lastFpsUpdate = now;
    }
    
    super.update(deltaTime);
  }
  
  drawSelf(ctx) {
    ctx.font = '16px Arial';
    ctx.fillStyle = 'black';
    ctx.textAlign = 'left';
    ctx.textBaseline = 'top';
    
    ctx.fillText(`FPS: ${this.fps}`, 0, 0);
    ctx.fillText(`按方向键或WASD移动`, 0, 20);
  }
}

// 创建游戏实例
// const game = new SimpleGame('canvas');

10.6 小结与最佳实践

在本章中,我们学习了Canvas性能优化和最佳实践的关键技术。以下是一些重要的总结点:

10.6.1 性能优化核心原则

  1. 测量先于优化:使用性能分析工具确定瓶颈,避免过早优化。
  2. 减少状态更改:批量处理相似的绘图操作,减少Canvas状态切换。
  3. 使用分层渲染:将静态和动态内容分离,减少重绘区域。
  4. 离屏渲染与缓存:预渲染复杂图形,减少每帧的绘制开销。
  5. 视口裁剪:只渲染可见区域内的对象,使用空间分区技术优化大型场景。
  6. 内存管理:使用对象池和TypedArray减少垃圾回收,避免在动画循环中创建临时对象。
  7. 选择合适的API:使用最高效的Canvas API进行绘图操作。

10.6.2 架构设计最佳实践

  1. 模块化设计:将游戏或应用分解为独立的组件,提高可维护性。
  2. 资源管理:集中管理和预加载资源,避免运行时加载导致的卡顿。
  3. 响应式设计:适应不同屏幕尺寸和设备像素比,提供一致的用户体验。
  4. 输入抽象:统一处理键盘、鼠标和触摸输入,简化交互逻辑。

10.6.3 调试与优化工具

  1. 浏览器开发者工具:使用Performance和Memory面板分析性能问题。
  2. 自定义性能监控:实现FPS计数器和渲染时间监控,实时评估性能。
  3. 可视化调试:绘制碰撞边界、四叉树分区等辅助信息,帮助调试复杂问题。

10.6.4 常见性能陷阱

  1. 过度绘制:绘制不可见区域或重复绘制相同区域。
  2. 频繁的DOM操作:在动画循环中修改DOM或调整Canvas大小。
  3. 复杂路径:使用过于复杂的路径或不必要的曲线。
  4. 未优化的图像:使用过大的图像或未使用适当的图像格式。
  5. 忽略设备像素比:在高DPI设备上未正确缩放Canvas,导致模糊或性能问题。

10.7 练习

  1. 性能分析工具:实现一个完整的性能监控面板,显示FPS、渲染时间、内存使用和对象计数。

  2. 优化粒子系统:使用本章学习的技术优化粒子系统,使其能够高效地渲染10000+粒子。

  3. 响应式游戏:创建一个在不同设备上都能良好运行的Canvas游戏,包括桌面和移动设备。

  4. 四叉树碰撞检测:实现一个使用四叉树的碰撞检测系统,并与暴力检测方法进行性能比较。

  5. 模块化游戏引擎:基于本章的架构示例,扩展实现一个完整的2D游戏引擎,包括场景管理、物理系统和动画系统。

下一章预告

在下一章中,我们将学习Canvas与其他Web技术的集成,包括与WebGL、SVG、CSS和Web组件的结合使用,以及如何将Canvas应用部署到生产环境。