在网页交互开发中,控制动画的播放节奏是一项基础但容易出错的能力。HTML5生态下动画主要依赖CSS过渡、CSS关键帧以及Canvas或SVG的脚本驱动,不同技术路线的暂停与恢复机制并不一致。理解它们背后的时间轴模型,才能在实际业务中准确冻结画面而不破坏后续逻辑。

CSS动画的暂停与恢复原理
CSS动画由浏览器合成线程或主线程按关键帧时间轴推进,开发者无需手动计算每一帧。规范为动画元素提供了animation-play-state属性,其默认值为running,改为paused后,浏览器会保留当前帧的 computed style,并停止向后续关键帧插值。这种声明式控制不依赖JavaScript时钟,因此切换标签页导致主线程休眠时,动画也会自然停在当前位置。
使用行内样式或类切换均可改变该属性。下面示例通过按钮切换容器的类名,从而让旋转动画暂停或继续,注意我们操作的是外层包裹元素的style对象而不是直接重写所有动画声明,这样能保留原有的animation简写配置。
<div id="box" class="spin"></div>
<button id="toggle">暂停/恢复</button>
<style>
.spin {
width: 100px; height: 100px; background: #39c;
animation: rotate 2s linear infinite;
}
@keyframes rotate { to { transform: rotate(360deg); } }
.paused { animation-play-state: paused; }
</style>
<script>
const box = document.getElementById('box');
document.getElementById('toggle').onclick = () => {
box.classList.toggle('paused');
};
</script>
这种方案的优点是零计算开销、代码极少,缺点是只能整体暂停,无法在暂停时读取“还剩多少时间”。如果需要精确知道动画进度,可以借助getComputedStyle读取animation-delay的负值和transform矩阵,但跨浏览器解析矩阵较为繁琐。在表单校验提示、加载等待等轻量场景里,纯CSS控制已足够稳健。
Canvas与requestAnimationFrame的命令式控制
当动画绘制在<canvas>元素上,或需要按物理曲线逐帧运算时,通常利用requestAnimationFrame(简称rAF)驱动循环。rAF本身没有暂停方法,所谓暂停其实是停止继续注册下一帧回调,并记录已流逝时间。恢复时再根据时间差补偿,避免画面跳跃。
下面是一个典型封装:用变量rafId保存句柄,lastTime标记上次绘制时刻,paused标志位决定是否执行绘制。点击暂停时调用cancelAnimationFrame并记录断点,恢复时重置lastTime为当前时间戳再重启循环。这种写法能保证圆球在暂停期间不位移,恢复后速度连续。
let canvas = document.getElementById('cv');
let ctx = canvas.getContext('2d');
let x = 0, lastTime = 0, rafId = null, paused = false;
function draw(t) {
if (paused) return;
if (!lastTime) lastTime = t;
let dt = t - lastTime;
lastTime = t;
x += dt * 0.1;
if (x > canvas.width) x = 0;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(x, 50, 30, 30);
rafId = requestAnimationFrame(draw);
}
function start() { lastTime = 0; rafId = requestAnimationFrame(draw); }
function pause() { paused = true; cancelAnimationFrame(rafId); }
function resume() { paused = false; lastTime = 0; rafId = requestAnimationFrame(draw); }
document.getElementById('pauseBtn').onclick = pause;
document.getElementById('resumeBtn').onclick = resume;
start();
命令式控制灵活度极高,可以中途修改速度、插入缓动甚至合并多个动画时间轴。但代价是需要自行管理状态,若页面存在多个独立动画却共用一个paused变量,就容易出现A恢复而B仍冻结的错乱。推荐将每个动画实例化为对象,内部维护自己的标志位与rAF句柄,从架构层面隔离状态。
混合场景下的状态同步策略
实际项目常同时含有CSS加载动画与Canvas背景动效,例如步骤引导浮层弹出时,希望背后所有动画静音暂停以突出焦点。此时若只暂停Canvas而放任CSS旋转图标继续转,用户会感知到视觉重心冲突。统一控制层便显得必要:用一个全局MotionController收集所有可暂停单元,并对外暴露freeze与live方法。
实现上,CSS部分借助给body添加global-paused类,利用后代选择器批量设置animation-play-state: paused;Canvas部分则遍历注册过的实例调用其pause。恢复时反向操作即可。下表对比了三种粒度控制的适用边界。
| 控制粒度 | 实现方式 | 适用场景 |
|---|---|---|
| 单元素 | 切换自身类或变量 | 独立提示图标 |
| 容器级 | 父节点类覆盖子级动画 | 弹窗内局部冻结 |
| 全局 | 中央控制器广播状态 | 页面可见性变化 |
还需注意visibilitychange事件:当用户切到别的标签页,浏览器会自动降频rAF,但CSS动画在某些引擎仍会微幅推进。若业务要求严格静止,应在该事件中主动调用全局暂停,返回时再恢复。配合localStorage记录暂停前状态,还能在意外刷新后还原动画进度,提升长流程表单的体验连贯性。