如何无需高成本预计算即可实现贝塞尔曲线匀速运动
二次贝塞尔曲线没有直接通过长度百分比计算坐标的闭式解析公式,不过可以用高斯积分+牛顿迭代的方案完全替代你现在用的预计算查找表,性能、精度、内存占用都远好于现有方案。
核心实现逻辑
- 首先实现弧长快速计算函数:用2阶或3阶高斯-勒让德积分,仅需3~5个采样点就能得到误差小于1e-6的弧长结果,比逐段累加1000次的效率高100倍以上。
- 然后实现牛顿迭代求解t值:给定长度百分比s(01),通过迭代快速逼近对应的参数t,一般34次迭代就能达到视觉无差的精度。
修改后完整代码
let c = document.querySelector("canvas"); let ctx = c.getContext("2d"); class BezierCurve { constructor(x1, y1, cpX, cpY, x2, y2) { this.f = 0; this.x1 = x1; this.y1 = y1; this.cpX = cpX; this.cpY = cpY; this.x2 = x2; this.y2 = y2; // 仅预计算总长度,一次计算即可 this.totalLength = this.calcArcLength(1); } calcX(t) { return (1 - t) * (1 - t) * this.x1 + 2 * (1 - t) * t * this.cpX + t * t * this.x2; } calcY(t) { return (1 - t) * (1 - t) * this.y1 + 2 * (1 - t) * t * this.cpY + t * t * this.y2; } // 计算t处一阶导数的模长(即弧长对t的导数) calcDerivativeMod(t) { const dx = 2 * (1 - t) * (this.cpX - this.x1) + 2 * t * (this.x2 - this.cpX); const dy = 2 * (1 - t) * (this.cpY - this.y1) + 2 * t * (this.y2 - this.cpY); return Math.sqrt(dx * dx + dy * dy); } // 3阶高斯-勒让德积分计算0到t的弧长 calcArcLength(t) { const gaussPoints = [ { x: -0.7745966692, w: 0.5555555556 }, { x: 0, w: 0.8888888889 }, { x: 0.7745966692, w: 0.5555555556 } ]; let sum = 0; const halfT = t / 2; for (const p of gaussPoints) { const curT = halfT * (p.x + 1); sum += p.w * this.calcDerivativeMod(curT); } return halfT * sum; } // 输入长度百分比s(0~1),返回对应的贝塞尔参数t getTByLengthPercent(s) { if (s <= 0) return 0; if (s >= 1) return 1; let t = s; const targetLength = s * this.totalLength; // 迭代4次即可达到1e-6级精度 for (let i = 0; i < 4; i++) { const currentLength = this.calcArcLength(t); const derivative = this.calcDerivativeMod(t); if (derivative < 1e-8) break; t -= (currentLength - targetLength) / derivative; t = Math.max(0, Math.min(1, t)); } return t; } draw() { ctx.beginPath(); ctx.moveTo(this.x1, this.y1); ctx.quadraticCurveTo(this.cpX, this.cpY, this.x2, this.y2); ctx.stroke(); } tick(amount = 0.001) { this.f = this.f < 1 ? this.f + amount : 0; } } function drawCircle(x, y, r) { ctx.beginPath(); ctx.arc(x, y, r, 0, 2 * Math.PI); ctx.fill(); } let a = new BezierCurve(25, 25, 80, 250, 100, 50); let b = new BezierCurve(225, 25, 280, 250, 300, 50); function draw(curve, fraction) { let x = curve.calcX(fraction); let y = curve.calcY(fraction); curve.draw(); drawCircle(x, y, 5); curve.tick(); } function drawLoop(elapsed) { c.width = 600; c.height = 600; draw(a, a.f); // 直接调用方法获取对应长度百分比的参数t draw(b, b.getTByLengthPercent(b.f)); requestAnimationFrame(drawLoop); } drawLoop(0);
<canvas></canvas>
方案优势
- 内存占用极低:仅存储一个总长度数值,不需要存储上千个映射点
- 初始化开销几乎为0:实例化时仅计算一次总长度,3次采样即可完成,远低于原方案预计算1000次的开销
- 精度可控:普通视觉场景3次迭代就足够,需要更高精度时增加迭代次数即可
- 运行时性能高:单次查询仅需4次迭代,每次迭代只有3次采样计算,每帧调用上百次也不会有性能压力
你需要的50%长度位置坐标可以直接通过curve.calcX(curve.getTByLengthPercent(0.5))获取,完全符合需求。
内容的提问来源于stack exchange,提问作者Ryan Peschel
相关产品推荐
相关产品推荐

