pin_drop当前位置:知识文库 ❯ 图文

多媒体:动画 - 完整教程与代码示例

一、教程简介

Canvas 动画是通过在每一帧重新绘制画面来实现的。requestAnimationFrame 是现代浏览器提供的动画 API,它能够在浏览器的下一次重绘之前调用指定的回调函数,从而实现流畅的 60fps 动画效果。本教程将详细讲解 Canvas 动画的基本原理、动画循环的构建、帧率控制以及常见动画效果的实现。


二、核心概念

动画原理

Canvas 动画基于"逐帧渲染"原理:

  • 清除画布:擦除上一帧的内容

  • 更新状态:计算动画对象的新位置、大小、颜色等

  • 重新绘制:根据新状态绘制当前帧

  • 请求下一帧:通过 requestAnimationFrame 递归调用

requestAnimationFrame

requestAnimationFrame 相比 setInterval/setTimeout 的优势:

  • 与浏览器刷新率同步(通常 60fps)

  • 页面不可见时自动暂停,节省资源

  • 更平滑的动画效果

  • 电池友好

时间差(Delta Time)

基于时间的动画而非基于帧的动画,确保不同帧率下动画速度一致:

代码示例

let lastTime = 0;
function animate(timestamp) {
  const deltaTime = timestamp - lastTime;
  lastTime = timestamp;
  // 使用 deltaTime 计算位移
  requestAnimationFrame(animate);
}

三、语法与用法

requestAnimationFrame

代码示例

const id = requestAnimationFrame(callback);
参数 说明
callback 下一次重绘前调用的函数,接收 timestamp 参数
返回值 请求 ID,用于取消

cancelAnimationFrame

代码示例

cancelAnimationFrame(id);

取消之前通过 requestAnimationFrame 注册的回调。

基本动画循环

代码示例

function animate(timestamp) {
  // 1. 清除画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 2. 更新状态
  update();

  // 3. 绘制
  draw();

  // 4. 请求下一帧
  requestAnimationFrame(animate);
}

// 启动动画
requestAnimationFrame(animate);

四、代码示例

