Canvas平滑滚动需用time-based progress驱动,通过缓动函数映射位置,严格按清除→计算→重绘顺序执行,并在交互时动态校准时间轴实现无缝衔接。

Canvas 画布内实现平滑滚动,核心不是“慢慢挪动”,而是用时间驱动的位置插值——所有滚动状态都由一个 0 到 1 的 progress 值 决定,再通过缓动函数映射到实际坐标。关键在于清除、计算、重绘三步不可颠倒,且不能靠帧数或手动累加。
滚动位置必须由 time-based progress 驱动
不要写 scrollX += 2 这类固定增量逻辑。它会导致不同设备帧率下速度不一致,掉帧时还会卡顿或跳变。
- 记录起始时间:
startTime = performance.now() - 每帧计算已过时长:
elapsed = performance.now() - startTime - 算出归一化进度:
progress = Math.min(elapsed / duration, 1)(duration 是总滚动时长,比如 300ms) - 用缓动函数处理 progress:
eased = easeOutCubic(progress),再乘以总位移量得到真实滚动偏移
推荐使用 easeOutCubic 等物理感更强的缓动
CSS 的 cubic-bezier(0.215, 0.61, 0.355, 1) 对应的 easeOutCubic,在 Canvas 中可直接实现:
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
比起线性或默认 ease,它让滚动在结束前明显减速,更符合手指松开后惯性滑动的直觉。若需更柔和,可用 easeOutQuint 或自定义贝塞尔系数。
滚动过程必须绑定 requestAnimationFrame
每次回调中严格按顺序执行:
- 先清空画布:
ctx.clearRect(0, 0, canvas.width, canvas.height) - 再根据当前
progress计算所有可视元素的新坐标(含背景、内容块、滚动条 thumb) - 最后一次性重绘全部内容
避免在循环里只 redraw 部分区域——Canvas 是位图,残留旧像素会引发拖影或重叠。
用户交互时要实时校准时间轴
当拖拽滚动条、点击锚点或用键盘翻页时,不要中断 rAF 循环再重置计数器。正确做法是:
- 立刻更新
startTime = performance.now() - (targetProgress * duration),使新起点对应目标位置 - 后续帧仍用同一套 progress → position 映射逻辑,视觉上无缝衔接
- 若目标已到达(
progress ≥ 1),停止动画但保留最终位置,不强制跳帧
这样既响应迅速,又保持运动连贯性,不会出现“闪一下再滑”的断裂感。











