10
2025
12

贝赛尔曲线添加锚点-不改变曲线形状

以下内容来自AI。

在贝塞尔曲线上添加锚点且不改变曲线形状,核心算法是德卡斯特里奥(De Casteljau)算法。该算法能将一段贝塞尔曲线按指定参数 t 拆分为两段相连的同阶贝塞尔曲线,新增的锚点就是拆分处的曲线点,两段新曲线拼接后与原曲线完全重合,以此实现无形状变化的锚点添加。以下是具体实现逻辑、步骤及代码示例:

  1. 核心原理铺垫

    贝塞尔曲线的核心是通过参数 t(t∈[0,1])描述曲线上的位置。比如三阶贝塞尔曲线由起点 P0、两个控制点 P1/P2、终点 P3 定义,曲线上任意点 B (t) 可通过线性插值逐步计算得出。De Casteljau 算法的优势在于,拆分时不仅能确定拆分点(新增锚点),还能精准计算出两段新曲线的所有控制点,确保拆分前后曲线形态一致。

  2. 通用实现步骤

    以最常用的三阶贝塞尔曲线为例,完整的锚点添加流程如下:

    • 第一次线性插值:在各控制点连线上按 t 值取点,得到 Q0、Q1、Q2。其中 Q0 是 P0 和 P1 的插值点,Q1 是 P1 和 P2 的插值点,Q2 是 P2 和 P3 的插值点,插值公式为L(a,b,t)=a+(b-a)*t

    • 第二次线性插值:对第一次得到的点继续按 t 值插值,得到 R0、R1。R0 是 Q0 和 Q1 的插值点,R1 是 Q1 和 Q2 的插值点。

    • 第三次线性插值:对 R0 和 R1 按 t 值插值,得到拆分点 S,这个 S 就是曲线上 t 对应的点,也是新增的锚点。

    1. 确定拆分参数 t:先获取用户点击或选中的曲线位置,计算该位置对应的参数 t。具体是通过求点击点到曲线的最近点,遍历曲线计算距离后,确定距离最近点对应的 t 值(t∈(0,1)),该点就是新增锚点的位置。

    2. 应用 De Casteljau 算法拆分曲线:对原三阶贝塞尔曲线(P0,P1,P2,P3)执行算法,生成两段新的三阶贝塞尔曲线,步骤如下:

    3. 确定两段新曲线的控制点:拆分后得到的两段三阶贝塞尔曲线,控制点已由上述插值过程确定。第一段曲线的控制点为(P0, Q0, R0, S),第二段为(S, R1, Q2, P3)。两段曲线拼接处就是锚点 S,且平滑无断点,与原曲线完全一致。

  3. 代码示例(三阶贝塞尔曲线)

    以下代码基于 JavaScript 实现,输入原曲线控制点和拆分参数 t,输出拆分后的两段曲线数据,新增锚点即为拆分点 S:

