視覺化研發之線的畫法:直線,曲線,動畫(Canvas版)

語言: CN / TW / HK

“線”是視覺化展現中最常見的圖形元素,最直觀的就是折線圖,如圖一。

圖一 折線圖

一條線由多個點來定義,按照點與點之間的連線方式,通常將線分為“折線”和“曲線”,在畫法上又分為“實線”和“虛線”,如圖二:

圖二 折線和曲線

我們也經常使用線來繪製閉合的路徑,從而形成可填充區域,比如面積圖和雷達圖,如圖三和圖四。

圖三 面積圖

圖四 雷達圖

本篇文章在 Canvas API 的基礎上,為大家講解視覺化研發中線的畫法封裝和線的動畫實現方案(整體方案建立在圖形學基礎上,同樣適用於WegGL 和3D場景)。

* 0.1 線的定義

前面我們提到過線的基本組成單位是“點(Point)”,兩個相鄰的點連線在一起成為一個“段(Segment)”,多個段拼裝在一起組成一條線。如圖六,這條線由7個點劃分成的6個段組合而成。

圖六 點和段

曲線的每個段的起止點會因為插值演算法的不同而不同,後面我們會詳細介紹。

圖七所示的虛擬碼展示了我們對線的基本定義。

圖七 線的定義

線的繪製是以段為單位的,不同的形狀的線對段的拆分邏輯和畫法都是有區別的,我們從最簡單的折線開始。

0.2 折線畫法

0.2.1 獲取段

折線對段的拆分很簡單,根據傳入的點資料,相鄰兩點劃為一段。

圖八 折線段的拆分

如上面的程式碼,實現很簡單,依次遍歷點資料,初始化段物件。這裡有一個計算段的長度的操作,段的長度在動畫場景是必須引數,在非動畫場景則可以不用關心。折線的段的長度計算,就是計算一個線段的長度(兩點間距離),如圖九所示。

圖九 線段的距離計算

另外圖八的程式碼中,有一段是否是空段的判斷邏輯。在實際的線圖應用中,我們在某些情況需要隱藏線的某些段,比如傳入了空資料或者使用者指定了過濾條件。

圖十 空段

0.2.2 Canvas中線段的畫法

在Canvas中畫線段只需要兩個api——moveTo 和 lineTo。圖十一展示了連線[(0,0),(300,150),(400,150)]三個點的折線。

圖十一 moveTo 與 lineTo

從上面的示例可以看到,Canvas 中繪製線段,只需要通過moveTo將畫筆(Canvas 繪圖上下文)定位到線段的起點,然後通過lineTo 繪製到線段的終點即可。多個首位相接的線段可以省略moveTo,直接lineTo。 要實現圖十的空段效果,只需要moveTo到新段的起點即可,例如:

圖十二 繪製空段

理解了基本的api之後,我們回到我們的折線上來,看看以段為單位的繪製方法。

0.2.3 折線繪製

基於上面畫線的方法,我們只需要遍歷一條線中的所有段,依次連線就可以了。為了處理空段的繪製,設定一個lineStart的標記變數,如果處於start狀態,會先moveTo到新的點,而不是lineTo。大致的繪圖流程如下:

圖十三 線的繪製基本流程

drawSegment方法如下:

圖十四 drawSegment

這裡你可能要疑惑,這裡將線拆成段並沒有什麼優勢,為什麼不直接連線各個點呢?分成段完成了一個線的繪製的骨架,在這個骨架基礎上,很多功能都會很容易的擴充套件。比如,線的每一段都有不同的含義,視覺化層面要展現這些不同的含義需要給線賦予不同的樣式。這裡我們可以給LineSegment配置一個LineSegConfig,獨立配置每個段的樣式,在繪製的過程中如果發現新的段的樣式發生了變化,就可以立即進行渲染,然後開始繪製新段,靈活拼裝。比如下圖,末尾的紅色虛線用來表示預測資料。

圖十五 分段渲染不同樣式

另外,分段會大大降低動畫效果的實現成本,後面我們詳細介紹。

瞭解了折線的基本畫法之後,我們來看看曲線。

