深刻淺出貝塞爾曲線及應用示例

前言

貝塞爾曲線是計算機圖形學(Computer Graphics)中至關重要的參數曲線,在前端領域中,尤爲是可視化項目中扮演者舉足輕重的做用,好比:css

  1. CSS動畫中使用cubic-bezier緩動函數
  2. 使用貝塞爾曲線擬合折線圖中的點位,使折線圖看起來更圓滑、美觀。
  3. 鋼筆工具

今天咱們從貝塞爾曲線的定義出發,再到貝塞爾曲線的應用場景中,爲你們深刻的介紹貝塞爾曲線是怎樣一回事。html

貝塞爾曲線的定義

遞歸定義

一次貝塞爾曲線

首先咱們先來看一下一次貝塞爾曲線前端

1-bezier.gif

圖中,這個黃色的小球的運動軌跡即爲貝塞爾曲線。算法

從上圖中咱們能夠看到:一次貝塞爾曲線有2個控制點,設黃色小球的位置由一參數 t [ 0 , 1 ] t\in[0,1] 肯定。canvas

image.png

因此:markdown

P = ( 1 t ) P 1 + t P 2 P = (1-t)P_1 + tP_2

app

P = P 1 + t ( P 2 P 1 ) P = P_1 + t(P_2 - P_1)

咱們能夠看出,這就是一個線性插值的公式。ide

二次貝塞爾曲線

那麼咱們再來看一下二次貝塞爾曲線svg

2-bezier.gif

從上圖中咱們能夠看到,二次貝塞爾曲線擁有3個控制點。圖中綠色小圓點的運動軌跡即爲二次貝塞爾曲線。產生該運動軌跡的步驟以下:函數

  1. 依次連線3個控制點,由此產生兩條直線(上圖中灰色的兩條直線)
  2. 根據當前參數t,肯定上述兩條直線中,經過線性插值獲得的新的兩個點的位置(上圖中橙色的兩個點)
  3. 將上述兩個點鏈接起來,再次根據參數t,經過線性插值獲得最後的一個點。
  4. 最後這個一個點的運動軌跡即爲貝塞爾曲線

到這裏,相信讀者已經對貝塞爾曲線有了一個大概的認識,那咱們再看一下三次貝塞爾曲線

三次貝塞爾曲線

3-bezier.gif

與二次貝塞爾曲線相似,咱們也能夠經過遞歸的方式,不斷的根據當前參數 t [ 0 , 1 ] t\in[0,1] 進行線性插值來獲得新的點,最後獲得的一個點運動軌跡則爲貝塞爾曲線。

貝塞爾曲線的數學表達式

在上面,咱們已經給出了一次貝塞爾曲線(線性插值)的數學表達式: P = ( 1 t ) P 1 + t P 2 P = (1-t)P_1 + tP_2

那麼,對於二次貝塞爾曲線、三次貝塞爾曲線甚至N次貝塞爾曲線的數學表達式又如何呢?

二次貝塞爾曲線的數學表達式推導

image.png

由上圖,P點的位置是咱們最後要求的點位。點 P P 是由 P 1 , P 2 P_1', P_2' 和參數 t [ 0 , 1 ] t\in[0,1] 共同決定的。因此:

P = ( 1 t ) P 1 + t P 2 (a) P = (1-t)P_1' + tP_2' \tag{a}

P 1 , P 2 P_1', P_2' 又是由 P 1 , P 2 , P 3 P_1, P_2, P_3 決定,因此又有:

P 1 = ( 1 t ) P 1 + t P 2 (b) P_1' = (1-t)P_1 + tP_2 \tag{b}
P 2 = ( 1 t ) P 2 + t P 3 (c) P_2' = (1-t)P_2 + tP_3 \tag{c}

將b,c 兩式子代入到a中。能夠獲得:

P = ( 1 t ) 2 P 1 + 2 ( 1 t ) t P 2 + t 2 P 3 (d) P = (1-t)^2P_1 + 2(1-t)tP_2 + t^2P_3 \tag{d}

