如何将一条贝塞尔曲线拆分为两条贝塞尔曲线?

224 阅读5分钟

大家好,我是前端西瓜哥。

图形编辑器中,我们有时候希望在 path 中微调一条曲线,在中间新增一个锚点,然后调整这个锚点和它的控制点。

图片

为了实现这个功能,我们需要实现算法:将一条贝塞尔曲线拆分为两条贝塞尔曲线

下面我们将以三阶贝塞尔为例进行说明,其他阶贝塞尔曲线是同理的。

我们需要用到 De Casteljau 算法,一般我们计算贝塞尔上 t 对应的点,会直接带入参数方程计算,De Casteljau 算法则是遵循古法,从贝塞尔的定义入手。

即将点依次相连为线段,然后进行 线性插值 取线段 t 位置上的点,然后继续同样的操作,这样不断递归线性插值直到只剩下一个点,这个点就是 t 在贝塞尔曲线上对应的点。

图片

交互演示

codesandbox.io/p/sandbox/p…

原理

假设我们想在 t1 的位置,将三阶贝塞尔拆成左右两部分。

图片

看图可知,对于左边,其实就是将原来 t 从 0 到 1 才走完所有的插值,变成 0 到 t1 就算走完,这样我们就能产生一个局部的三阶贝塞尔曲线

图片

上面这几个点就是拆分后左侧三阶贝塞尔的 4 个点,它们是线性插值递归过程中产生的。

右侧同理,为 t1 到 1 的这部分插值形成的曲线。

算法实现

所以算法实现就是:

const splitCubicBezier = (p1, p2, p3, p4, t) => {
  // 第一次线性插值
  const a = lerp(p1, p2, t);
  const b = lerp(p2, p3, t);
  const c = lerp(p3, p4, t);

  // 第二次线性插值
  const d = lerp(a, b, t);
  const e = lerp(b, c, t);

  // 第三次线性插值
  const f = lerp(d, e, t);

  return [
    [p1, a, d, f],
    [f, e, c, p4],
  ];
};

// 线性插值
const lerp = (p1Pointp2Pointt: number): Point => {
  return {
    x: p1.x + (p2.x - p1.x) * t,
    y: p1.y + (p2.y - p1.y) * t,
  };
};

可视化交互

写个交互验证一下。

图片

推广到任意阶贝塞尔

然后我们就发现了其中的规律,可以推广到任意阶的贝塞尔曲线。

我们将每轮线性插值产生的点记录下来,每一轮递归都会产生一批点,每一轮数量都会少一个,最后会形成一个倒三角的形状。

[p0,   p1,   p2,  p3]
[p01,  p12,  p23]
[p012, p123]
[p01234]

则左边为:
[p0, p01, p012, p01234]

右边为:
[p01234, p123, p23, p3]

左侧贝塞尔就是每个数组的第一个元素组成的顺序数组。

右侧贝塞尔就是每个数组末尾元素组成的倒序数组

算法实现:

const splitBezier = (points, t) => {
  const degree = points.length - 1// 贝塞尔阶数
  const layersPoint[][] = [[...points]]; // 存储每一层的插值点

  // 计算 de Casteljau 三角形
  for (let i = 1; i <= degree; i++) {
    const currentLayerPoint[] = [];
    const prevLayer = layers[i - 1];

    // 计算当前层的插值点
    for (let j = 0; j < degree - i + 1; j++) {
      currentLayer.push(lerp(prevLayer[j], prevLayer[j + 1], t));
    }

    layers.push(currentLayer);
  }

  // 取第一个元素,顺序
  const leftPointsPoint[] = [];
  for (let i = 0; i <= degree; i++) {
    leftPoints.push(layers[i][0]);
  }

  // 取末尾数组,倒序
  const rightPointsPoint[] = [];
  for (let i = degree; i >= 0; i--) {
    rightPoints.push(layers[i][layers[i].length - 1]);
  }

  return [leftPoints, rightPoints];
};

结尾

我是前端西瓜哥,关注我,学习更多平面几何知识。


相关阅读,

平面几何算法:求点到直线和圆的最近点

贝塞尔曲线:求点到贝塞尔曲线的投影

二阶贝塞尔曲线,如何升阶为三阶贝塞尔?

如何用三阶贝塞尔曲线拟合圆形、椭圆、任意圆弧?

贝塞尔曲线是什么?如何用 Canvas 绘制三阶贝塞尔曲线?