前言
当你打开一个炫酷的数据大屏,或者一个在浏览器里流畅运行的HTML5小游戏,背后支撑这一切的往往是同一个技术------Canvas 。但很多人写Canvas动画时,习惯性地复制粘贴,并不清楚每一帧发生了什么,更不理解 requestAnimationFrame 为何比 setInterval 高级。
本文不打算贴出一整篇雷霆战机代码或长篇ECharts配置(那些只是应用示例)。我们将直击本质:Canvas 的基础绘图API 、动画中的清除与重绘 、以及浏览器的帧动画调度器 requestAnimationFrame。读完此文,你不仅能流畅写出动画,还能讲清楚它为什么"顺滑且省电"。
适合读者:有JS基础,想系统掌握Canvas动画原理的前端开发者。
一、Canvas 标签与绘图上下文
1.1 画布元素
Canvas 是 HTML5 新增的绘图容器,本身没有任何绘图能力,它只提供一块"画布区域"。绘图全部由 JavaScript 完成。
html
<canvas id="myCanvas" width="600" height="400" style="border:1px solid #ccc">
你的浏览器不支持 Canvas(旧IE会显示这段文字)
</canvas>
-
width/height属性定义画布的真实分辨率(像素尺寸),无单位,默认为 300×150。 -
CSS 的
width/height只是显示缩放,不会改变绘图像素数。二者最好保持一致,否则图像会被拉伸。
1.2 获取绘图上下文
所有绘图操作都依赖一个 上下文对象(Context),它像一支笔,提供各种绘图方法
javascript
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
-
getContext('2d')返回CanvasRenderingContext2D实例,支持矩形、路径、文字、图像等绘图。 -
getContext('webgl')可获取 WebGL 上下文,用于 3D 绘图(例如 Three.js 底层)。
注意:Canvas 不支持
getContext('3d'),那是早期某些浏览器的非标准实验。
二、基础绘图 API:矩形、颜色、擦除
Canvas 提供了最基础的图形------矩形的绘制方法,其他复杂图形(圆、线、多边形)需要通过路径(Path)实现。
2.1 实心矩形 fillRect
javascript
ctx.fillStyle = '#4299e1'; // 设置填充色
ctx.fillRect(20, 20, 100, 80); // 在 (20,20) 绘制 100×80 的蓝色矩形
-
fillRect(x, y, width, height):(x,y)是矩形左上角坐标(Canvas 坐标系原点在左上角,x 向右,y 向下)。 -
fillStyle可以是任何 CSS 颜色值,一旦设置,后续所有实心图形都会沿用,直到重新赋值。
2.2 空心矩形 strokeRect
javascript
ctx.strokeStyle = '#f56565';
ctx.lineWidth = 4;
ctx.strokeRect(150, 20, 100, 80);
-
strokeRect只描边,内部透明。 -
strokeStyle控制描边颜色,lineWidth控制线宽(单位:像素)。
2.3 擦除区域 clearRect ------ 动画的基石
javascript
ctx.clearRect(50, 50, 40, 30);
-
清除指定矩形区域内的所有像素,使其完全透明(恢复初始状态)。
-
为什么动画必须用它?
动画的本质是每一帧绘制不同的画面。如果不擦除,上一帧的图形会残留,形成"拖尾"或"鬼影"。
每一帧的正确流程 :
clearRect(0,0,canvas.width,canvas.height)→ 更新坐标 → 绘制新图形。
若不调用
clearRect,你将看到一长串矩形拖尾,那不是动画,而是"涂鸦"。
页面效果(滑动效果):


