深刻理解贝塞尔曲线

贝塞尔曲线(Bezier Curve)在计算机图形领域应用很是普遍,好比咱们熟知的 CSS 动画、 Canvas 以及 Photoshop 等均可以看到贝塞尔曲线的身影。javascript

文章目录

1、什么是贝塞尔曲线?

贝塞尔曲线于 1962 年,由法国工程师皮埃尔·贝济埃(Pierre Bézier)所普遍发表,他运用贝塞尔曲线来为汽车的主体进行设计。java

贝塞尔曲线主要用于二维图形应用程序中的数学曲线,曲线由起始点,终止点(也称锚点)和控制点组成,经过调整控制点,经过必定方式绘制的贝塞尔曲线形状会发生变化。后面会具体介绍绘制的方法。git

在计算机图形学中贝赛尔曲线的运用很普遍,例如Photoshop中的钢笔效果,Flash5的贝塞尔曲线工具,在软件GUI开发中通常也会提供对应的方法来实现贝赛尔曲线,咱们熟知的CSS动画过渡时间函数也是经过贝塞尔曲线(三阶贝塞尔曲线)获取的。github

2、贝塞尔曲线分为哪些类型?

贝塞尔曲线根据控制点的数量分为:segmentfault

  • 一阶贝塞尔曲线(2 个控制点)
  • 二阶贝塞尔曲线(3 个控制点)
  • 三阶贝塞尔曲线(4 个控制点)
  • n阶贝塞尔曲线(n+1个控制点)

3、贝塞尔曲线是如何绘制出来的?

下图为一个三阶的贝塞尔曲线,包括四个控制点,分别为P_0,P_1,P_2,P_3数组

三阶贝塞尔曲线

那咱们经过控制点是怎么绘制出贝塞尔曲线的呢?缓存

