包教包會-貝塞爾曲線的繪製原理與應用

xietao3發表於2017-06-23

說來話長,這一切都得從PhotoShop中的鋼筆工具開始說起...

宣告:本文不含複雜數學公式,學渣放心閱讀吧?(我彷彿看到了學渣們留下了激動的淚水)

背景

貝塞爾曲線(Bézier curve)是應用於二維圖形應用程式的數學曲線,貝塞爾曲線基於多個點構成。它的應用非常廣泛,比如說PS中的鋼筆工具所繪畫的曲線就是貝塞爾曲線,繪製動畫的運動軌跡等等,而最近一次想用到貝塞爾曲線是想做一個 路徑動畫

簡介

在iOS開發中一般通過UIBezierPath來實現貝塞爾曲線的繪製,平時一般使用繪製二階和三階貝塞爾曲線的方法。而我們要做的遠超二三階的貝塞爾曲線,本文 iOS Demo在原理上實現了N階貝塞爾曲線的繪製,未使用任何相關API,純手動繪製貝塞爾曲線,並且可以拖動滑塊瀏覽貝塞爾曲線的繪製過程。

本文 iOS Demo 實現以下功能:

實現功能 描述
繪製貝塞爾曲線 1、點選空白處設定貝塞爾曲線的點
2、可以設定貝塞爾曲線階數
3、播放貝塞爾曲線繪製過程
4、拖動滑塊,自由檢視繪製過程每一個瞬間
簡易曲線圖表 每兩個點之間都是用3階貝塞爾曲線連線(細節待完善)
雲霄飛車 1、在空白處繪製貝塞爾曲線
2、雲霄飛車沿著繪製的貝塞爾曲線行駛
3、支援多個連線的貝塞爾曲線路徑

Demo示例圖

8階貝塞爾曲線繪製過程
8階貝塞爾曲線繪製過程

貝塞爾曲線的繪製原理

說到繪製原理,如果貼?這張圖,我只能說:什麼鬼!!!我看不懂,聽不見,你說什麼...
路人甲:簡單點...說話的方式簡單點~

失敗案例
失敗案例

首先提供一個可以動態繪製貝塞爾曲線的網站幫助你更好地理解貝塞爾曲線的繪製。

1. 點

貝塞爾曲線點的數量決定了曲線的階數,一般N個點構成的N-1階貝塞爾曲線,即3個點為二階,至少由3個點組成,為什麼兩個點不行,兩個點組成的是直線。按順序,第一個點為 起點 ,最後一個點為 終點 ,其餘點都為 控制點

A起點、B控制點 、C終點以及繪製的貝塞爾曲線
A起點、B控制點 、C終點以及繪製的貝塞爾曲線

2. 點生線

這裡說的線不是貝塞爾曲線,而是各個點按順序連線起來,形成的直線,如上圖ABBC兩條線。在這裡我們要將整個曲線的繪製量化為從0~1的過程,用progress為當前過程的進度,progress的區間即0~1。每一條線都需要根據progress生成一個點,如下圖,一個點從P0移動到P1,這是這條線從0~1的過程。

根據進度點從起點向終點移動
根據進度點從起點向終點移動

下面是繪製一個二階貝塞爾曲線過程,先給口訣: 點生線,線生點 ?。由ABC這3個點組成2條線ABBC,2條線根據progress分別生成2個移動的點DE,而DE又連成一條線,始終保持AD:DB=BE:EC

progress為0.3 的連線
progress為0.3 的連線

移動的線
移動的線

DE,DE再根據progress生成點F,只剩一個點,無法構成線,即為最終構成貝塞爾曲線的點。紅色點為progress0~1過程中點F的移動過程,保持AD:DB=BE:EC=DF:FE

progress為0.3 最終的點
progress為0.3 最終的點

點F的移動過程
點F的移動過程

3. 繪製貝塞爾曲線

經過上面 點生線,線生點 的過程 ,我們拿到了點F在移動中所有點的,將這些點集合連線起來,即形成了貝塞爾曲線。progress自增越慢,點集合的點越多,曲線就越細緻。

繪製二階貝塞爾曲線過程
繪製二階貝塞爾曲線過程

4. N階貝塞爾曲線

稍微瞭解演算法的同學就能發現,其實 點生線,線生點 是一個遞迴的過程,通過底層的點,一步步推算出最高階的點。整個推導過程像一個金字塔,底部點的數量最多,每高一階點的數量就減1,直至最高階只有1個點。

下面是遞迴程式碼:

// 貝塞爾曲線每高一階  需要遞迴次數+1
+ (NSArray *)recursionGetsubLevelPointsWithSuperPoints:(NSArray *)points progress:(CGFloat)progress{
    // 得到最終的點 正確結束遞迴 
    if (points.count == 1) return points;

    NSMutableArray *tempArr = [[NSMutableArray alloc] init];
    for (int i = 0; i < points.count-1; i++) {
        // 第一個點 
        NSValue *preValue = [points objectAtIndex:i];
        CGPoint prePoint = preValue.CGPointValue;
        // 第二個點
        NSValue *lastValue = [points objectAtIndex:i+1];
        CGPoint lastPoint = lastValue.CGPointValue;

        // 兩點座標差
        CGFloat diffX = lastPoint.x-prePoint.x;
        CGFloat diffY = lastPoint.y-prePoint.y;

        // 根據當前progress得出高一階的點
        CGPoint currentPoint = CGPointMake(prePoint.x+diffX*progress, prePoint.y+diffY*progress);
        [tempArr addObject:[NSValue valueWithCGPoint:currentPoint]];
    }
    // 繼續下一次遞迴過程
    return [self recursionGetsubLevelPointsWithSuperPoints:tempArr progress:progress];
}複製程式碼

8階貝塞爾曲線繪製過程:

8階貝塞爾曲線繪製過程
8階貝塞爾曲線繪製過程

貝塞爾曲線的應用

光講原理脫離實踐這不是程式設計師的風格,簡單地寫了2個貝塞爾曲線的應用,都在本文 iOS Demo 裡面,歡迎執行體驗。

1. 雲霄飛車

通過點選螢幕收集點,將點集合生成貝塞爾曲線,可生成多個相連的貝塞爾曲線。小車按照生成的貝塞爾曲線路徑前進。

a. 畫路徑
通過計算貝塞爾曲線的長度,根據曲線長度分配點的數量,達到點的相對均勻分佈,使雲霄飛車 勻速前進

畫路徑
畫路徑

b. 發車
每個點都與前面一個點連線,通過計算得出兩點的連線與水平形成的夾角,將角度賦予雲霄飛車實現 轉向功能

發車
發車

2. 簡易曲線圖表

a. 直線圖表
即最簡單的兩點連成直線。

直線圖表
直線圖表

b. 曲線圖表
曲線圖表的曲線全部由3階貝塞爾曲線構成,整個曲線圖不含任何稜角。

曲線圖表
曲線圖表

擴充

PaintCode
PaintCode

推薦一個iOS畫路徑神器PaintCode,畫好圖形直接生成程式碼,用鋼筆工具畫貝塞爾曲線也十分方便。下圖為用鋼筆工具畫一個圓球(貌似不夠圓?):

生成程式碼
生成程式碼

總結

為了準備這一篇文章差不多理解了貝塞爾曲線的繪製原理,但是在細節處,比如說真正意義上貝塞爾曲線點的均勻分佈還有待完善,求曲線公式也沒有去研究,貝塞爾曲線在複雜的動畫方向地應用也是大有作為。

參考

貝塞爾曲線開發的藝術
Android:貝塞爾曲線原理分析

個人水平有限,歡迎提出建議。

相關文章