d式即爲二次貝塞爾曲線的數學表達式。

任意次數貝塞爾曲線的數學表達式

與上述的二次貝塞爾曲線的推導過程相似,咱們同理能夠推導出三次貝塞爾曲線、四次貝塞爾曲線的數學表達式,咱們將一次貝塞爾曲線到四次貝塞爾曲線的表達式一塊兒書寫出來,咱們來觀察他們的係數有什麼聯繫?

一次貝塞爾曲線:

P = ( 1 t ) P 1 + t P 2 P = (1-t)P_1 + tP_2

二次貝塞爾曲線:

P = ( 1 t ) 2 P 1 + 2 ( 1 t ) t P 2 + t 2 P 3 P = (1-t)^2P_1 + 2(1-t)tP_2 + t^2P_3

三次貝塞爾曲線:

P = ( 1 t ) 3 P 1 + 3 ( 1 t ) 2 t P 2 + 3 ( 1 t ) t 2 P 3 + t 3 P 4 P = (1-t)^3P_1 +3(1-t)^2tP_2 + 3(1-t)t^2P_3 + t^3P_4

四次貝塞爾曲線:

P = ( 1 t ) 4 P 1 + 4 ( 1 t ) 3 t P 2 + 6 ( 1 t ) 2 t 2 P 3 + 4 ( 1 t ) t 3 P 4 + t 4 P 5 P = (1-t)^4P_1 +4(1-t)^3tP_2 + 6(1-t)^2t^2P_3 + 4(1-t)t^3P_4 + t^4P_5

tips: 將他們的係數按行排列起來再觀察, 再思考一會吧

細心的讀者應該也發現規律了,咱們將他們的係數依次寫出來:

image.png

這個三角形很熟悉有沒有?這就是著名的楊輝三角!

楊輝三角,是二項式係數在三角形中的一種幾何排列,中國南宋數學家楊輝1261年所著的《詳解九章算法》一書中出現。在歐洲,帕斯卡(1623----1662)在1654年發現這一規律,因此這個表又叫作帕斯卡三角形。帕斯卡的發現比楊輝要遲393年,比賈憲遲600年

楊輝三角有一個很重要的性質: 第n行的第m個數能夠表示爲:

C n 1 m 1 = ( n 1 ) ! ( m 1 ) ! C_{n-1}^{m-1} = \frac{(n-1)!}{(m-1)!}

即爲從n-1個不一樣元素中取m-1個元素的組合數。

那麼,咱們如今獲得了他們的係數的分佈規律,咱們能夠輕易的寫出N介貝塞爾曲線的數學表達式:

P = i = 0 n C n i ( 1 t ) n i t i P i P = \sum_{i=0}^{n}C_n^i(1-t)^{n-i}t^iP_i

上述就是關於貝塞爾曲線的定義的介紹了,接下來咱們從一些實際的應用問題出發,但願讀者對貝塞爾曲線有一個更深刻的理解。

貝塞爾曲線的應用

1、CSS動畫中的緩動函數

咱們在網頁中製做一些動畫或者是過渡效果時一般會用到貝塞爾曲線做緩動函數,例如:

.transition {
    transition: left 2s cubic-bezier(0.5, 0.15, 0.5, 0.9) ;
}
複製代碼

這裏就是一個經典的將貝塞爾曲線用做緩動函數。緩動函數的做用爲將一個線性變化的參數t,經過一系列的運算映射爲另外一個值t',好比0.5經過上述的緩動函數映射後的值是0.51875

咱們能夠在cubic-bezier.com/ 這個網站上查看上述參數造成的貝塞爾曲線。

image.png

咱們在 cubic-bezier 中填入的4個數字,即爲上圖中P二、P3點的x,y座標。即P2 = (0.5, 0.15) P3 = (0.5, 0.9)。

咱們就以一個元素使用上述緩動函數,在2s內從left=0的位置移動到left=200的位置這樣的一個動畫來進行說明。具體的計算流程以下:

