第四集:让火柴人动起来——帧动画与 requestAnimationFrame
69
0
0
“老板让我画个‘奔跑吧火柴人’,我画了张‘静止吧火柴棍’,他说我像 KPI 一样一动不动。”
—— 某位被扣绩效的前端小可爱
大家好,欢迎来到《Canvas 翻车指南》第四集!
前面我们把图形画出了“高级感”,今天让它蹦跶起来:
- 拆素材:把火柴人切成“帧”
- 算帧率:为什么
setInterval会被打回姥姥家 - 上 RAF:
requestAnimationFrame才是正统“时间管理局” - 写跑酷:60 FPS 不卡成 PPT,老板看完加预算!
准备好薯片,咱们让火柴人从静态到社恐!
一、帧动画原理:翻小人书,翻得快就是动画
小时候把课本角画成“打人”序列,一翻就动——这就是帧动画。
在 Canvas 里同理:
- 把每帧画成独立函数
- 定时清屏 → 画下一帧
- 只要速度 > 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)
- 把 8 帧换成跳跃序列(下蹲→上升→下降→落地),空格键触发,做出“跳一跳”。
- 加粒子尘土,落地时爆一圈,像火影跑完结印。
- Bonus:用
OffscreenCanvas把星空背景离屏渲染,主循环只清 1/4 区域,帧率锁 60,截图晒性能面板。
九、下集预告
第五集《交互!让鼠标成为上帝的指挥棒》
带你玩:
- 点击生成粒子烟花
- 拖拽橡皮擦,把画布当沙盘
- 防抖 + 节流,鼠标疯移也不卡成狗
“人生就像 requestAnimationFrame,
你不主动 call 下一帧,
就只能卡在原地,被老板 clear。”
—— 我说的。
第四集结束,点赞、转发、晒火柴人,
下集一起让鼠标当上帝,回见!
0
快来点个赞吧
发表评论
评论列表