贝塞尔曲线算法:求贝塞尔曲线的包围盒

606 阅读8分钟

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

今天来实现贝塞尔曲线的包围盒算法。

本文将以三阶贝塞尔曲线为例。

图片

由图可知,要求得贝塞尔曲线的包围盒,我们就需要求出贝塞尔曲线上的转折点,然后加上贝塞尔起点和终点,得出包围这些点的包围盒。

这些点的特点为,x 或 y 的值从递增转为递减状态或反过来,这两个状态的分界点,便是要求的转折点。我们要找出这些点对应的 t。

你也可以认为找出贝塞尔曲线上 切线斜率为 0 或无穷大 对应的点,二者是等价的。

导函数

三阶贝塞尔曲线的参数方程为:

图片

P0 到 P3 表示三阶贝塞尔曲线顺序的 4 个点。

需要注意的是我们用的是参数方程,所有对应的是两个方程 x=f(t)y=g(t),x 和 y 是要隔离开分别计算的。

我们对这个方程求导,得到对应的 导函数。这个要用到高中知识套公式来计算,这里直接给出结果。

图片

这个导函数代表的几何意义,是 x 或 y 曲线在 t 位置上的斜率

顺带一提,我们仔细观察这个导函数的特征,会发现它是一个二阶贝塞尔曲线,其点值为三阶贝塞尔相邻点相减然后乘以 3。

找到导函数中 f(t) 为 0 的点(原函数上斜率为 0 的点),那就是转折点了。

解方程

我们把导函数转换一下,尝试转换为标准的一元二次方程。

在这之前,我们假设:

const a = 3 * (P1 - P0);
const b = 3 * (P2 - P1);
const c = 3 * (P3 - P2);

导函数就变成了:

图片

转换为标准一元二次方程:

图片

B'(t) 等于 0,求 t。

图片

我们用 求根公式 求这个一元二次方程。

首先判断二次项系数 a - 2b + c 是否为 0。

一元二次方程的求根公式

如果不为 0,说明是一元二次方程。接着我们就用求根公式求解了。

图片

(注意这里的 a 并不对应上面我们的 a)

约分得到:

图片

// 二次项系数,同时求根公式的分母 denominator
const d = a - 2 * b + c;

if (d !== 0) {
  // 正常的一元二次方程
  // 一元二次方程组的判别式 delta 的的化简版的平方,常数提取出去了,并被约掉了。
  const deltaSquare = b * b - a * c;
  if (deltaSquare < 0) {
    // 负数,方程无实数根,无解
    return [];
  }
  const delta = Math.sqrt(deltaSquare);
  const m = a - b;
  if (delta === 0) {
    // 两个相等的实数根
    return [(m - delta) / d];
  } else {
    // 两个不等的实数根
    return [(m - delta) / d, (m + delta) / d];
  }
}

如果二次项系数是 0,且一次项系数不为 0,说明是一元一次方程。

如果一次项系数也是 0,那就啥也不是。

if (d !== 0) {
  // 正常的一元二次方程
  // ...else if (a !== b) {
  // d 为 0,代表一元二次方程的 x^2 前面的系数也是 0,退化为一元一次方程,只有一个解
  // 但也要确保 x 前的系数不为 0,否则连一次方程都不是了
  return [a / (a - b) / 2];
} else {
  return [];
}

讨论完所有的情况,最后我们计算出 t 的值。

然后记得丢掉不在 [0, 1] 区间内的 t。

extrema.filter((t) => t >= 0 && t <= 1);

计算点位置和包围盒

求出所有极值点的 t 后,我们计算 t 对应的贝塞尔曲线上的点,这个我们之前的文章讲过,直接套贝塞尔公式即可。

然后再加上贝塞尔曲线的起点和终点,计算包围盒。

完整代码

还是看看完整代码吧。

/** 求三阶贝塞尔曲线包围盒 */
const getBezier3Bbox = (pts: Point[]) => {
  const extremas = bezier3Extrema(pts);
  const extremaPts = extremas.map((t) => getBezier3Point(pts, t));
  return getPointsBbox([...extremaPts, pts[0], pts[pts.length - 1]]);
};

/** 计算三阶贝塞尔曲线 t 对应的点(套用参数方程公式) */
const getBezier3Point = (pts: Point[], t: number) => {
  const t2 = t * t;
  const ct = 1 - t;
  const ct2 = ct * ct;
  const a = ct2 * ct;
  const b = 3 * t * ct2;
  const c = 3 * t2 * ct;
  const d = t2 * t;

  const [p1, cp1, cp2, p2] = pts;

  return {
    x: a * p1.x + b * cp1.x + c * cp2.x + d * p2.x,
    y: a * p1.y + b * cp1.y + c * cp2.y + d * p2.y,
  };
};

/* 求三阶贝塞尔曲线的转折点 */
const bezier3Extrema = (pts: Point[]) => {
  // 先求 y 维度的极值
  const extrema = getRoot(
    3 * (pts[1].y - pts[0].y),
    3 * (pts[2].y - pts[1].y),
    3 * (pts[3].y - pts[2].y),
  );

  // 再求 x
  extrema.push(
    ...getRoot(
      3 * (pts[1].x - pts[0].x),
      3 * (pts[2].x - pts[1].x),
      3 * (pts[3].x - pts[2].x),
    ),
  );

  // 这里可以再做个去重
  return extrema.filter((t) => t >= 0 && t <= 1);
};

/* 求方程的根 */
const getRoot = (a: number, b: number, c: number) => {
  // 二次项系数,同时求根公式的分母 denominator
  const d = a - 2 * b + c;

  if (d !== 0) {
    // 正常的一元二次方程
    // 一元二次方程组的判别式 delta 的的化简版的平方,常数提取出去了,并被约掉了。
    const deltaSquare = b * b - a * c;
    if (deltaSquare < 0) {
      // 负数,方程无实数根,无解
      return [];
    }
    const delta = Math.sqrt(deltaSquare);
    const m = a - b;
    if (delta === 0) {
      // 两个相等的实数根
      return [(m - delta) / d];
    } else {
      // 两个不等的实数根
      return [(m - delta) / d, (m + delta) / d];
    }
  } else if (a !== b) {
    // d 为 0,代表一元二次方程的 x^2 前面的系数也是 0,退化为一元一次方程,只有一个解
    // 但也要确保 x 前的系数不为 0,否则连一次方程都不是了
    return [a / (a - b) / 2];
  } else {
    return [];
  }
};

const getPointsBbox = (points: Point[]) => {
  let minX = Infinity;
  let minY = Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;

  for (const pt of points) {
    minX = Math.min(minX, pt.x);
    minY = Math.min(minY, pt.y);
    maxX = Math.max(maxX, pt.x);
    maxY = Math.max(maxY, pt.y);
  }

  return {
    minX,
    minY,
    maxX,
    maxY,
  };
};

interface Point {
  xnumber;
  ynumber;
}

效果

图片

结尾

全是数学公式,没有一点技巧。

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


相关阅读,

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

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

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

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

图形编辑器开发:钢笔工具功能说明书