0.3 曲線畫法

0.3.1 貝塞爾曲線

曲線有很多種,畫曲線的方法也有很多種。由於Canvas 支援貝塞爾二次和三次曲線畫法,曲線圖表通常使用三次貝塞爾曲線畫法,本文也將重點放在三次貝塞爾曲線的應用講解上。那麼什麼是貝塞爾曲線呢?

Bézier curve(貝塞爾曲線)是應用於二維圖形應用程式的數學曲線。貝塞爾曲線點的數量決定了曲線的階數,一般N個點構成的N-1階貝塞爾曲線,即3個點為二階。一般我們都會要求曲線至少包含3個點,因為兩個點的貝塞爾曲線是一條直線。按順序,第一個點為 起點 ,最後一個點為 終點 ,其餘點都為 控制點

下面我們以二次貝塞爾曲線為例,討論其生成過程。

二次貝塞爾曲線

給定點P0,P1,P2 ,P0 和 P2 為起點和終點,P1為控制點。從P0到P2的弧線即為一條二次貝塞爾曲線。

圖十六 二次貝塞爾曲線

在這裡我們要將整個曲線的繪製量化為從 0~1 的過程,用 t 為當前過程的進度, t 的區間即 0~1 。每一條線都需要根據 t 生成一個點,如下圖,一個點從 P0 移動到 P1 ,這是這條線從 0~1 的過程。

下面我們還原一下一個二次貝塞爾曲線的生成過程。

圖十七 繪製二次貝塞爾曲線(1)

如圖十七,首先我們連結P0P1,P1P2,得到兩條線段。然後我們對進度t進行取值,比如0.3,取一個Q0點,使得P0Q0的長度為P0P1總長度的0.3倍。

圖十八 繪製二次貝塞爾曲線(2)

同時我們在P1P2上取一點Q1,使得 P0Q0: P0P1 = P1Q1: P1P2 。接下來我們再在Q0Q1上取一點B,使得 P0Q0: P0P1 = P1Q1: P1P2 = Q0B:Q0Q1, 如圖十九。

圖十九 繪製二次貝塞爾曲線(3)

現在我們得到的點B就是二次貝塞爾曲線的上的一個點,如果我們使t=0開始取值,逐步遞增進行插值,就會得到一系列的點B,進行連線就會形成一條完整的曲線,如圖二十。

圖二十 二次貝塞爾曲線繪製過程

上面展示了完整的二次貝塞爾曲線的產生過程,這個過程我們經過數學推導,最終可以得到如下公式:

根據這個公式,我們只要變更t值,就可以得到對應的點。

三次貝塞爾曲線

對應的,三次貝塞爾曲線由四個點組成,通過更多的迭代步驟來確定曲線的上點,如圖二十一所示。完整的生成如果如圖二十二所示。

圖二十一 三次貝塞爾曲線

圖二十二 三次貝塞爾曲線生成過程

三次貝塞爾曲線的數學公式為:

0.3.2 Canas中如何繪製貝塞爾曲線

在canvas中繪製二次貝塞爾曲線使用的是 quadraticCurveTo 函式,引數定義如下:

函式只定義了控制點和終點,起點需要我們使用 moveTo 來確定,如圖二十三的程式碼示例。

圖二十三 canvas 繪製二次貝塞爾曲線

三次貝塞爾曲線使用 bezierCurveTo() 方法來繪製,引數定義如下:

和二次曲線的繪製方式類似,如圖二十四。

圖二十四 canvas繪製三次貝塞爾曲線

下面的動圖展示了控制點對貝塞爾曲線形狀的影響。

圖28 控制點對貝塞爾曲線的影響

0.3.3 樣條曲線 與 獲取段