javascript
运行
// 线性插值函数:计算两点a、b在参数t处的插值点function lerp(a, b, t) {
  return {
    x: a.x + (b.x - a.x) * t,
    y: a.y + (b.y - a.y) * t  };}// 拆分三阶贝塞尔曲线,返回两段新曲线function splitCubicBezier(p0, p1, p2, p3, t) {
  // 第一次插值
  const q0 = lerp(p0, p1, t);
  const q1 = lerp(p1, p2, t);
  const q2 = lerp(p2, p3, t);

  // 第二次插值
  const r0 = lerp(q0, q1, t);
  const r1 = lerp(q1, q2, t);

  // 第三次插值,得到拆分点(新增锚点)
  const s = lerp(r0, r1, t);

  // 两段新的三阶贝塞尔曲线
  const curve1 = { p0, p1: q0, p2: r0, p3: s };
  const curve2 = { p0: s, p1: r1, p2: q2, p3 };

  return { curve1, curve2, newAnchor: s };}// 示例:原曲线控制点const p0 = { x: 50, y: 200 };const p1 = { x: 100, y: 50 };const p2 = { x: 300, y: 50 };const p3 = { x: 350, y: 200 };const t = 0.4; // 拆分参数// 执行拆分const result = splitCubicBezier(p0, p1, p2, p3, t);console.log("新增锚点:", result.newAnchor);console.log("第一段曲线控制点:", result.curve1);console.log("第二段曲线控制点:", result.curve2);
  1. 拓展到其他阶次曲线

    该逻辑可适配不同阶次的贝塞尔曲线:

    • 二阶贝塞尔曲线(P0,P1,P2):只需两次线性插值。先插值得到 Q0(P0-P1)、Q1(P1-P2),再插值 Q0-Q1 得到拆分点 S。新曲线为(P0,Q0,S)和(S,Q1,P2)。

    • 高阶贝塞尔曲线:阶数为 n 时,需执行 n 次线性插值,最终同样能拆分为两段同阶曲线,新增锚点为最终插值点,保证曲线形状不变。

这种算法是 Illustrator、Figma 等设计工具添加锚点功能的底层逻辑,也是实现 Flash 中矢量图锚点编辑的核心技术之一。


在一条已有的贝塞尔曲线上增加一个锚点(分割曲线),但曲线的形状保持完全不变。

这类操作的核心算法是 De Casteljau(德卡斯特里奥)算法,它可以在任意参数  处,将一条贝塞尔曲线精确拆分成两段新的贝塞尔曲线,这两段拼接起来与原曲线完全一致。


1. 基本原理

对于一个 n 阶贝塞尔曲线(n=2 为二次,n=3 为三次等),控制点为:

在参数  处,De Casteljau 算法通过线性插值得到一系列中间点,直到得到曲线上对应的点 ,同时得到两组新的控制点:

  • 第一组控制点定义左半段曲线

  • 第二组控制点定义右半段曲线


2. 三次贝塞尔示例

原控制点:

plaintext复制P0, P1, P2, P3

算法步骤(t 为分割位置,比如 t=0.5 表示中点):

  1. 第一次插值

    plaintext复制P01 = (1-t)*P0 + t*P1P12 = (1-t)*P1 + t*P2
    P23 = (1-t)*P2 + t*P3
  2. 第二次插值

    plaintext复制P012 = (1-t)*P01 + t*P12P123 = (1-t)*P12 + t*P23
  3. 第三次插值(曲线上点)

    plaintext复制Q = (1-t)*P012 + t*P123
  4. 结果

    • 左半段控制点:P0, P01, P012, Q

    • 右半段控制点:Q, P123, P23, P3

这两条三次贝塞尔曲线拼起来就是原曲线,Q 就是新增的锚点。


3. 代码示例(JavaScript)

javascript复制function splitCubicBezier(P0, P1, P2, P3, t) {  function lerp(a, b, t) {    return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };  }  const P01 = lerp(P0, P1, t);  const P12 = lerp(P1, P2, t);  const P23 = lerp(P2, P3, t);  const P012 = lerp(P01, P12, t);  const P123 = lerp(P12, P23, t);  const Q = lerp(P012, P123, t);  return {    left:  [P0, P01, P012, Q],    right: [Q, P123, P23, P3]  };}

调用示例:

javascript复制const P0 = {x:0, y:0}, P1 = {x:10, y:15}, P2 = {x:20, y:15}, P3 = {x:30, y:0};const result = splitCubicBezier(P0, P1, P2, P3, 0.5);console.log(result.left, result.right);

4. 特点

  • 形状完全不变:因为只是数学上拆分曲线。

  • 适用任意阶贝塞尔:不仅是三次,二次、一阶都可以用同样的递归插值方法。

  • 可多次分割:可以在不同 t 上反复分割,得到多个锚点。



« 上一篇下一篇 »

相关文章:

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。