经过上图的三阶贝塞尔曲线举例,基本的步骤以下:函数

  1. 四个控制点经过前后顺序进行链接,造成了三条线段,也就是上图中的P_0P_1,P_1P_2,P_2P_3,而后经过一个参数t,其中 t\in[0,1],该参数的值等于线段上某一个点距离起点的长度除以线段长度。就好比P_0P_1线段上有一个点P_0^{'},此时t的值就是\frac{P_0P_0^{'}}{P_0P_1},其中P_0^{'}位置以下图所示。

bezier-01

  1. 接下来对每一条线段作一样的操做,获得三个控制点P_0^{'},P_1^{'},P_2^{'},以下图所示。

bezier-02

  1. 而后对这三个控制点重复第1步操做,得出两个控制点P_0^{''},P_1^{''},以下图所示。

bezier-03

  1. 最后再使用一样的方法能够获得,最终的一个点P_0^{'''},以下图所示,此时这个点就是贝塞尔曲线上的一个点。

bezier-04

经过控制t的值,由 0 增长至 1,就绘制出了一条由起点P_0至终点P_1的贝塞尔曲线。工具

你能够经过下面这个动画直观感觉一下绘制的过程:动画

三阶贝塞尔曲线绘制过程

4、如何求贝塞尔曲线上的点坐标?

一、一阶贝塞尔曲线

一阶贝塞尔曲线绘制过程

对于一阶贝塞尔曲线,咱们能够经过几何知识,很容易根据t的值得出线段上那个点的坐标:

B_{1}(t) = P_0 + (P_1 - P_0)t

而后能够得出:

B_{1}(t) = (1 - t)P_0 + tP_1,t\in[0,1]

二、二阶贝塞尔曲线

二阶贝塞尔曲线绘制过程

对于二阶贝塞尔曲线,其实你能够理解为:在P_0P_1上利用一阶公式求出点P_0^{'},而后在P_1P_2上利用一阶公式求出点P_1^{'},最后在P_0^{'}P_1^{'}上再利用一阶公式就能够求出最终贝塞尔曲线上的点P_0{''}。具体推导过程以下:

先求出线段上的控制点。

P_0^{'} = (1 - t)P_0 + tP_1
P_1^{'} = (1 - t)P_1 + tP_2

将上面的公式带入至下列公式中:

B_{2}(t) = (1 - t)P_0^{'} + tP_1^{'}
= (1 - t)((1 - t)P_0 + tP_1) + t((1 - t)P_1 + tP_2)
= (1 - t)^2P_0 + 2t(1 - t)P_1 + t^2P_2

得出如下公式:

B_{2}(t) = (1 - t)^2P_0 + 2t(1 - t)P_1 + t^2P_2 , t\in[0, 1]

三、三阶贝塞尔曲线

三阶贝塞尔曲线绘制过程

与二阶贝塞尔曲线相似,能够经过相同的方法得出如下坐标公式:

B_{3}(t) = (1 - t)^3P_0 + 3t(1 - t)^2P_1 + 3t^2(1 - t)P_2 + t^3P_3 , t\in[0, 1]

四、多阶贝塞尔曲线

这里我就直接把n阶贝塞尔曲线公式给出来了,有兴趣的同窗能够自行研究一下。

B(t) = \sum_{i=0}^{n}C_n^{i}P_i(1-t)^{n-i}t^i,t\in[0,1]

即:

B(t) = \sum_{i=0}^{n}P_ib_{i,n}(t),t\in[0,1]

公式中C_n^i的值为\frac{n!}{(n - i)!\cdot i!},与统计学有关,有兴趣的同窗能够看一看个人这篇文章

其中b_{i,n}(t)的值为:

b_{i,n}(t)=C_n^{i}(1-t)^{n-i}t^i,其中i=0,1,...,n

5、如何实现一个相似CSS中easing属性的三阶贝塞尔曲线构造函数?

若是要实现一个这样的三阶贝塞尔曲线,咱们须要不只须要获取到一些曲线上的点,还须要经过x轴获取y轴坐标。

CSS中的easing贝塞尔曲线有一个特色,那就是起点和终点是固定的,也就是分别是[0, 0],\ [1,1]。因此未知的点就只有两个,也就是须要传入四个值,而且这四个值的范围须要在[0,1]内。

因此咱们须要建立一个类CubicBezier,它拥有属性controlPoints

class CubicBezier {
  constructor(x1, y1, x2, y2) {
    this.controlPoints = [x1, y1, x2, y2];
  }
}
复制代码

经过上述代码初始化之后,咱们还须要根据t(取值范围为[0, 1])值获取坐标,以及一个曲线上坐标集合的数组。另外还须要使用三阶贝塞尔公式:

B_{2}(t) = (1 - t)^3P_0 + 3t(1 - t)^2P_1 + 3t^2(1 - t)P_2 + t^3P_3 , t\in[0, 1]

由于P_0点坐标为[0, 0],P_1点坐标为[1, 1]为因此公式进而能够写成:

B_{3, x}(t) = 3t(1 - t)^2x_1 + 3t^2(1 - t)x_2 + t^3 , t\in[0, 1]
B_{3, y}(t) = 3t(1 - t)^2y_1 + 3t^2(1 - t)y_2 + t^3 , t\in[0, 1]
class CubicBezier {
  constructor(x1, y1, x2, y2) {
    this.controlPoints = [x1, y1, x2, y2];
  }

  getCoord(t) {
    // 若是t取值不在0到1之间,则终止操做
    if (t > 1 || t < 0) return;
    const _t = 1 - t;
    const [ x1, y1, x2, y2 ] = this.controlPoints;
    const coefficient1 = 3 * t * Math.pow(_t, 2);
    const coefficient2 = 3 * _t * Math.pow(t, 2);
    const coefficient3 = Math.pow(t, 3);
    const px = coefficient1 * x1 + coefficient2 * x2 + coefficient3;
    const py = coefficient1 * y1 + coefficient2 * y2 + coefficient3;
    // 结果只保留三位有效数字
    return [parseFloat(px.toFixed(3)), parseFloat(py.toFixed(3))];
  }
}
复制代码

利用上述的Bezier类,咱们就能够根据两个控制点构建Bezier实例,经过这个实例咱们能够根据t值,获取点上的近似值。

那么若是咱们想要根据x轴坐标值,来获取y轴坐标时,咱们该怎么作呢?

这里我使用了一个近似处理的办法,具体以下:

  1. 先获取离须要求值点最近的两个点。
  2. 而后经过这两个点能够获得一个直线方程。
  3. 最后经过将x轴坐标传入直线方程中,就能够近似求得y轴坐标值了。

因此咱们须要进一步改造Bezier构造函数,须要缓存固定数量坐标数组的属性coords,以及获取coords的方法getCoordsArray,最后还有获取y轴坐标的方法getY,具体的实现方法以下:

class CubicBezier {
  constructor(x1, y1, x2, y2) {
    const precision = 100;
    this.controlPoints = [x1, y1, x2, y2];
    this.coords = this.getCoordsArray(precision);
  }
  
  getCoord(t) {
    // 若是t取值不在0到1之间,则终止操做
    if (t > 1 || t < 0) return;
    const _t = 1 - t;
    const [ x1, y1, x2, y2 ] = this.controlPoints;
    const coefficient1 = 3 * t * Math.pow(_t, 2);
    const coefficient2 = 3 * _t * Math.pow(t, 2);
    const coefficient3 = Math.pow(t, 3);
    const px = coefficient1 * x1 + coefficient2 * x2 + coefficient3;
    const py = coefficient1 * y1 + coefficient2 * y2 + coefficient3;
    // 结果只保留三位有效数字
    return [parseFloat(px.toFixed(3)), parseFloat(py.toFixed(3))];
  }
  
  getCoordsArray(precision) {
    const step = 1 / (precision + 1);
    const result = [];
    for (let t = 0; t <= precision + 1; t++) {
      result.push(this.getCoord(t * step));
    }
    this.coords = result;
    return result;
  }
  
  getY(x) {
    if (x >= 1) return 1;
    if (x <= 0) return 0;
    let startX = 0;
    for (let i = 0; i < this.coords.length; i++) {
      if (this.coords[i][0] >= x) {
        startX = i;
        break;
      }
    }
    const axis1 = this.coords[startX];
    const axis2 = this.coords[startX - 1];
    const k = (axis2[1] - axis1[1]) / (axis2[0] - axis1[0]);
    const b = axis1[1] - k * axis1[0];
    // 结果也只保留三位有效数字
    return parseFloat((k * x + b).toFixed(3));
  }
}
复制代码

而后经过下述方式就可使用咱们的CubicBezier了:

const cubicBezier = new CubicBezier(0.3, 0.1, 0.3, 1);
cubicBezier.getY(0.1); // 0.072
cubicBezier.getY(0.7); // 0.931
复制代码

我写了一个应用这个CubicBezier构造函数的库Animate-Scroll,有兴趣的能够去看一下源码。

6、如何用高阶贝塞尔曲线表示低阶贝塞尔曲线?

一个n阶贝塞尔曲线能够经过一个形状彻底一致的n+1阶贝塞尔曲线表示。那咱们该怎么作,才能获取这个n+1阶贝塞尔曲线呢?

由高阶贝塞尔曲线表示低阶贝塞尔曲线的过程,咱们称之为升阶

咱们须要用到B(t)=(1-t)B(t)+tB(t)这个等式来作升阶。

  1. 先以二阶升三阶为例,二阶贝塞尔曲线坐标公式为:
B(t) = (1 - t)^2P_0 + 2t(1 - t)P_1 + t^2P_2

将如下等式带入上面这个公式中:

P_0=(1-t)P_0 + tP_0
P_1=(1-t)P_1 + tP_1
P_2=(1-t)P_2 + tP_2

而后得出如下公式:

B(t) = (1-t)^3P_0 + (1-t)^2tP_0 + 2t(1-t)^2P_1
+ 2t^2(1-t)P_1 + t^2(1-t)P_2 + t^3P_2
=(1-t)^3P_0 + 3(1-t)^2t\frac{P_0+2P_1}{3} + 3(1-t)t^2\frac{2P_1+P_2}{3} + t^3P_2

根据以上结果能够得出控制点由以前的P_0,P_1,P_2变成了P_0\frac{P_0+2P_1}{3}\frac{2P_1+P_2}{3}P_2四个控制点了,从而完成了升阶。

  1. 若是对于任意的n值,咱们该如何进行升阶呢?(如下为推导过程,没兴趣的同窗能够直接跳转至下面👇的公式)

这里须要进行一些推导(这里的推导须要用到C_n^{i}公式,有兴趣的同窗能够本身推导一下),由于:

(1-t)b_{i,n}=\frac{n+1-i}{n+1}b_{i,n+1}
tb_{i,n}=\frac{i+1}{n+1}b_{i+1,n+1}

贝塞尔公式能够表示为:

B(t) = (1-t)\sum_{i=0}^{n}b_{i,n}(t)P_i+t\sum_{i=0}^{n}b_{i,n}(t)P_{i}

带入上述两个等式,得:

B(t) = \sum_{i=0}^{n}\frac{n+1-i}{n+1}b_{i,n+1}(t)P_i+\sum_{i=0}^{n}\frac{i+1}{n+1}b_{i+1,n+1}P_i \quad--\ (0)

由于当i=n+1时:

\frac{i}{n+1}P_{i-1}=0

因此该式能够写成:

\sum_{i=0}^{n}\frac{n+1-i}{n+1}P_i = \sum_{i=0}^{n+1}\frac{n+1-i}{n+1}P_i \quad--\ (1)

又由于:

\sum_{i=0}^{n}\frac{i+1}{n+1}P_{i} = \sum_{i=1}^{n+1}\frac{i}{n+1}P_{i-1}

i=0时:

\frac{i}{n+1}P_{i-1} = 0

因此:

\sum_{i=0}^{n}\frac{i+1}{n+1}P_i=\sum_{i=0}^{n+1}\frac{i}{n+1}P_{i-1} \quad--\ (2)

将上述两个等式(1)和(2)代入公式(0)中,最终能够得出下面这个升阶公式:

B(t) = \sum_{i=0}^{n+1}(\frac{i}{n+1}P_{i-1} + \frac{n+1-i}{n+1}P_i)b_{i,n+1}(t)
B(t) = \sum_{i=0}^{n+1}(P_{i}^{'})b_{i,n+1}(t)
式中\ P_{i}^{'} = \frac{i}{n+1}P_{i-1} + \frac{n+1-i}{n+1}P_i,其中i=0,1,...n+1

关于贝塞尔曲线基本的内容就差很少讲完了,若是您发现不正确或者有补充的地方,欢迎在评论里指出😊。

参考文献

相关文章
相关标签/搜索