image.png

對於上圖中緩動函數求值這一步,咱們經過已逝去時間/總時間算得 progress,這裏progress表明的是貝塞爾曲線中x的值。

因爲咱們沒有創建x與y之間的映射關係,那麼咱們如何根據x的值,計算貝塞爾曲線的y值呢?

值得慶幸的是,咱們擁有貝塞爾曲線的參數方程,咱們有函數 P ( t ) = x P(t) = x ,若是咱們可以經過x反計算出t的值,再使用t的值代入參數方程中便可獲得y的值了。

因此,如今問題轉化爲了:如何求解方程 P ( t ) = x P(t) = x

首先咱們思考這樣的一道題:
LeetCode-367.有效的徹底平方數

給定一個 正整數 num ,編寫一個函數,若是 num 是一個徹底平方數,則返回 true ,不然返回 false 。

簡單的講就是說在不使用Math.sqrt的狀況下,如何對一個數字進行開平方?

這裏給你們介紹一種很實用的經過迭代的方式來解方程的方法: 牛頓法 (Newton's method)

牛頓法

牛頓法是一種用逼近的思想來求解方程根的方法。此處假設咱們須要求解 x 2 = 4 x^2 = 4 ,其實就是求解方程 x 2 4 = 0 x^2 - 4 = 0 的根,即求函數圖像與x軸的交點。

image.png

求解的流程大體以下:

  1. 選取一個初始值 x n x_n
  2. 在函數圖像 ( x n , f ( x n ) ) (x_n, f(x_n)) 處做函數的切線,該切線與x軸相交與 x n + 1 x_{n+1}
  3. 判斷 f ( x n + 1 ) f(x_{n+1}) f ( x n ) f(x_n) 的值之間的差是否小於某一精度(精度值自行設定),若是達到精度則結束迭代,反之則重複步驟2.

f ( x n + 1 ) f(x_{n+1}) 的求解方法以下:

x n + 1 = x n f ( x n ) f ( x n ) x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)}

以上,就是關於牛頓法的內容。

有了牛頓法這樣的一個求解方程的利器,咱們如今能夠解決上述的問題了: 求解方程 P ( t ) = x P(t) = x ,該問題等價於求解方程: P ( t ) x = 0 P(t) - x = 0

咱們能夠寫出一個函數來實現這個方法:

