Canvas动画,可不是给元素加个CSS的animation属性那么简单。它更像是在指挥一场精密的舞台剧:你得手动控制每一帧的清场、演员(图形)的登场、以及他们位置的实时更新。这一切的核心,都依赖于Ja vaScript中一个名为requestAnimationFrame的循环引擎。 为什么不能用
HTML Canvas简单动画实现方法
时间:2026-06-03
浏览:533
文章目录
Canvas动画,可不是给元素加个CSS的animation属性那么简单。它更像是在指挥一场精密的舞台剧:你得手动控制每一帧的清场、演员(图形)的登场、以及他们位置的实时更新。这一切的核心,都依赖于Ja vaScript中一个名为requestAnimationFrame的循环引擎。

为什么不能用 setTimeout 或 setInterval
很多初学者会习惯性地想到用setTimeout或setInterval来驱动动画循环,但这其实是个坑。这两个方法跟屏幕的刷新节奏是脱节的,很容易导致动画掉帧、时间漂移,而且就算页面切到后台,它们依然会默默执行,白白消耗电量。
而requestAnimationFrame则聪明得多。它会自动与显示器的刷新率(通常是每秒60次)同步,确保动画丝滑流畅。更重要的是,当页面被隐藏或最小化时,它会自动暂停动画循环,真正做到节能高效。
这里有个常见的低级错误:只调用一次requestAnimationFrame(draw),结果动画只跑一帧就停了。记住,必须在你的绘制函数末尾再次调用自身,才能形成一个持续的递归循环链。
- 别在循环里做耗时操作,比如遍历超长数组、解析大块JSON,或者触发页面重排(layout),这会直接卡死动画。
- 启动动画前,务必确认canvas的宽高尺寸已经确定。如果canvas尺寸后续被CSS缩放,而绘制坐标没跟着调整,画面就会错乱。
clearRect 清屏后图形还残留?
你是不是也遇到过:明明调用了clearRect清空了画布,但上一帧的图形好像还有淡淡的“影子”?问题在于,clearRect只负责擦除像素点,却不会重置绘图上下文(ctx)的状态。
想象一下,如果上一帧你设置了半透明(ctx.globalAlpha = 0.5)或者很粗的线条宽度(ctx.lineWidth = 4),下一帧直接画新图形,这些状态会被继承,导致图形叠加变糊、线条变粗。
- 最稳妥的做法永远是:
ctx.clearRect(0, 0, canvas.width, canvas.height)。 - 清屏后,手动重置关键状态是个好习惯,比如
ctx.globalAlpha = 1,或者用ctx.setTransform(1, 0, 0, 1, 0, 0)复位变换矩阵。 - 尽量避免在每一帧都使用
sa ve()和restore()来保存恢复状态,这对性能有开销。优先考虑直接重置特定属性。 - 如果动画只涉及局部区域更新(比如一个小图标),可以精确计算其“脏区域”,用
clearRect(x, y, w, h)进行局部擦除,能提升性能。
小球动起来:位置更新 + 边界反弹的最小闭环
Canvas没有“对象”的概念,所有运动都得你亲手计算。实现一个弹跳小球,是最经典的入门案例。其核心就是每帧更新小球的坐标(x, y),加上速度(vx, vy),并在碰到画布边缘时反转速度方向。
- 速度值别用整数:用
vx = 2.3代替vx = 2。整数速度容易因为像素对齐问题,导致动画产生肉眼可见的抖动,小数移动则平滑得多。 - 边界判断要宽容:别写
x === 0这种精确判断。高速运动下,小球很可能在某一帧直接从-1跳到了1,完美跳过0这个临界点。应该用x <= 0或x >= canvas.width - diameter(diameter是球直径)。 - 反弹后微调位置:检测到碰撞后,除了反转速度(
vx = -vx),最好将位置修正到边界上,例如x = 0,避免小球卡在边界反复触发碰撞检测。 - 记得开启新路径:在绘制图形(如
arc画圆)前,调用ctx.beginPath()。否则,所有绘制命令会连成一条复杂的复合路径,影响性能和渲染。
最后,一个真正容易踩坑的细节:当动画涉及图片(使用drawImage)时,90%的黑屏、错位或“Failed to execute 'drawImage'”这类错误,根源都是图片还没加载完成就开始绘制。务必确保在图片的onload事件触发后再启动动画循环,或者先用img.complete属性判断图片是否已就绪。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。