悟夕导航

第四集:让火柴人动起来——帧动画与 requestAnimationFrame

69 0 0
“老板让我画个‘奔跑吧火柴人’,我画了张‘静止吧火柴棍’,他说我像 KPI 一样一动不动。”
—— 某位被扣绩效的前端小可爱

大家好,欢迎来到《Canvas 翻车指南》第四集!
前面我们把图形画出了“高级感”,今天让它蹦跶起来:

  • 拆素材:把火柴人切成“帧”
  • 算帧率:为什么 setInterval 会被打回姥姥家
  • 上 RAF:requestAnimationFrame 才是正统“时间管理局”
  • 写跑酷:60 FPS 不卡成 PPT,老板看完加预算!

准备好薯片,咱们让火柴人从静态到社恐


一、帧动画原理:翻小人书,翻得快就是动画

小时候把课本角画成“打人”序列,一翻就动——这就是帧动画。
在 Canvas 里同理:

  1. 把每帧画成独立函数
  2. 定时清屏 → 画下一帧
  3. 只要速度 > 12 帧/秒,大脑就被骗:它在动!

二、setInterval 翻车现场:为什么别用它?

// 别抄,反面教材!
setInterval(() => {
  ctx.clearRect(0,0,w,h);
  drawFrame(index++);
}, 1000/60);

问题:

  • 屏幕刷新率 60Hz,setInterval 无视 VSYNC掉帧撕裂
  • 切到后台仍狂奔,CPU 像加班的你一样崩溃
  • 误差累积,跑得越来越飘

结论:

“setInterval 做动画,就像用菜刀剪指甲——能剪,但血条见底。”

三、requestAnimationFrame:官方“时间管理局”

function loop() {
  ctx.clearRect(0,0,canvas.width,canvas.height);
  update();   // 更新数据
  render();   // 画出来
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

优点:

  • 跟浏览器刷新同步,天生 60 FPS
  • 切后台自动暂停,省电省咖啡
  • 自带高精度时间戳,物理计算不漂移

口诀:

“RAF 一出手,帧率稳如狗。”

四、实战:8 帧火柴人奔跑

1. 素材拆解(灵魂手绘)

用 ASCII 表示 8 帧(实际用 PNG 序列也一样逻辑):

帧0  │  ○
     │ /│\
     │  │
     │ / \

每帧微调手脚角度,8 张简笔画塞数组:

const frames = [
  {head:[0,0], body:[0,20], armL:-30, armR:30, legL:-20, legR:20},
  // ... 省略其余 7 帧,角度差 15°
];
let idx = 0;
let last = 0;

2. 画单帧函数

function drawStick(f) {
  ctx.save();
  ctx.translate(100, 150); // 基准点
  // 头
  ctx.beginPath();
  ctx.arc(f.head[0], f.head[1], 10, 0, Math.PI*2);
  ctx.fill();
  // 身体
  ctx.moveTo(0,10);
  ctx.lineTo(0,50);
  ctx.stroke();
  // 手臂
  ctx.save();
  ctx.rotate(f.armL * Math.PI/180);
  ctx.moveTo(0,20); ctx.lineTo(-20,35); ctx.stroke();
  ctx.restore();
  // 其余四肢同理...
  ctx.restore();
}

3. 主循环 + 帧率控制

const FPS = 12; // 故意低一点,复古感
function loop(timestamp) {
  requestAnimationFrame(loop);
  const delta = timestamp - last;
  if (delta > 1000/FPS) {
    last = timestamp;
    ctx.clearRect(0,0,canvas.width,canvas.height);
    drawStick(frames[idx]);
    idx = (idx+1) % frames.length;
  }
}
requestAnimationFrame(loop);

刷新:
火柴人原地奔跑帧稳 12 FPS像早期 GBA 游戏情怀拉满


五、进阶:让火柴人“跑酷”

目标:

  • 每帧右移 5px,跑出屏幕
  • 背景循环,假装世界在动
let x = -50;
function loop(timestamp) {
  requestAnimationFrame(loop);
  const delta = timestamp - last;
  if (delta > 1000/FPS) {
    last = timestamp;
    ctx.clearRect(0,0,canvas.width,canvas.height);
    
    // 画地面
    ctx.beginPath();
    ctx.moveTo(0,180);
    ctx.lineTo(canvas.width,180);
    ctx.stroke();
    
    // 更新位置
    x += 5;
    if (x > canvas.width + 50) x = -50;
    
    // 画火柴人
    ctx.save();
    ctx.translate(x, 150);
    drawStick(frames[idx]);
    ctx.restore();
    
    idx = (idx+1) % frames.length;
  }
}

刷新:
火柴人一路狂奔地面飞退像 Flappy Bird 的穷亲戚
把背景换成“星空粒子”,瞬间拥有微信小游戏即视感


六、性能锦囊:对象池 + 防抖

  • 星星/粒子提前 new 好,避免运行时 malloc
  • RAF 里不要 new Image()预加载再上场
  • canvas.offscreenCanvas 把背景离屏渲染前景只清一小条帧率稳到 144Hz 电竞屏都不慌

七、本集知识点一口闷

技能口诀
帧动画翻小人书帧数太低像 PPT
setInterval剪刀剪指甲掉帧+后台耗电
requestAnimationFrame官方时钟必须自己算 delta
对象池提前占位运行时 new 会卡

八、课后作业(翻车 field)

  1. 把 8 帧换成跳跃序列(下蹲→上升→下降→落地),空格键触发做出“跳一跳”
  2. 粒子尘土落地时爆一圈像火影跑完结印
  3. Bonus:用 OffscreenCanvas 把星空背景离屏渲染主循环只清 1/4 区域帧率锁 60截图晒性能面板

九、下集预告

第五集《交互!让鼠标成为上帝的指挥棒》
带你玩:

  • 点击生成粒子烟花
  • 拖拽橡皮擦,把画布当沙盘
  • 防抖 + 节流,鼠标疯移也不卡成狗

“人生就像 requestAnimationFrame,
你不主动 call 下一帧,
就只能卡在原地,被老板 clear。”

—— 我说的。

第四集结束,点赞、转发、晒火柴人
下集一起让鼠标当上帝,回见!

0
快来点个赞吧

发表评论

隐私评论

评论列表

来写一个评论吧