背景:
? ? ? ?給一系列頂點,如果只是用直線將其中的各個點依次連接起來,最終形成一個折線圖,這種很容易實現。但是現實中事物的變化往往具有連續的特性,即使是給定了一系列離散的點,基于以往的生活經驗,人們也更愿意接受那種曲線連接的趨勢圖。可是在程序中繪制直線很容易,要是繪制曲線將各個頂點連接起來,這又要如何實現呢?一種很直觀的思路就是將連接各點的直線替換成平滑的曲線,只要各段曲線在頂點處是平滑的過度,那么對應的曲線圖就是所需的了。因此問題變成了尋找一種容易實現的曲線來連接各個頂點。
工具--貝塞爾曲線:
? ? ? ?計算機圖形學中有一類很常用的曲線,俗稱貝塞爾曲線。1962年,法國數學家Pierre Bézier第一個研究了這種矢量繪制曲線的方法,并給出了詳細的計算公式,因此按照這樣的公式繪制出來的曲線就用他的姓氏來命名是為貝塞爾曲線。很多程序語言都有實現貝塞爾曲線的API,而該曲線本身也擁有強大的近似其它曲線的能力,即使一條不能夠勝任,那么分段的多條貝塞爾曲線也足夠用來近似我們想繪制的曲線。
貝塞爾曲線數學表示:
? 一階貝塞爾曲線:給定點P0、P1,一階貝塞爾曲線只是一條兩點之間的直線。這條線由下式給出: ? ? ? ? ? ? ? ??
其中P0和P1為兩個端點,P對應于貝塞爾曲線上的點,隨著t在[0,1]中變化,P點的集合構成一條連接P0與P1的線段。
二階貝塞爾曲線:當引入一個控制點P1的時候,就可以生成二階貝塞爾曲線,它是一個由二次函數描述的曲線,最多有一個頂點。
如下圖所示,P點的集合構成一個拋物線
這里解釋下上圖的綠線是如何產生的:
首先,我們已知端點P0、P2以及控制點P1,那么如何確定確定當t取某個固定值時位于貝塞爾曲線上的點P?一種簡單的方式可以通過貝塞爾曲線的公式,算出P的x和y坐標。但如何通過幾何畫法來計算出來呢?
根據貝塞爾曲線的定義,首先P0A/P0P1 = t,P1B/P1P2 = t,這樣我們可以分別確定點A與點B。然后連接AB,取AP/AB = t,那么P點就是貝塞爾曲線上的點了。
三階貝塞爾曲線:
三階貝塞爾曲線可以用一個三次函數描述,最多擁有兩個拐點。用來做兩點之間的曲線連接已經夠用了。我們來看下它的直觀形式:
一般參數公式:
給定點P0、P1、…、Pn,其貝塞爾曲線即:
公式說明
1.開始于P0并結束于Pn的曲線,即所謂的端點插值法屬性。
2.曲線是直線的充分必要條件是所有的控制點都位在曲線上。同樣的,貝塞爾曲線是直線的充分必要條件是控制點共線。
3.曲線的起始點(結束點)相切于貝塞爾多邊形的第一節(最后一節)。
4.一條曲線可在任意點切割成兩條或任意多條子曲線,每一條子曲線仍是貝塞爾曲線。
5.一些看似簡單的曲線(如圓)無法以貝塞爾曲線精確的描述,或分段成貝塞爾曲線(雖然當每個內部控制點對單位圓上的外部控制點水平或垂直的的距離為時,分成四段的貝茲曲線,可以小于千分之一的最大半徑誤差近似于圓)。
6.位于固定偏移量的曲線(來自給定的貝塞爾曲線),又稱作偏移曲線(假平行于原來的曲線,如兩條鐵軌之間的偏移)無法以貝茲曲線精確的形成(某些瑣屑實例除外)。無論如何,現存的啟發法通常可為實際用途中給出近似值
已知P0、P1...PN如何確定貝塞爾曲線上的點呢?
如上圖所示,存在頂點00,05;控制點01,02,03,04;實際上這是一條5階貝塞爾曲線。
? ? ? ?首先我們將點00到05連接起來,這樣它會有5條邊,這些邊用棕色表示。
? ? ? ?針對邊00-01,我們取一個點10,使得該點將邊00-01分成比例為t和1-t的兩部分。針對每條邊我們都取一個這樣的點。然后將這一系列點再次連接起來,這次會有4條邊,在上圖用墨綠色表示。注意到我們這樣操作之后,邊會比前面少一條。
? ? ? ?重復上面的操作,直到只有一條邊。在上圖中用綠色表示,我們取到點50,該點將這條邊分成t與(1-t)的兩部分。點50就是最終在貝塞爾曲線上的點。上述操作可以用如下公式表示:
可以查看原文更詳細的解釋
回到我們最初的問題
? ? ? ? 我們已經了解關于貝塞爾曲線的公式以及幾何畫法,但是要如何來解決我們用曲線來連接各個頂點的問題呢?
? ? ? ?前面已經提到,對于兩個點之間我們可以使用三階貝塞爾曲線來連接,這樣通過多段貝塞爾曲線相連,就可以得到我們想要的曲線。而三階貝塞爾曲線需要兩個控制點來確定,很顯然貝塞爾曲線不一定通過控制點,但是肯定通過端點。所以給定的頂點只能做端點,那問題就變成了如何計算所需要的控制點?
首先要保證曲線在頂點處連續,就要求左邊曲線在頂點處的切線和右邊曲線在頂點處的切線一致。即函數的左導數等于右導數。
根據前面的公式說明3 ?曲線的起始點(結束點)相切于貝塞爾多邊形的第一節(最后一節),我們知道,保持連續的必要條件是頂點和它前后的控制點在同一條直線上,而該直線就是曲線在該頂點的切線。
這里有一種思路:穿過已知點畫平滑曲線;英文原文地址,這里也有一篇文章說的是用lua語言來實現的:開放的多點貝塞爾曲線實現
總結一下如下圖所示:
如上圖所示:如果需要繪制一條通過點A、B、C的曲線,我們需要計算各條用于連接的貝塞爾曲線的控制點。
以頂點B為例:
1、取AB和BC的中點E、F,并連接E、F
2、在EF上取點D,使得FD/DE = BC/AB
3、將直線EF按照矢量DB平移到通過B點,并且使得平移后的D和B點重合
4、得到E'與F'點用作貝塞爾曲線的控制點。
將上述算法應用于多邊形的各個頂點,可以計算出2*n個控制點(每個頂點對應兩個控制點)
下面是一種利用OC代碼的實現(實現還比較粗糙在獲,取控制點之后直接繪制了曲線,實際應用中應該先緩存起來等到繪制的時候再使用控制點。)
-(void) getControlPointx0:(CGFloat)x0 andy0:(CGFloat)y0
x1:(CGFloat)x1 andy1:(CGFloat)y1
x2:(CGFloat)x2 andy2:(CGFloat)y2
x3:(CGFloat)x3 andy3:(CGFloat)y3
path:(UIBezierPath*) path
{
CGFloat smooth_value =0.6;
CGFloat ctrl1_x;
CGFloat ctrl1_y;
CGFloat ctrl2_x;
CGFloat ctrl2_y;
CGFloat xc1 = (x0 + x1) /2.0;
CGFloat yc1 = (y0 + y1) /2.0;
CGFloat xc2 = (x1 + x2) /2.0;
CGFloat yc2 = (y1 + y2) /2.0;
CGFloat xc3 = (x2 + x3) /2.0;
CGFloat yc3 = (y2 + y3) /2.0;
CGFloat len1 = sqrt((x1-x0) * (x1-x0) + (y1-y0) * (y1-y0));
CGFloat len2 = sqrt((x2-x1) * (x2-x1) + (y2-y1) * (y2-y1));
CGFloat len3 = sqrt((x3-x2) * (x3-x2) + (y3-y2) * (y3-y2));
CGFloat k1 = len1 / (len1 + len2);
CGFloat k2 = len2 / (len2 + len3);
CGFloat xm1 = xc1 + (xc2 - xc1) * k1;
CGFloat ym1 = yc1 + (yc2 - yc1) * k1;
CGFloat xm2 = xc2 + (xc3 - xc2) * k2;
CGFloat ym2 = yc2 + (yc3 - yc2) * k2;
ctrl1_x = xm1 + (xc2 - xm1) * smooth_value + x1 - xm1;
ctrl1_y = ym1 + (yc2 - ym1) * smooth_value + y1 - ym1;
ctrl2_x = xm2 + (xc2 - xm2) * smooth_value + x2 - xm2;
ctrl2_y = ym2 + (yc2 - ym2) * smooth_value + y2 - ym2;
[path addCurveToPoint:CGPointMake(x2, y2) controlPoint1:CGPointMake(ctrl1_x, ctrl1_y)controlPoint2:CGPointMake(ctrl2_x, ctrl2_y)];
}
代碼中的smooth_value是一個縮放值,取值范圍為[0,1]。通過調整這個值可以控制曲線的銳度。
給定一組測試頂點如下:
CGFloat dx =20;
CGFloat x0 =0+ dx;
CGFloat y0 =0+ dx;
CGFloat x1 =80+ dx;
CGFloat y1 =120+ dx;
CGFloat x2 =150+ dx;
CGFloat y2 =200+ dx;
CGFloat x3 =200+ dx;
CGFloat y3 =50+ dx;
調用的代碼如下:
UIBezierPath* path = [[UIBezierPathalloc]init];
[pathmoveToPoint:CGPointMake(x1, y1)];
[selfgetControlPointx0:x0andy0:y0x1:x1andy1:y1x2:x2andy2:y2x3:x3andy3:y3path:path];
[selfgetControlPointx0:x1andy0:y1x1:x2andy1:y2x2:x3andy2:y3x3:x0andy3:y0path:path];
[selfgetControlPointx0:x2andy0:y2x1:x3andy1:y3x2:x0andy2:y0x3:x1andy3:y1path:path];
[selfgetControlPointx0:x3andy0:y3x1:x0andy1:y0x2:x1andy2:y1x3:x2andy3:y2path:path];
[pathstroke];
效果:
可是雖然實現了用曲線包圍多邊形,但是依然沒有實現我們的需求,用曲線連接各個頂點...
其實走到這一步已經離我們的目標很近了!只需要修改一下我們生成貝塞爾曲線的調用方式
[pathmoveToPoint:CGPointMake(x0, y0)];
[selfgetControlPointx0:0andy0:0x1:x0andy1:y0x2:x1andy2:y1x3:x2andy3:y2path:path];
[selfgetControlPointx0:x0andy0:y0x1:x1andy1:y1x2:x2andy2:y2x3:x3andy3:y3path:path];
[selfgetControlPointx0:x1andy0:y1x1:x2andy1:y2x2:x3andy2:y3x3:250andy3:0path:path];
[pathstroke];
效果如下:
這里需要注意的是要處理一下起始點和結束點。上面設置的為(0,0)和(250,0);這兩個點是原有的點集沒有的,根據需要可以適當設置,會影響到第一段和最后一段曲線的轉向。