Canvas 动画工程实践:requestAnimationFrame、停启与时间步长
本文目录
Canvas 动画通常由三个步骤组成:根据时间更新状态、清理需要重绘的区域、按当前状态绘制。requestAnimationFrame() 是一次性请求,回调中要再次请求下一帧;它通常贴合屏幕刷新节奏,后台标签页也可能暂停。它不是固定 60 FPS 的计时器。
用时间推进状态
如果每帧都把位置加固定像素,120Hz 屏幕上的动画会比 60Hz 快一倍。使用回调提供的高精度时间戳计算经过的秒数,再按速度更新状态。暂停后恢复时限制最大 delta,避免长时间后台停留造成物体瞬间跳过屏幕。
function createCanvasMotion(canvas) {
const context = canvas.getContext('2d');
if (!context) return () => {};
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
let frameId = 0;
let lastTime = 0;
let x = 24;
let running = false;
function render() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = '#0f766e';
context.fillRect(x, 32, 24, 24);
}
function frame(now) {
if (!running) return;
const seconds = lastTime ? Math.min((now - lastTime) / 1000, 0.05) : 0;
lastTime = now;
x = (x + 120 * seconds) % Math.max(1, canvas.width - 24);
render();
frameId = requestAnimationFrame(frame);
}
function stop() {
running = false;
cancelAnimationFrame(frameId);
frameId = 0;
lastTime = 0;
}
function start() {
if (running || document.hidden || reducedMotion.matches) return;
running = true;
lastTime = 0;
frameId = requestAnimationFrame(frame);
}
function onVisibilityChange() {
if (document.hidden) stop();
else start();
}
function onMotionChange() {
if (reducedMotion.matches) {
stop();
render();
} else {
start();
}
}
document.addEventListener('visibilitychange', onVisibilityChange);
reducedMotion.addEventListener('change', onMotionChange);
render();
start();
return () => {
stop();
document.removeEventListener('visibilitychange', onVisibilityChange);
reducedMotion.removeEventListener('change', onMotionChange);
};
}
const canvas = document.querySelector('#motion');
const disposeMotion = canvas ? createCanvasMotion(canvas) : () => {};
cancelAnimationFrame() 取消排队中的帧;组件卸载时还要移除事件监听并阻止再次启动。若模拟对数值稳定性敏感,采用固定时间步长累积器并设置单帧更新上限,而非把超大 delta 一口气喂给物理更新。
清除与重绘
clearRect() 清除画布像素,但不会重置变换、裁剪或绘图样式。高 DPR 画布已经把坐标系缩放到 CSS 像素时,清除尺寸也要用同一逻辑坐标;如果直接改 canvas.width/height,缓冲区会清空且上下文状态恢复默认值。
简单场景可以每帧清全屏;复杂场景可比较脏矩形、分层 canvas、静态背景缓存或 OffscreenCanvas,但先在低端设备测量,避免复杂化后反而增加复制和合成成本。限制帧内工作,避免同步读取布局或在每帧分配大量对象。
可控且不打扰的动态效果
提供真实的暂停/继续控件,且让静态内容本身可理解。尊重 prefers-reduced-motion:非必要运动可停止或替换成静止状态。不能只在 prefers-reduced-motion 下“把动画速度改快”,却让用户继续看到大幅运动。离开页面、切换路由、隐藏标签页和销毁组件都要停止循环。
发布前检查 60/120/144Hz、后台切换、重复 start/stop、组件卸载、窗口缩放、DPR 变化、无 2D context、减少动态效果和屏幕阅读器替代说明。
参考:MDN requestAnimationFrame、MDN cancelAnimationFrame、Canvas clearRect、Page Visibility API、prefers-reduced-motion、WHATWG Canvas。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。