贝塞尔曲线算法:求 t 在三阶贝塞尔曲线上的点、切向量、法向量

2024-07-31 19:59:55 浏览数 (3)

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

今天我们开始学习贝塞尔曲线的算法。

我们有 p1(锚点 1)、cp1(控制点 1)、cp2(控制点 2)、p2(锚点 2) 表示的一条三阶贝塞尔曲线,给定曲线参数 t,求其对应的点位置,以及这个点的切向量和法向量。

求 t 对应的点

贝塞尔曲线本质是 线性插值 的升阶。

2 个 点组成直线(或者叫线性贝塞尔曲线),基于 t 进行线性插值,拿到插值点,这便是线性插值。

代码语言:javascript复制
const lerp = (p1: Point, p2: Point, t: number) => {
  return {
    x: (1 - t) * p1.x   t * p2.x,
    y: (1 - t) * p1.y   t * p2.y,
  }
}

lerp 是 Linear interpolation(线性插值) 的缩写。

升阶为 3 个点(二阶贝塞尔曲线,p1、cp、p2),则这三个点依次连线,求出两个插值点,然后我们接着给这两个插值点的线性插值,得到 1 个带你。则这个点为该二阶贝塞尔曲线上 t 对应的点。

变成 4 个点(三阶贝塞尔曲线,p1、cp1、cp2、p2)也是同理,求出 3 个插值点,然后继续求出 2 个插值点,最后求出 1 个插值点。则这个点为该三阶阶贝塞尔曲线上 t 对应的点。

算法实现:

代码语言:javascript复制
/** 计算三阶贝塞尔曲线 t 对应的点 */
const getBezier3Point = (
  p1: Point,
  cp1: Point,
  cp2: Point,
  p2: Point,
  t: number,
) => {
  const a = lerp(p1, cp1, t);
  const b = lerp(cp1, cp2, t);
  const c = lerp(cp2, p2, t);

  const e = lerp(a, b, t);
  const f = lerp(b, c, t);
  return lerp(e, f, t);
};

上面这个算法还可以优化,将其化简成一个专用的三阶贝塞尔曲线公式,以减少运算量,读者可自行尝试。

算法对应的示意图:

如果变成 N 个点,也一样,计算 N-1 个插值点,然后是 N-2,最后变成只有 1 个的时候,就是这个 N 阶贝塞尔曲线 t 对应的点。

我们可以实现一个通用的方法:

代码语言:javascript复制
/** 计算 N 阶贝塞尔曲线 t 对应的点 */
const getBezierNPoint = (pts: Point[], t: number) => {
  while (pts.length > 1) {
    const nextPts = [];
    for (let i = 0, size = pts.length - 1; i < size; i  ) {
      nextPts.push(lerp(pts[i], pts[i   1], t));
    }
    pts = nextPts;
  }
  return pts[0];
};

求切向量

接着我们来求三阶贝塞尔曲线 t 所在点的切向量(tangent vector)。

切向量是描述曲线上某一点相切的向量。

上面我们知道,通过对多个连续点不断做线性插值,减少到插值点只有 1 个为止,此时这个点就是 t 对应的点。

那如果我们 让插值点保留位为 2 个,就能得到一条线,这条线便是 t 对应点的 切线

代码语言:javascript复制
/** 计算三阶贝塞尔曲线 t 位置的切线 */
const getBezier3TangentLine = (
  p1: Point,
  cp1: Point,
  cp2: Point,
  p2: Point,
  t: number,
) => {
  const a = lerp(p1, cp1, t);
  const b = lerp(cp1, cp2, t);
  const c = lerp(cp2, p2, t);

  return [lerp(a, b, t), lerp(b, c, t)];
};

切线求出来了,切向量自然也能计算出来了。这里使用贝塞尔前进方向为切向量方向。

代码语言:javascript复制
/** 计算三阶贝塞尔曲线 t 位置的切向量 */
const getBezier3Tangent = (
  p1: Point,
  p2: Point,
  cp1: Point,
  cp2: Point,
  t: number,
) => {
  const [a, b] = getBezier3TangentLine(p1, p2, cp1, cp2, t);
  const dist = distance(a, b);
  return {
    x: (b.x - a.x) / dist,
    y: (b.y - a.y) / dist,
  };
};

求法向量

法向量是垂直于点所在切线的向量。

也就是是切向量旋转 90 度。

法向量也有两个方向,这里我们选择贝塞尔前进方向的右方作为法向量方向。

代码语言:javascript复制
/** 计算三阶贝塞尔曲线 t 位置的法向量 */
const getBezier3Normal = (
  p1: Point,
  p2: Point,
  cp1: Point,
  cp2: Point,
  t: number,
) => {
  const tangent = getBezier3Tangent(p1, p2, cp1, cp2, t);
  return {
    x: -tangent.y,
    y: tangent.x,
  }
};

演示

结尾

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

0 人点赞