我們瞭解瞭如何繪製三次貝塞爾曲線,但是回到我們的線圖,一個線圖會有不確定數量的點被平滑的連線起來,但是目前三次貝塞爾曲線顯然無法滿足這個需求。我們前面談到了分段的概念,一條完整的曲線被分成了多段,如果每一段都是一條三次貝塞爾曲線,問題就解決了。那麼問題就轉化成了如何構造多條能依次平滑拼接的貝塞爾曲線。在圖形學中有個概念叫“樣條曲線”,專業的概念有點難懂,我們這裡簡單理解就是將一個點的集合,分成多段曲線,各曲線處的連線點處有可以平滑連線(有連續的一次和二次導數)。關於樣條曲線的連續性以及貝塞爾曲線的更多特性,讀者可以參考《計算機圖形學(第四版)》一書第14章——《樣條表示》,這裡我們就不深入解釋了,直接看例子。

圖29 一段由四條三次貝塞爾曲線拼接而成的曲線

以圖29為例,如我我們要將這條曲線分成四條三次貝塞爾曲線,我們要確定兩個引數:

  1. 每條三次貝塞爾曲線的起點和終點
  2. 每條三次貝塞爾曲線的兩個控制點

只有選取合適的起點、終點和控制點,我們才能使得相鄰的兩條曲線可以平滑連線。樣條曲線的拆分演算法有很多種,這裡也不詳細介紹了,感興趣的同學可以參考圖形學相關書籍;JavaScript 實現可以參考 d3-shape 的 Curves 介面( https:// github.com/d3/d3-shape ),d3-shape Curves 中的 curveBasis、curveBasisClosed、curveBasisOpen、curveBundle、curveCardinal、curveCardinalClosed、curveCardinalOpen、curveCatmullRom、curveCatmullRomClosed、curveCatmullRomOpen、curveNatural、curveMonotoneX 和 curveMonotoneY 都是基於三次貝塞爾曲線的樣條實現。

下面我們以Basis 演算法的實現為例,進行講解曲線如何獲取“段”。

主流程

Basis 演算法要求點集中的點的數量至少為3個,然後我們利用如下邏輯進行段的獲取:

  • 圖30 獲取曲線的 “段” 的主流程
  • 我們的主流程邏輯很簡單,迴圈給到的點,從當前索引位置開始向後取出3個點,然後根據這三個點以及當前段的起始點計算結束點和控制點。每個新段的起點是上一個相鄰段的終點。隨後計算當前段的長度。 當前的迴圈邏輯不會計算到最後一個點,所以會少一個段,最後加個單獨的邏輯來處理。

點的計算

下面來看看 Basis 演算法點的計算:

  • 圖31 basis 樣條演算法

如圖31,我們基於很簡單的公式來計算各個點的值,這個公式是怎麼來的呢?簡單說是結合了B樣條曲線和三次貝塞爾曲線在端點處的一階和二階匯出得來的。這裡就不深入了,否則本篇文章會嚴重偏離主題,感興趣的讀者請參閱計算機圖形學相關書籍。總之,我們通過公式計算可以得到我們需要的點。

曲線分割與長度計算

計算曲線的長度並不是一件容易的事情,由於貝塞爾函式是插值函式,所以計算方法就是先對曲線進行切割,切割到足夠小的範圍,然後計算這一小段的曲線近似長度,再累加。0.3.1節給出了三次貝塞爾曲線的函式,我們只需要將變數t取足夠小的值,然後計算兩個點之間的直線距離進行累加就可以,但是這種方案的效能消耗比較大。我在

https:// community.khronos.org/t /3d-cubic-bezier-segment-length/62363/2

看到一種近似方法,利用該方法可以縮減切割次數。 基於三次貝塞爾曲線的函式,對一個貝塞爾曲線進行切割,很簡單。我們再把圖21拿來說明一下各點的計算。

圖21

第一步:找到連線點

如圖21,假設我要在t=0.25的位置將當前曲線切分成兩條曲線,首先我們要知道點B的位置。根據公式帶入即可:

圖33 根據t計算3次貝塞爾的點

第二步:獲取控制點

拿到點P之後,P就是第一段的終點,第二段的起點,這樣我們只需要計算控制點即可。根據我們之前對貝塞爾曲線繪製過程的理解,我們可以得出如下結論:

  1. 第一段曲線的第一個控制點的運動軌跡是線段P0P1,和t線性相關
  2. 第一段曲線的第二個控制點的運動軌跡是線段Q0Q1,和t線性相關
  3. 第二段曲線的第一個控制點的運動軌跡是線段Q1Q2,和t線性相關
  4. 第二段曲線的第二個控制點的運動軌跡是線段P2P3,和t線性相關

依據上面的結論,三次貝塞爾曲線拆分的方法就很容易實現了:

圖34 貝塞爾曲線拆分

圖34 所示程式碼中 pointAt 方法為根據t獲取直線上點的方法。如下:

圖35 根據t獲直線上的點

第三步 長度計算

我們可以在任意位置對三次貝塞爾曲線進行拆分了,結合二分法,控制迭代次數,結合近似長度計算函式,我們可以得到想要精度的長度值了。如圖36。

圖36 三次貝塞爾曲線的分割

獲取段

內部細節我們都梳理清楚了,獲取所有的段也很簡單了。現在需要特殊處理的是最後一個點資料,這裡我們將第二個點和第三個點都用最後一個點表示。

圖37 basis 最後一段生成方法

0.3.4 曲線畫法

關於曲線的所有準備工作都完成了,下面我們要把它畫出來。和畫折線的方法類似,我們只需要迴圈呼叫"段" 的繪製方法進行繪圖即可。內部,只需要呼叫bezierCurveTo即可。如下:

圖38 繪製曲線的段

0.4 動畫

我們完成了折線和曲線的繪製,想要線通過動畫的方式畫出來,只需要做少量的改動。首先不論直線還是曲線我們都分成了多段,每一段都是和t相關的函式。

0.4.1 基本方案

動畫和非動畫的本質區別就是一次畫多少的問題,我們將整條線圖的繪製放置在[0,1]區間內,啟動一個動畫迴圈,每次繪圖的時候更新的t的值,在我們上面迴圈繪製segment 的程式碼中,將整條線圖的t轉化為每一個段內部的t值。段 內部根據傳入的t值,對自身進行切割,只畫應該繪製的那部分。

圖39 t值換算

因為我們已經計算了每個段的長度,和總長度,所以每個段的佔比由長度可以獲得,此佔比在和整個線圖的t值進行換算即可。

以圖39為例,比如我們傳入的t值為0.1,整條線圖的0.1 換算到第一個段是0.4,那麼第一個段只需繪製前40% 部分即可。我們在圖39的基礎上,做少量的改動。

圖40 支援區域性繪製

如圖40,我們將外部計算的t(percent)傳入繪製段的方法內,該方法會使用我們之前介紹過的 divideCubic 方法對當前曲線進行切割,然後進行區域性繪製。效果如下:

圖41 動畫

0.4.2 和其他動畫方案的對比

實現線和麵積的動畫的方案還有整體Clip和生成點集兩種方案,下面我們簡單對比一下,以說明我們的分段繪製的優勢。

0.4.3 動畫同步

上面我們看到的動畫不同的線之間雖然可以再同一時間到大終點,但是過程中在x方向的位移是不同步的。同步和不同步都各有需求,尤其是在面積圖情況下,單個面積圖實際被拆分了上下兩組segment。如圖41.

圖41 基本面積圖的segement

我們觀察上面面積圖的繪圖動畫,它是從左到右推進的,比如當前的t值繪製到圖41的矩形框的位置,那麼首先會繪製第一段,計算第12段應該被繪製的區間,最後填充上下兩段的閉合區間。這裡有一個問題,如果是相同的t值,帶入1和12的函式,產生的x值是不一樣的,那麼繪製出來的效果就不對了,切面可能是斜的。

解決這個問題做法是根據x或者y值反求t值,再帶入目標函式中。對於三次貝塞爾曲線來說,這又是一個大難題,由於篇幅所限及程式碼實現的比較複雜,這裡就不再講解了,大家可以參考文後的參考資料。

0.5 參考資料:

一個超酷的貝塞爾類庫: https:// pomax.github.io/bezierj s/

一本超級棒的貝塞爾電子書 https:// pomax.github.io/bezieri nfo/

關於根據x或y反算t的討論: https://www. zhihu.com/question/3057 0430

圖形學必讀書物:《計算機圖形學》