示例1:基本动画效果集合

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>基本动画效果集合</title>
  <style>
    body {
      margin: 0;
      padding: 20px;
      background: #0d1117;
      color: #c9d1d9;
      font-family: 'Segoe UI', sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    canvas {
      border: 1px solid #30363d;
      border-radius: 8px;
      background: #161b22;
    }
  </style>
</head>
<body>
  <h1>Canvas 动画效果集合</h1>
  <canvas id="myCanvas" width="750" height="450"></canvas>

  <script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');
    let time = 0;

    // === 弹跳球 ===
    const ball = {
      x: 120, y: 100, vx: 3, vy: 2, r: 15,
      color: '#e94560'
    };

    function updateBall() {
      ball.x += ball.vx;
      ball.y += ball.vy;
      if (ball.x - ball.r < 10 || ball.x + ball.r > 230) ball.vx *= -1;
      if (ball.y - ball.r < 50 || ball.y + ball.r > 210) ball.vy *= -1;
    }

    function drawBall() {
      ctx.fillStyle = '#0f172a';
      ctx.fillRect(10, 50, 220, 160);
      ctx.strokeStyle = '#30363d';
      ctx.lineWidth = 1;
      ctx.strokeRect(10, 50, 220, 160);

      ctx.fillStyle = '#c9d1d9';
      ctx.font = '11px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('弹跳球', 120, 45);

      ctx.beginPath();
      ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
      ctx.fillStyle = ball.color;
      ctx.fill();

      // 拖尾
      ctx.beginPath();
      ctx.arc(ball.x - ball.vx * 2, ball.y - ball.vy * 2, ball.r * 0.7, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(233, 69, 96, 0.3)';
      ctx.fill();
    }

    // === 旋转方块 ===
    function drawRotatingSquare() {
      const cx = 370, cy = 130;
      ctx.fillStyle = '#0f172a';
      ctx.fillRect(260, 50, 220, 160);
      ctx.strokeStyle = '#30363d';
      ctx.strokeRect(260, 50, 220, 160);

      ctx.fillStyle = '#c9d1d9';
      ctx.font = '11px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('旋转方块', 370, 45);

      ctx.save();
      ctx.translate(cx, cy);
      ctx.rotate(time * 0.03);
      ctx.fillStyle = '#58a6ff';
      ctx.fillRect(-30, -30, 60, 60);
      ctx.restore();
    }

    // === 脉冲圆 ===
    function drawPulsingCircle() {
      const cx = 620, cy = 130;
      ctx.fillStyle = '#0f172a';
      ctx.fillRect(510, 50, 220, 160);
      ctx.strokeStyle = '#30363d';
      ctx.strokeRect(510, 50, 220, 160);

      ctx.fillStyle = '#c9d1d9';
      ctx.font = '11px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('脉冲圆', 620, 45);

      const pulse = Math.sin(time * 0.05) * 20 + 40;
      ctx.beginPath();
      ctx.arc(cx, cy, pulse, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(63, 185, 80, 0.3)';
      ctx.fill();
      ctx.strokeStyle = '#3fb950';
      ctx.lineWidth = 2;
      ctx.stroke();
    }

    // === 粒子系统 ===
    const particles = [];
    for (let i = 0; i < 50; i++) {
      particles.push({
        x: 375, y: 350,
        vx: (Math.random() - 0.5) * 4,
        vy: (Math.random() - 0.5) * 4,
        life: Math.random() * 100,
        maxLife: 100 + Math.random() * 50,
        size: 2 + Math.random() * 3,
        hue: Math.random() * 60 + 200
      });
    }

    function updateParticles() {
      particles.forEach(p => {
        p.x += p.vx;
        p.y += p.vy;
        p.vy += 0.02; // 重力
        p.life++;
        if (p.life > p.maxLife) {
          p.x = 375;
          p.y = 350;
          p.vx = (Math.random() - 0.5) * 4;
          p.vy = (Math.random() - 0.5) * 4 - 2;
          p.life = 0;
        }
      });
    }

    function drawParticles() {
      ctx.fillStyle = '#0f172a';
      ctx.fillRect(10, 230, 730, 210);
      ctx.strokeStyle = '#30363d';
      ctx.strokeRect(10, 230, 730, 210);

      ctx.fillStyle = '#c9d1d9';
      ctx.font = '11px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('粒子系统', 375, 225);

      particles.forEach(p => {
        const alpha = 1 - p.life / p.maxLife;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2);
        ctx.fillStyle = `hsla(${p.hue}, 70%, 60%, ${alpha})`;
        ctx.fill();
      });
    }

    // === 正弦波 ===
    function drawSineWave() {
      ctx.fillStyle = '#c9d1d9';
      ctx.font = '11px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('正弦波动画', 375, 225);

      ctx.beginPath();
      for (let x = 10; x < 740; x++) {
        const y = 335 + Math.sin((x + time * 2) * 0.03) * 30
                     + Math.sin((x + time * 3) * 0.02) * 15;
        if (x === 10) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
      }
      ctx.strokeStyle = '#d2a8ff';
      ctx.lineWidth = 2;
      ctx.stroke();
    }

    // === 主动画循环 ===
    function animate() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      updateBall();
      drawBall();
      drawRotatingSquare();
      drawPulsingCircle();
      updateParticles();
      drawParticles();
      drawSineWave();

      time++;
      requestAnimationFrame(animate);
    }

    requestAnimationFrame(animate);
  </script>
</body>
</html>

由于代码较长,完整的示例2(基于时间的动画与帧率控制)请参考原始Markdown文件。


五、浏览器兼容性

API Chrome Firefox Safari Edge IE
requestAnimationFrame 24+ 23+ 6.1+ 12+ 10+
cancelAnimationFrame 24+ 23+ 6.1+ 12+ 10+

六、注意事项与最佳实践

1. 始终使用 requestAnimationFrame

代码示例

// 推荐
function animate() {
  // 动画逻辑
  requestAnimationFrame(animate);
}

// 不推荐
// setInterval(animate, 16); // 不与浏览器刷新同步

2. 基于时间的动画

代码示例

// 推荐:基于时间的动画
function animate(timestamp) {
  if (!lastTime) lastTime = timestamp;
  const dt = (timestamp - lastTime) / 1000;
  lastTime = timestamp;

  object.x += speed * dt; // 速度 * 时间 = 位移
  requestAnimationFrame(animate);
}

// 不推荐:基于帧的动画
// object.x += speed; // 不同帧率下速度不同

3. 暂停与恢复

代码示例

let isPaused = false;
let lastTime = 0;

function animate(timestamp) {
  if (isPaused) return;

  if (!lastTime) lastTime = timestamp;
  const dt = (timestamp - lastTime) / 1000;
  lastTime = timestamp;

  update(dt);
  draw();
  requestAnimationFrame(animate);
}

function pause() {
  isPaused = true;
}

function resume() {
  isPaused = false;
  lastTime = 0; // 重置时间避免跳帧
  requestAnimationFrame(animate);
}

4. 清除画布的选择

代码示例

// 完全清除(无拖尾)
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 半透明覆盖(拖尾效果)
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);

