
本文详解如何将用户在 html5 canvas 上的手绘轨迹(含贝塞尔曲线平滑)完整录制为坐标序列,并通过 requestanimationframe 实现流畅、可控的逐点回放,解决因绘制过快导致的视觉断裂问题。
本文详解如何将用户在 html5 canvas 上的手绘轨迹(含贝塞尔曲线平滑)完整录制为坐标序列,并通过 requestanimationframe 实现流畅、可控的逐点回放,解决因绘制过快导致的视觉断裂问题。
在 Canvas 中实现“录制—回放”功能时,常见误区是直接复用原始 draw() 逻辑进行批量重绘——这不仅会叠加历史笔迹、污染画布,更因缺乏节奏控制而使动画瞬间完成,丧失回放意义。正确方案需从职责分离、画布隔离与帧率节制三方面重构。
✅ 核心改进策略
职责解耦:提取绘制核心逻辑
将坐标到路径渲染的底层操作(如 moveTo、bezierCurveTo、stroke)封装为独立函数 add(x, y)。该函数不关心数据来源(鼠标事件 or 回放数组),只专注单次笔触渲染,确保 draw() 与 play() 共享同一绘制引擎。-
画布重置:保障回放纯净性
每次点击“回放”按钮前,必须清空画布:context.clearRect(0, 0, canvas.width, canvas.height);
否则新回放轨迹将叠加在原画布上,造成视觉混乱。
帧率可控:用 requestAnimationFrame + 边界检查实现渐进式播放
避免 for 循环导致的瞬时绘制。改用递归调用 requestAnimationFrame,并在每帧后检查 step
? 完整回放实现代码
// 初始化上下文(推荐使用 const 声明避免污染)
const canvas = document.querySelector('#surface');
const ctx = canvas.getContext('2d');
// 存储所有坐标点(格式:{x: number, y: number})
const coordinates = [];
// 【关键】独立绘制函数:接收坐标,执行贝塞尔平滑绘制
function add(x, y) {
ctx.shadowColor = "rgba(0,0,0,.5)";
ctx.shadowBlur = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = 2;
ctx.strokeStyle = 'red';
if (!inProgress) {
ctx.beginPath();
ctx.moveTo(x, y);
inProgress = true;
skip1 = true;
skip2 = false;
} else {
if (skip1) {
cp1x = x; cp1y = y;
skip1 = false; skip2 = true;
} else if (skip2) {
cp2x = x; cp2y = y;
skip1 = false; skip2 = false;
} else {
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
skip1 = true; skip2 = false;
}
}
ctx.stroke();
}
// 【回放主函数】
document.getElementById('coordinatesDrawBtn').addEventListener('click', () => {
// 1. 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 2. 重置状态变量(重要!否则平滑算法错乱)
inProgress = false;
skip1 = skip2 = false;
step = 0;
// 3. 启动帧动画
function animate() {
if (step < coordinates.length) {
add(coordinates[step].x, coordinates[step].y);
step++;
requestAnimationFrame(animate); // 浏览器自动调度,约60fps
}
}
animate();
});⚠ 注意事项与最佳实践
- 状态变量重置不可省略:inProgress、skip1、skip2 等控制贝塞尔锚点计算的状态,在每次回放前必须显式重置,否则首段曲线可能异常。
- 坐标精度建议:若需更高保真度,可对原始坐标做防抖采样或记录时间戳,回放时按时间插值(本例为简化采用等间隔帧)。
- 性能优化提示:对于超长轨迹(>5000 点),可考虑分段 requestAnimationFrame 或添加加载提示,避免主线程阻塞。
- 移动端兼容性:当前代码已处理 touchmove 阻止默认行为,但回放按钮建议增加 touchstart 事件监听以提升响应性。
✅ 总结
Canvas 回放的本质不是“重复执行”,而是“按序触发渲染”。通过抽离 add() 函数统一绘制入口、强制清空画布保证环境纯净、利用 requestAnimationFrame 实现自然帧率,即可优雅实现带平滑曲线的书写回放。此模式可无缝扩展至压力感应、速度渐变、多色轨迹等高级场景,是构建交互式绘图工具的坚实基础。










