如何使用Canvas绘制二次贝塞尔曲线的指定片段并实现消耗动画?
最优实现方案
有两种性能和精度都远优于手动打点模拟的方案,均可直接复用原生quadraticCurveTo的绘制能力:
方案1:使用setLineDash实现擦除动画(最简单)
利用Canvas虚线的偏移特性,不需要修改原有曲线绘制逻辑,仅需调整虚线参数即可实现「已移动部分擦除」的效果:
- 先提前计算二次贝塞尔曲线的总长度
- 动画时将虚线设置为「总长度的实线 + 总长度的透明段」,通过调整虚线偏移量,让实线部分只露出你需要的后段
- 圆形的移动坐标直接用你原有的t值公式计算即可,仅需单次计算,开销可以忽略
示例代码:
const ctx = document.querySelector('canvas').getContext('2d'); // 贝塞尔曲线三个点 const p0 = {x:50, y:20}, p1 = {x:230, y:30}, p2 = {x:50, y:100}; // 预计算曲线总长度 const totalLength = getQuadraticBezierLength(p0, p1, p2); // 动画进度,0=完全显示,1=完全擦除 let progress = 0; function animate() { ctx.clearRect(0,0, ctx.canvas.width, ctx.canvas.height); // 绘制剩余曲线:虚线偏移量=已消耗长度,露出后面的部分 ctx.setLineDash([totalLength, totalLength]); ctx.lineDashOffset = progress * totalLength; // 你的原有绘制逻辑完全不用改 ctx.beginPath(); ctx.moveTo(p0.x, p0.y); ctx.quadraticCurveTo(p1.x, p1.y, p2.x, p2.y); ctx.stroke(); // 重置虚线避免影响其他绘制 ctx.setLineDash([]); // 绘制跟随移动的圆形 const t = progress; const x = (1-t)*(1-t)*p0.x + 2*(1-t)*t*p1.x + t*t*p2.x; const y = (1-t)*(1-t)*p0.y + 2*(1-t)*t*p1.y + t*t*p2.y; ctx.beginPath(); ctx.arc(x,y,4,0,Math.PI*2); ctx.fill(); // 更新进度 progress += 0.005; if(progress > 1) progress = 0; requestAnimationFrame(animate); } // 二次贝塞尔曲线长度计算函数,迭代30次精度足够用 function getQuadraticBezierLength(p0, p1, p2, steps = 30) { let length = 0; let prevX = p0.x, prevY = p0.y; for(let i = 1; i <= steps; i++) { const t = i / steps; const x = (1-t)*(1-t)*p0.x + 2*(1-t)*t*p1.x + t*t*p2.x; const y = (1-t)*(1-t)*p0.y + 2*(1-t)*t*p1.y + t*t*p2.y; const dx = x - prevX, dy = y - prevY; length += Math.sqrt(dx*dx + dy*dy); prevX = x; prevY = y; } return length; } animate();
配套的HTML代码和你原有一致:
<canvas></canvas>
方案2:直接截取目标区间的贝塞尔曲线(精度最高)
如果需要更灵活的曲线截取(比如不仅是后段,任意区间的子段),可以用德卡斯特里奥算法直接计算子段的三个控制点,直接用原生quadraticCurveTo绘制,完全没有锯齿,计算量极小:
比如你要绘制t∈[a, 1]的最后90%曲线,a=0.1,计算新的三个点:
function getQuadraticSubCurve(p0, p1, p2, tStart) { // 计算tStart处的点作为新起点 const x0 = (1-tStart)*(1-tStart)*p0.x + 2*(1-tStart)*tStart*p1.x + tStart*tStart*p2.x; const y0 = (1-tStart)*(1-tStart)*p0.y + 2*(1-tStart)*tStart*p1.y + tStart*tStart*p2.y; // 计算新控制点 const x1 = (1-tStart)*p1.x + tStart*p2.x; const y1 = (1-tStart)*p1.y + tStart*p2.y; // 原终点不变 const x2 = p2.x, y2 = p2.y; return [{x:x0,y:y0}, {x:x1,y:y1}, {x:x2,y:y2}]; }
使用时直接拿返回的三个点绘制即可:
// 绘制最后90%的曲线,a=0.1 const [newP0, newP1, newP2] = getQuadraticSubCurve(p0, p1, p2, 0.1); ctx.beginPath(); ctx.moveTo(newP0.x, newP0.y); ctx.quadraticCurveTo(newP1.x, newP1.y, newP2.x, newP2.y); ctx.stroke();
两种方案都完全解决了你提到的三个问题:
- 计算开销极低:长度计算仅需几十次迭代,子段截取仅需几次算术运算
- 精度完全由Canvas原生API保证,不需要考虑采样点数量
- 原生绘制自带抗锯齿,不会出现折线锯齿问题
内容的提问来源于stack exchange,提问作者Ryan Peschel
相关产品推荐
相关产品推荐