5. 性能优化

  • 减少状态切换(fillStyle、font 等)

  • 合并相同样式的绘制操作

  • 使用离屏 Canvas 缓存静态内容

  • 避免在动画循环中创建对象


七、代码规范示例

代码示例

// 推荐:动画类封装
class CanvasAnimation {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.isRunning = false;
    this.lastTime = 0;
    this.animationId = null;
    this.objects = [];
  }

  start() {
    this.isRunning = true;
    this.lastTime = 0;
    this.animationId = requestAnimationFrame(this._loop.bind(this));
  }

  stop() {
    this.isRunning = false;
    if (this.animationId) {
      cancelAnimationFrame(this.animationId);
    }
  }

  _loop(timestamp) {
    if (!this.isRunning) return;

    if (!this.lastTime) this.lastTime = timestamp;
    const dt = Math.min((timestamp - this.lastTime) / 1000, 0.1); // 限制最大 dt
    this.lastTime = timestamp;

    this.update(dt);
    this.draw();
    this.animationId = requestAnimationFrame(this._loop.bind(this));
  }

  update(dt) {
    // 子类重写
  }

  draw() {
    // 子类重写
  }
}

// 使用
class MyAnimation extends CanvasAnimation {
  update(dt) {
    this.objects.forEach(obj => {
      obj.x += obj.vx * dt;
      obj.y += obj.vy * dt;
    });
  }

  draw() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.objects.forEach(obj => {
      this.ctx.beginPath();
      this.ctx.arc(obj.x, obj.y, obj.r, 0, Math.PI * 2);
      this.ctx.fill();
    });
  }
}

八、常见问题与解决方案

常见问题

动画在不同设备上速度不同怎么办?

原因是使用基于帧的动画,不同帧率导致速度差异。解决方案是使用基于时间的动画,通过 deltaTime 计算位移,确保不同帧率下动画速度一致。

切换标签页后动画跳帧怎么解决?

原因是 requestAnimationFrame 在标签页不可见时暂停,恢复后 dt 值很大。解决方案是限制最大 dt 值,如 dt = Math.min((timestamp - lastTime) / 1000, 0.1)。

如何实现缓动效果?

可以使用缓动函数:linear(线性)、easeIn(缓入)、easeOut(缓出)、easeInOut(缓入缓出)。通过 progress = easeInOut(elapsed / duration) 计算进度,然后 object.x = startX + (endX - startX) * progress 实现平滑过渡。

如何实现拖尾效果?

有两种方案:方案1是使用半透明覆盖 ctx.fillStyle = 'rgba(0, 0, 0, 0.1)' 代替 clearRect;方案2是记录轨迹点数组,逐帧绘制历史位置并逐渐降低透明度。

如何控制动画帧率?

可以通过计算目标帧间隔来控制帧率。设置 targetFPS = 30,interval = 1000 / targetFPS,在动画循环中判断 if (timestamp - lastFrameTime < interval) return,从而实现降帧效果。


九、总结

Canvas 动画是创建交互式和动态内容的核心技术,关键要点:

  • requestAnimationFrame:浏览器原生动画 API,与刷新率同步

  • 动画循环:清除 -> 更新 -> 绘制 -> 请求下一帧

  • 基于时间:使用 deltaTime 确保不同帧率下动画速度一致

  • 暂停恢复:暂停时取消动画,恢复时重置 lastTime

  • 拖尾效果:半透明覆盖或记录轨迹点

  • 缓动函数:easeIn、easeOut、easeInOut 实现自然运动

  • 性能优化:减少状态切换、合并绘制、使用离屏 Canvas

小贴士

在开发动画时,建议先在低帧率(如 30fps)下调试,确保逻辑正确后再提升到 60fps。使用 Chrome DevTools 的 Performance 面板可以分析动画性能,找出瓶颈。对于复杂动画,考虑使用 WebGL 或 CSS 动画以获得更好的性能。

标签: Canvas动画 requestAnimationFrame 帧率控制 缓动函数 Canvas教程 动画循环

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 多媒体:Canvas裁剪 - 完整教程与代码示例 下一篇: 多媒体:像素操作 - 完整教程与代码示例

poll相关推荐