如何在贝塞尔曲线上应用线性插值实现路径跟随
实现沿贝塞尔曲线路径移动的带方向三角形
核心思路
要让三角形沿贝塞尔曲线路径移动并指向运动方向,需要先预存每个贝塞尔段的控制点参数,再通过三次贝塞尔曲线的数学公式计算任意参数对应的位置和切线方向,最后基于位置和角度绘制旋转后的三角形。
步骤1:重构路径生成代码,预存贝塞尔段
原代码仅绘制路径,需修改为同时保存每个贝塞尔段的4个关键控制点(起点、两个控制点、终点),方便后续插值计算:
const bezierSegments = []; // 用于保存所有贝塞尔段的参数 // 生成路径并保存贝塞尔段 for (let i = 0; i <= thePoints.length - 2; i++) { if(i == thePoints.length - 2){ const lastPoint = thePoints[thePoints.length - 2]; const pt0 = thePoints[thePoints.length - 3]; const pt1 = thePoints[thePoints.length - 1]; const pt2 = thePoints[0]; const v1 = [(pt1[0] - pt0[0]) * 0.5, (pt1[1] - pt0[1]) * 0.5]; const v2 = [(pt2[0] - lastPoint[0]) * 0.5, (pt2[1] - lastPoint[1]) * 0.5]; // 保存当前贝塞尔段 bezierSegments.push({ p0: lastPoint, c0: [lastPoint[0] + v1[0]/3, lastPoint[1] + v1[1]/3], c1: [pt1[0] - v2[0]/3, pt1[1] - v2[1]/3], p1: pt1 }); // 绘制路径 context.beginPath(); context.moveTo(lastPoint[0], lastPoint[1]); context.bezierCurveTo(lastPoint[0] + (v1[0]/3), lastPoint[1] + (v1[1]/3), pt1[0] - (v2[0]/3), pt1[1] - (v2[1]/3), pt1[0], pt1[1]); context.stroke(); } else{ const pt0 = thePoints[i === 0 ? thePoints.length - 1 : i - 1]; const pt1 = thePoints[i]; const pt2 = thePoints[i + 1]; const pt3 = thePoints[i + 2] || pt0; const v1 = [(pt2[0] - pt0[0]) * 0.5, (pt2[1] - pt0[1]) * 0.5]; const v2 = [(pt3[0] - pt1[0]) * 0.5, (pt3[1] - pt1[1]) * 0.5]; // 保存当前贝塞尔段 bezierSegments.push({ p0: pt1, c0: [pt1[0] + v1[0]/3, pt1[1] + v1[1]/3], c1: [pt2[0] - v2[0]/3, pt2[1] - v2[1]/3], p1: pt2 }); // 绘制路径 context.beginPath(); context.moveTo(pt1[0], pt1[1]); context.bezierCurveTo(pt1[0]+(v1[0]/3), pt1[1]+(v1[1]/3), pt2[0]-(v2[0]/3), pt2[1]-(v2[1]/3), pt2[0], pt2[1]); context.stroke(); } } // 处理最后一段路径并保存贝塞尔段 const lastPoint = thePoints[thePoints.length - 1]; const pt0 = thePoints[thePoints.length - 2]; const pt1 = thePoints[0]; const pt2 = thePoints[1]; const v1 = [(pt1[0] - pt0[0]) * 0.5, (pt1[1] - pt0[1]) * 0.5]; const v2 = [(pt2[0] - lastPoint[0]) * 0.5, (pt2[1] - lastPoint[1]) * 0.5]; bezierSegments.push({ p0: lastPoint, c0: [lastPoint[0] + v1[0]/3, lastPoint[1] + v1[1]/3], c1: [pt1[0] - v2[0]/3, pt1[1] - v2[1]/3], p1: pt1 }); context.beginPath(); context.moveTo(lastPoint[0], lastPoint[1]); context.bezierCurveTo(lastPoint[0] + (v1[0]/3), lastPoint[1] + (v1[1]/3), pt1[0] - (v2[0]/3), pt1[1] - (v2[1]/3), pt1[0], pt1[1]); context.stroke();
步骤2:实现贝塞尔曲线的位置与切线计算函数
利用三次贝塞尔曲线的数学公式,计算任意参数t(0~1)对应的路径点和切线方向:
// 计算三次贝塞尔曲线上的点 function getBezierPoint(p0, c0, c1, p1, t) { const mt = 1 - t; const mt2 = mt * mt; const mt3 = mt2 * mt; const t2 = t * t; const t3 = t2 * t; const x = mt3 * p0[0] + 3 * mt2 * t * c0[0] + 3 * mt * t2 * c1[0] + t3 * p1[0]; const y = mt3 * p0[1] + 3 * mt2 * t * c0[1] + 3 * mt * t2 * c1[1] + t3 * p1[1]; return [x, y]; } // 计算三次贝塞尔曲线在t处的切线向量 function getBezierTangent(p0, c0, c1, p1, t) { const mt = 1 - t; const mt2 = mt * mt; const t2 = t * t; const dx = 3 * mt2 * (c0[0] - p0[0]) + 6 * mt * t * (c1[0] - c0[0]) + 3 * t2 * (p1[0] - c1[0]); const dy = 3 * mt2 * (c0[1] - p0[1]) + 6 * mt * t * (c1[1] - c0[1]) + 3 * t2 * (p1[1] - c1[1]); return [dx, dy]; }
步骤3:解析全局参数,获取位置与朝向
将输入的全局参数(范围0~点数,如1.5表示点1和点2之间)转换为对应贝塞尔段的索引和段内t值,再计算位置和旋转角度:
function getPositionAndDirection(param, bezierSegments) { const segmentCount = bezierSegments.length; // 处理参数超出范围的情况,实现路径循环 const normalizedParam = param % segmentCount; const adjustedParam = normalizedParam < 0 ? normalizedParam + segmentCount : normalizedParam; // 拆分出贝塞尔段索引和段内t值 const segmentIndex = Math.floor(adjustedParam); const t = adjustedParam - segmentIndex; const segment = bezierSegments[segmentIndex]; const pos = getBezierPoint(segment.p0, segment.c0, segment.c1, segment.p1, t); const tangent = getBezierTangent(segment.p0, segment.c0, segment.c1, segment.p1, t); // 计算切线对应的旋转角度(弧度) const angle = Math.atan2(tangent[1], tangent[0]); return { pos, angle }; }
步骤4:绘制带方向的三角形
基于计算出的位置和角度,平移并旋转坐标系后绘制三角形:
function drawTriangle(context, pos, angle, size = 20) { context.save(); // 平移到目标位置 context.translate(pos[0], pos[1]); // 旋转到运动方向 context.rotate(angle); // 绘制指向x轴正方向的三角形(旋转后自动指向运动方向) context.beginPath(); context.moveTo(size, 0); context.lineTo(-size/2, size/2); context.lineTo(-size/2, -size/2); context.closePath(); context.fillStyle = '#ff0000'; context.fill(); context.strokeStyle = '#000'; context.stroke(); context.restore(); }
步骤5:动画循环示例
通过更新全局参数,实现三角形沿路径持续移动:
let param = 0; const speed = 0.02; // 控制移动速度 function animate() { // 清除画布 context.clearRect(0, 0, canvas.width, canvas.height); // 绘制路径(若需要实时更新路径可保留) // drawPath(); // 获取当前位置与朝向 const { pos, angle } = getPositionAndDirection(param, bezierSegments); // 绘制三角形 drawTriangle(context, pos, angle); // 更新参数,实现循环移动 param += speed; if (param >= thePoints.length) param = 0; requestAnimationFrame(animate); } // 启动动画 animate();
内容的提问来源于stack exchange,提问作者BAK
相关产品推荐
相关产品推荐

