You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Canvas绘制二次贝塞尔曲线的指定片段并实现消耗动画?

最优实现方案

有两种性能和精度都远优于手动打点模拟的方案,均可直接复用原生quadraticCurveTo的绘制能力:


方案1:使用setLineDash实现擦除动画(最简单)

利用Canvas虚线的偏移特性,不需要修改原有曲线绘制逻辑,仅需调整虚线参数即可实现「已移动部分擦除」的效果:

  1. 先提前计算二次贝塞尔曲线的总长度
  2. 动画时将虚线设置为「总长度的实线 + 总长度的透明段」,通过调整虚线偏移量,让实线部分只露出你需要的后段
  3. 圆形的移动坐标直接用你原有的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 09:15:01