深刻理解貝塞爾曲線

貝塞爾曲線(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數組

三階貝塞爾曲線

那咱們經過控制點是怎麼繪製出貝塞爾曲線的呢?緩存

經過上圖的三階貝塞爾曲線舉例,基本的步驟以下:markdown

  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

關於貝塞爾曲線基本的內容就差很少講完了,若是您發現不正確或者有補充的地方,歡迎在評論裏指出😊。

參考文獻

相關文章
相關標籤/搜索