如何计算贝塞尔曲线上弧长中点及实现点沿曲线匀速运动?
贝塞尔曲线参数t与匀速移动解决方案
你当前使用的二次贝塞尔曲线计算公式完全符合数学定义,出现偏差的核心原因是贝塞尔曲线的参数t不属于弧长参数:t仅代表参数空间的插值进度,和沿曲线实际移动的路程长度没有线性对应关系,所以t=0.5对应的点不是曲线的几何中点,同时也会出现沿曲线变速移动的现象。
匀速移动实现方案
实现沿贝塞尔曲线匀速移动的通用方案是弧长重映射,通过预计算弧长查找表建立路程进度到参数t的映射关系,步骤如下:
1. 预生成弧长查找表
将t从0到1切分为足够多的采样段(通常100~200段即可满足普通场景精度需求),依次计算每个采样点的坐标,累加相邻两点的直线距离,得到每个采样t对应的累计弧长,存储为查找表。
// 采样精度,数值越高精度越高 const SAMPLE_COUNT = 100; // 弧长查找表,每个元素结构为 { t: 采样t值, len: 累计弧长 } let arcLengthTable = []; function buildArcLengthTable() { arcLengthTable = []; let prevX = x1, prevY = y1; let totalLen = 0; arcLengthTable.push({ t: 0, len: 0 }); for (let i = 1; i <= SAMPLE_COUNT; i++) { const t = i / SAMPLE_COUNT; const x = calcX(t); const y = calcY(t); // 累加当前段距离 const dx = x - prevX; const dy = y - prevY; totalLen += Math.sqrt(dx*dx + dy*dy); arcLengthTable.push({ t, len: totalLen }); prevX = x; prevY = y; } }
2. 路程进度转参数t
当你需要获取沿曲线走了s比例(s范围0~1,0为起点,1为终点)的点时,先计算目标弧长为总弧长*s,再在查找表中匹配对应区间插值得到实际t值:
function getTByProgress(s) { const totalLength = arcLengthTable[arcLengthTable.length - 1].len; const targetLen = s * totalLength; // 二分查找目标弧长所在的区间 let low = 0, high = arcLengthTable.length - 1; while (low < high) { const mid = Math.floor((low + high) / 2); if (arcLengthTable[mid].len < targetLen) { low = mid + 1; } else { high = mid; } } // 区间边界处理 if (low === 0) return arcLengthTable[0].t; // 线性插值得到精确t值 const prev = arcLengthTable[low - 1]; const curr = arcLengthTable[low]; const segmentLen = curr.len - prev.len; const segmentProgress = (targetLen - prev.len) / segmentLen; return prev.t + segmentProgress * (curr.t - prev.t); }
3. 替换原有逻辑
在动画循环中,直接将进度s传入getTByProgress得到实际t,再计算坐标即可实现匀速移动,此时s=0.5对应的点就是曲线的几何中点,符合大众认知。
修改后的动画循环核心逻辑:
let progress = 0; function drawLoop(elapsed) { c.width = 600; c.height = 600; // 用进度获取映射后的t值 const t = getTByProgress(progress); let x = calcX(t); let y = calcY(t); drawCurve(); ctx.beginPath(); ctx.rect(x, y, 3, 3); ctx.stroke(); progress = progress < 1 ? progress + 0.001 : 0; document.querySelector(".debug").innerHTML = progress.toFixed(2); requestAnimationFrame(drawLoop); } // 初始化时先构建弧长表 buildArcLengthTable(); drawLoop(0);
优化提示
如果你的曲线控制点是动态变化的,每次控制点修改后重新调用buildArcLengthTable更新查找表即可。如果对性能要求极高,可以降低采样精度,或者使用近似公式计算弧长,普通场景下100次采样的性能损耗可以忽略不计。
内容的提问来源于stack exchange,提问作者Ryan Peschel
相关产品推荐
相关产品推荐