三、requestAnimationFrame ------ 专业的动画调度器
3.1 传统定时器的问题
早期的动画常使用 setInterval 或 setTimeout:
javascript
setInterval(() => {
// 更新并绘图
}, 1000 / 60);
但这种方式存在三大缺陷:
-
时间不精确:JS 是单线程,定时器回调可能被阻塞,实际间隔可能远大于设定值。
-
与屏幕刷新率不同步 :显示器一般 60Hz(约 16.6ms 一帧)。
setInterval几乎不可能恰好对齐这个节奏,可能导致丢帧(卡顿)或渲染撕裂。 -
不可见时依然执行:即使浏览器标签页被切换到后台,定时器仍在后台运行,浪费 CPU 和电量。
3.2 requestAnimationFrame 的工作原理
requestAnimationFrame 是浏览器专门为动画提供的 API。它会在浏览器下一次重绘之前调用指定的回调函数。
-
频率自适应:通常匹配屏幕刷新率(60Hz → 约 16.6ms 调用一次)。若显示器是 144Hz,则回调更频繁。
-
自动暂停:当页面切换到后台或不可见标签页时,回调会停止调用,节省资源。
-
精确同步:回调执行时机与渲染流水线对齐,避免撕裂和多余重绘。
3.3 基本用法
javascript
function draw(now) { // now 是 DOMHighResTimeStamp,表示回调触发的时间
// 更新动画状态
// 绘图操作
requestAnimationFrame(draw); // 递归调用,形成循环
}
requestAnimationFrame(draw);
递归是必须的 :requestAnimationFrame 只请求一帧,如果你想要持续动画,必须在回调内部再次调用它。
3.4 对比示例:setInterval 与 requestAnimationFrame 的感性差异
假设我们让一个方块横向移动 1000 像素,两种方式:
-
setInterval可能在 16.6ms 左右执行,但由于事件循环阻塞,实际可能跳跃到 20ms、30ms,移动不均,视觉上"一卡一卡"。 -
requestAnimationFrame每次重绘前恰好执行,移动步长均匀,极其顺滑。
3.5 高级用法:基于时间的运动(与帧率解耦)
如果直接在每一帧中移动固定像素(如 x += 5),在高刷新率(144Hz)下物体会移动更快,在低刷新率下移动更慢。正确的做法是根据时间差移动:
javascript
let lastTimestamp = 0;
let x = 0;
const speedPerSecond = 200; // 每秒移动 200 像素
function animate(now) {
if (!lastTimestamp) lastTimestamp = now;
const delta = Math.min(100, now - lastTimestamp); // 限制最大间隔
x += speedPerSecond * delta / 1000; // 与时间成正比
lastTimestamp = now;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(x, 20, 50, 50);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
这样无论帧率是 30 还是 144,物体每秒移动距离恒定。
3.6 取消动画
使用 cancelAnimationFrame,传入 requestAnimationFrame 返回的 ID:
javascript
let frameId = requestAnimationFrame(animate);
// 在某个条件满足时
cancelAnimationFrame(frameId);
四、Canvas 游戏循环的典型模式
虽然本文不重点展开游戏,但理解游戏循环有助于你掌握 requestAnimationFrame 的真实应用场景。
一个标准的游戏循环包含两大阶段:
-
更新(Update)
-
处理用户输入(键盘、鼠标)
-
更新所有实体坐标(子弹上移、敌机下移)
-
碰撞检测与逻辑响应(加分、爆炸、游戏结束)
-
-
绘制(Draw)
-
clearRect清空上一帧 -
调用各种绘图 API 绘制当前帧的所有元素(玩家、子弹、敌机、UI)
-
整个循环由 requestAnimationFrame 驱动:
javascript
function gameLoop() {
update(); // 更新游戏状态
draw(); // 重绘画布
requestAnimationFrame(gameLoop);
}
gameLoop();
这种模式干净、高效,而且完全可控。相比 Vue/React 的声明式渲染,Canvas 游戏采用命令式逐帧绘制,这也是它性能极高的原因之一。
五、Canvas 与数据可视化(浅谈)
数据可视化图表(如折线图、柱状图、地图)也是 Canvas 的重要应用领域。著名的 ECharts 库底层可选用 Canvas 或 SVG。
用 Canvas 实现图表的基本思路:
-
用
fillRect绘制柱状图; -
用
lineTo、stroke绘制折线; -
用
fillText绘制坐标轴标签。
ECharts 简化了大量底层细节,你只需配置数据即可得到炫酷的可视化效果。例如渐变柱状图(核心代码):
javascript
series: [{
type: 'bar',
data: [8.2, 6.5, 9.1, ...],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#667eea' },
{ offset: 1, color: '#764ba2' }
])
}
}]
但无论如何封装,其背后依然是 Canvas 绘图 API 与 requestAnimationFrame 的巧妙结合(用于动画过渡)。
用AI vibe coding 的一张Ecart 效果:

六、总结与扩展
6.1 关键点回顾
| 知识点 | 核心作用 | 注意事项 |
|---|---|---|
canvas 标签 |
绘图容器 | 设置正确的 width/height,避免 CSS 缩放失真 |
getContext('2d') |
获取画笔 | 上下文对象包含所有绘图方法 |
fillRect / strokeRect |
绘制矩形 | 配合 fillStyle / strokeStyle 控制颜色 |
clearRect |
擦除区域 | 每一帧动画的第一件事就是清空画布 |
requestAnimationFrame |
动画调度 | 自动与刷新率同步,后台暂停,性能最佳 |
6.2 后续进阶方向
-
路径绘图 :
beginPath()、arc()、lineTo()实现圆形、多边形、曲线。 -
变换 :
translate、rotate、scale实现复杂图形移动。 -
图像处理 :
drawImage绘制精灵图,getImageData/putImageData实现像素滤镜。 -
3D 扩展 :
WebGL或封装库Three.js进入三维世界。
6.3 最后的建议
不要依赖 setInterval 做动画。凡是涉及 Canvas 连续变化的需求(游戏、数据大屏滚动、交互动效),一律使用 requestAnimationFrame。它不仅能让你的动画丝滑流畅,更能体现你对浏览器渲染机制的理解深度。
如果你觉得这篇文章点透了 Canvas 动画的本质,欢迎点赞、收藏、转发。你也可以在评论区留言,告诉我你遇到过哪些 Canvas 动画的"坑",我们一起探讨。