function solveCurveX(x: number) {
    var t2 = x;
    var derivative;
    var x2;
    // 此處的8次是牛頓迭代的最大次數,能夠自行設定,防止函數不收斂陷入死循環
    for (let i = 0; i < 8; i++) {
        x2 = fn(t2) - x;
        if (Math.abs(x2) < ZERO_LIMIT) {
            return t2;
        }
        derivative = fnDerivativeX(t2);
        if (Math.abs(derivative) < ZERO_LIMIT) {
            break;
        }
        t2 -= x2 / derivative;
    }
    return t2;
複製代碼

這裏須要注意上述代碼的for循環部分,設定了循環次數爲8.這是爲了設定最大的迭代次數,由於牛頓法並非對於全部的方程都適用,首先要確保方程有根,而且在迭代區間內是單調的並且收斂的。若是不知足上面的條件,使用牛頓法會使程序陷入死循環中。

咱們如今經過solveCurveX這個函數獲得了x對應的t值,再將t值代入貝塞爾曲線的方程中,便可獲得最終的y值。

2、貝塞爾曲線重參數化

接着,咱們進入第二個應用場景。思考這樣一個問題,我使用貝塞爾曲線定義一條路徑L,如今我想讓某一個物體沿着貝塞爾曲線的路徑勻速運動。我該如何實現?

可能有的讀者已經想到一個方法了:

我能夠將參數 t [ 0 , 1 ] t\in[0,1] 平均的分紅若干份,再將其代入貝塞爾曲線方程中以獲得一系列的座標,我再給這個物體設置剛剛計算出的座標就行了。

這樣作真的能夠嗎? 答案是否認的。咱們看這樣一條貝塞爾曲線,我將參數 t [ 0 , 1 ] t\in[0,1] 平均的分紅25分,則曲線上的25個點爲以下: image.png

咱們能夠看到,上圖中,在曲線彎曲的厲害的地方,點位明顯比較密集,反之在曲線彎曲的不厲害的地方地位則比較稀疏。這樣勢必會致使一個問題:在曲線彎曲的厲害的地方物體運動的慢,在彎曲的不厲害的地方物體運動的快。這並非咱們想要獲得的勻速運動的效果。

若是咱們要獲得勻速運動的效果該怎麼作呢? 可能細心的讀者已經發現了,咱們要作的不是將參數t進行均分,而是應該將貝塞爾曲線的總長度進行均分纔對。對曲線長度均分後的效果以下: image.png

而後,咱們還但願可以經過任意的曲線長度,找到所對應的參數t,再根據反算出的參數t,代入貝塞爾曲線方程中獲得最終的座標。

小結一下大體的步驟:

  1. 求解曲線總長度L
  2. 對曲線總長度L進行均分
  3. 求解任意曲線長度l所對應的參數t
  4. 根據參數t代入貝塞爾曲線方程獲得最終座標

如今解決第一個問題:如何求解曲線的總長度L。

最容易讓人想到的一個辦法就是將貝塞爾曲線分割成若干條直線,而後將這些直線的長度相加便可。可是隨着分割的精度提升,運算量也是很大的。

可是這是一個很好的思想,微積分正是如此。因此,咱們是否可使用微積分的方法來求解曲線的長度呢?答案是確定的。 image.png

咱們用一小段的直線近似表示曲線,這一小段直線咱們用 d s ds 表示。那麼整段曲線的長度則是在整個曲線上進行積分。以下:

S = L d s S = \int_{L}ds

對於 d s ds 能夠用 d x 2 + d y 2 \sqrt{dx^2 + dy^2} 來表示。因爲咱們是使用參數方程,由

d x d t = ϕ ( t )     d x = ϕ ( t ) d t \frac{dx}{dt} = \phi'(t) \\\ \\\ dx = \phi'(t)dt
d y d t = ψ ( t )     d y = ψ ( t ) d t \frac{dy}{dt} = \psi'(t) \\\ \\\ dy = \psi'(t)dt

能夠獲得

d s = d x 2 + d y 2 = ϕ 2 ( t ) + ψ 2 ( t ) d t ds = \sqrt{dx^2 + dy^2} = \sqrt{\phi'^2(t) + \psi'^2(t)}dt

因爲 ϕ ( t ) \phi(t) ψ ( t ) \psi(t) 的函數表達式相同,而且上述式子表示的就是兩點之間的距離。因此

ϕ 2 ( t ) + ψ 2 ( t ) d t = P ( t ) \sqrt{\phi'^2(t) + \psi'^2(t)}dt = ||P'(t)||

曲線總長度則爲

S = 0 1 P ( t ) d t S = \int_0^1||P'(t)||dt

對於任意參數 t [ 0 , 1 ] t\in[0,1] 對應的曲線長度爲:

L ( t ) = 0 t P ( x ) d x L(t) = \int_0^t||P'(x)||dx

如今另外一個問題呼之欲出了,如今咱們有了如何計算曲線長度的積分表達式,可是咱們如何計算積分呢?

這裏介紹一種快速計算一重積分的方法:高斯-勒讓德(Gauss-Lengendre)求積法

高斯-勒讓德(Gauss-Lengendre)求積法

其中具體的原理就不過多介紹了,簡單的來說,就是查表。只能感嘆一句高斯是神。

公式以下:

a b f ( x ) d x = b a 2 i = 1 n w i f ( b a 2 x i + b + a 2 ) \int _ { a } ^ { b } f ( x ) dx = \frac{b - a}{2} \sum_{i=1}^{n}w_i f(\frac{b-a}{2}x_i + \frac{b + a}{2})

image.png

至此,咱們已經能夠根據貝塞爾曲線的路徑積分和高斯-勒讓德求積法求的貝塞爾曲線的總長度和任意t時的長度了。接下來的問題來到了,如何根據任意長度L,求所對應的t?

這個問題是否是讓人感受不多熟悉?沒錯,就是使用牛頓法進行求解。

t = a L ( a ) L ( a ) t = a - \frac{L(a)}{L'(a)}

其中

L ( a ) = P ( a ) L'(a) = ||P'(a)||

可是牛頓法也並非萬能的,高次方程的解並非惟一的,例如這裏的3次方程,可能有1個解,2個解,3個解。咱們使用牛頓法求解的根可能並非咱們想要的那一個根。因此按上述方法對貝塞爾曲線曲線進行重參數化可能會出現下面的這種狀況:

image.png

如上圖所示,圖中的藍色點位是對曲線長度進行均分後,使用牛頓法求得對應的t值,所對應的點位。咱們能夠看出來這明顯是有必定問題的,緣由就是因爲方程有多個根,因爲初始值選擇的問題,沒有求得咱們想要的那一個根。以下圖所示:

image.png

如上圖所示,由於初始點選擇的問題,致使在迭代過程當中一些中間狀態的點發生了「跳躍」的現象,從而找到了更遠處的根,但這不是咱們想要的結果。那麼如何避免出現這一情況呢?這裏咱們退而求其次,使用二分法求根。

二分法求根

二分法始終都會找到離初始值最近的根,因此不會出現相似於牛頓法的「跳躍」現象。可是二分法所以也要付出執行效率不如牛頓法的代價。二分法做爲一種經典算法,在此就不過多的贅述了。

咱們改成使用二分法進行方程求解後,能夠獲得正確的效果,以下圖所示:

image.png

如今咱們已經完成了貝塞爾曲線的重參數化。

接着,讓咱們進入下一個應用場景:

3、貝塞爾曲線分割

使用過Photoshop中鋼筆工具的朋友們應該知道:在一條已有的路徑中,咱們能夠隨意的往其中插入控制點。這樣的過程就是分割貝塞爾曲線的過程,咱們將一條貝塞爾曲線一分爲二,而且還要保證分割後的兩條曲線的鏈接部分是平滑連續的。那麼分割後的兩條貝塞爾曲線的控制點應該是處於什麼位置的呢?

image.png 如上圖所示,有一條點A、B、C、D爲控制點造成的貝塞爾曲線,咱們如今要在E點處對該條曲線進行分割,那麼分割後的兩條曲線的控制點分別應該是哪幾個點呢?

咱們經過觀察上圖,直覺告訴我:左邊的貝塞爾曲線的控制點看起來像是:A、F、I、E,右邊的貝塞爾曲線的控制點像是:E、J、H、D。那麼,其實是這樣的嘛??

假設A、F、I、E就是分割後的左邊貝塞爾曲線的控制點。

根據貝塞爾曲線的定義,由A、B、C、D組成的貝塞爾曲線的公式能夠寫爲:

E = ( 1 t ) 3 A + 3 ( 1 t ) 2 t B + 3 ( 1 t ) t 2 C + t 3 D t [ 0 , 1 ] E = (1-t)^3A + 3(1-t)^2tB + 3(1-t)t^2C + t^3D\quad t\in[0,1]

那麼左邊的貝塞爾曲線A、F、I、E,能夠表示爲:

E l = ( 1 e ) 3 A + 3 ( 1 e ) 2 t F + 3 ( 1 e ) e 2 I + e 3 E e [ 0 , 1 ] (a) E_l = (1-e)^3A + 3(1-e)^2tF + 3(1-e)e^2I + e^3E\quad e\in[0,1] \tag{a}

上式中,F、I能夠寫爲:

F = ( 1 t ) A + t B (b) F = (1 - t)A+ tB \tag{b}
I = ( 1 t ) F + t G = ( 1 t ) 2 A + 2 ( 1 t ) t B + t 2 C (c) I= (1 - t)F+ tG = (1-t)^2A+2(1-t)tB+t^2C \tag{c}

將式(b), (c)代入式子(a)中,化簡可得:

E l = ( 1 e t ) 3 A + 3 ( 1 e t ) 2 t B + 3 ( 1 e t ) ( e t ) 2 C + ( e t ) 3 D e [ 0 , 1 ] , e t [ 0 , t ] E_l = (1-et)^3A + 3(1-et)^2tB + 3(1-et)(et)^2C + (et)^3D\quad e\in[0,1], et\in[0,t]

u = e t u = et ,則 u [ 0 , t ] u\in[0,t]

E l = ( 1 u ) 3 A + 3 ( 1 u ) 2 t B + 3 ( 1 u ) u 2 C + u 3 D u [ 0 , t ] (d) E_l = (1-u)^3A + 3(1-u)^2tB + 3(1-u)u^2C + u^3D\quad u\in[0,t] \tag{d}

咱們能夠看出式(a)與式(d)的函數方程徹底相同,而且定義域也徹底相同。即A、F、I、E就是分割後左邊貝塞爾曲線的控制點。同理的,對於右側的貝塞爾曲線,咱們也可以證得E、J、H、D即爲右側貝塞爾曲線新的控制點。

接下來,咱們進入最後一個應用場景:使用貝塞爾曲線擬合一系列的點。

4、貝塞爾曲線擬合折線

繪製折線圖表是一個很是常見的需求,咱們可使用canvas或者SVG進行折線圖的繪製。咱們能夠簡單的將全部點簡單的首尾相連便可,可是這樣的折線圖,未免也太生硬了叭!以下圖:

image.png

若是你用過相似於Echarts的圖表庫,那麼你會發現其中的折線圖是這樣的:

image.png

這樣看起來的就柔和多了。這背後使用到的技術正是貝塞爾曲線。它將一條直線用一條貝塞爾曲線進行替換,以達到在轉角處平滑過渡的效果。

image.png

如上圖所示,咱們須要擬合A、B、I、J這一段折線。這裏有一個簡單的公式:

P i l = P i ( P i + 1 P i 1 ) e   P i r = P i + ( P i 1 P i + 1 ) e P_{il} = P_i - (P_{i+1} - P_{i-1})e \\\ P_{ir} = P_i + (P_{i-1} - P_{i+1})e

其中,e是用於控制曲線圓滑程度的參數, e [ 0 , 1 ] e\in[0,1]

對於起始點有: P i + 1 = P i P_{i+1} = P_{i} ,終止點有: P i 1 = P i P_{i-1} = P_{i}

根據上述公式,咱們實現的效果以下:

image.png

能夠看出,整體效果仍是很是不錯的。

以上就是本文中關於貝塞爾曲線的全部應用示例了。

總結

大體的總結一下,本文主要講述瞭如下幾個方面:

  1. 介紹了貝塞爾曲線的定義(遞歸定義及數學定義)
  2. 在數學定義中,揭示了貝塞爾曲線的的解析式中各項係數的分佈規律(楊輝三角、二項式分佈、組合數)
  3. 介紹了CSS中貝塞爾曲線用做緩動函數其背後的計算邏輯
  4. 介紹了貝塞爾曲線如何進行重參數化
  5. 介紹了牛頓法解方程以及它的一些問題(用二分法來規避,但運算速度會降低)
  6. 介紹瞭如何使用貝塞爾曲線使折線圖變得更圓滑

但願讀者經過閱讀本文對貝塞爾曲線有更深入的認識,各位能夠經過編碼的方式自我實現一下本文中提到了一些算法,對自個人編碼能力和數學能力都有不小的提升。

若是你以爲本文對你有用,還請點個贊👍哦~

相關文章
相關標籤/搜索