繪圖動畫基礎-曲線篇
-
貝茲曲線(Bezier Curve)
學過幾何的同學都知道,平面上相異兩點決定一直線,所謂「決定」是指經過這兩點的直線恰僅有一條,不會多也不會少。那如果是曲線的話,需要多少點才能決定呢?
像上一課利用滾動軌跡來畫曲線,雖然有趣,但其實是最笨的方法,因為相當耗費電腦記憶體,若我們每轉動1°記錄一個軌跡點(存在陣列中),則轉一圈就需紀錄360點,畫一個餘弦函數通常要轉好幾圈,就需紀錄上千點,若是複雜的外擺線,甚至需要紀錄上萬點才能畫出完整曲線。
所以通常畫曲線不是用軌跡,而是用公式,只要決定係數,就能畫出一條曲線。不過,用公式的缺點是耗費CPU計算時間,因為每一點都須經過函數計算,當然會拖慢速度。
所以,畫曲線比畫直線困難得多,有沒有什麼方法,可以節省記憶體,又不耗費太多CPU時間呢?貝茲曲線就是基於這個目的所設計,它並不是幾百年前流傳下來的公式,而是現代數學家針對電腦化需求所發明的,至今已成為電腦曲線的標準,所有向量繪圖軟體、字型設計、3D模型、動漫遊戲...等,背後都是離不開貝茲曲線。
在SwiftUI裡面用畫布Canvas來畫曲線,實際上就是用二階與三階貝茲曲線,二階貝茲曲線需要3個點,除了指定曲線的起點與終點外,還需要額外1個控制點,三階則需要4個點(起點、終點、2個控制點),如下圖:
貝茲曲線的控制點若與起點、終點共線,則會畫出一條直線(線段),若控制點在線外,才會產生曲線,曲線雖不通過控制點,但控制點就像有引力,會吸引曲線靠近。上圖將三階的兩個控制點重合,可以看出與二階的差異,三階曲線會更接近控制點(感覺吸引力較強)。
畫曲線的語法非常簡單,三階(最通用)是 addCurve(),二階是 addQuadCurve(),Quad 是 Quadratic (二次方的)簡寫。
操作步驟如下:
選取畫面左方的「+」號新增電子書頁面。
將新增的電子書面頁命名為「(32)SwiftUI動畫-7曲線篇」。
在「Main」模組中撰寫程式:
// 4-7a 貝茲曲線 // Created by Philip, Heman, Jean 2022/05/09 // Revised by Jean 2025/01/26 import PlaygroundSupport import SwiftUI struct 二階貝茲曲線: View { var 說明 = "二階貝茲曲線" var body: some View { Canvas { 圖層, 尺寸 in let 寬 = 尺寸.width let 高 = 尺寸.height let 左上角 = CGPoint.zero let 左下角 = CGPoint(x: 0, y: 高) let 右上角 = CGPoint(x: 寬, y: 0) var 畫筆 = Path() // 對角線 畫筆.move(to: 左下角) 畫筆.addLine(to: 右上角) 圖層.stroke(畫筆, with: .color(.gray)) 畫筆 = Path() // 二階貝茲曲線 畫筆.move(to: 左下角) 畫筆.addQuadCurve(to: 右上角, control: 左上角) 圖層.stroke(畫筆, with: .color(.cyan), lineWidth: 3) var 文字 = 圖層.resolve(Text(說明)) // 說明文字 let 文字尺寸 = 文字.measure(in: 尺寸) let 文字框 = CGRect( x: 寬 - 文字尺寸.width - 10, y: 高 - 文字尺寸.height - 10, width: 文字尺寸.width, height: 文字尺寸.height) // print(文字尺寸, 文字框) 文字.shading = .color(.gray) 圖層.draw(文字, in: 文字框) } } } struct 三階貝茲曲線: View { var 說明 = "三階貝茲曲線" var body: some View { Canvas { 圖層, 尺寸 in let 寬 = 尺寸.width let 高 = 尺寸.height let 左上角 = CGPoint.zero let 左下角 = CGPoint(x: 0, y: 高) let 右上角 = CGPoint(x: 寬, y: 0) var 畫筆 = Path() // 對角線 畫筆.move(to: 左下角) 畫筆.addLine(to: 右上角) 圖層.stroke(畫筆, with: .color(.gray)) 畫筆 = Path() // 三階貝茲曲線 畫筆.move(to: 左下角) 畫筆.addCurve(to: 右上角, control1: 左上角, control2: 左上角) 圖層.stroke(畫筆, with: .color(.cyan), lineWidth: 3) var 文字 = 圖層.resolve(Text(說明)) // 說明文字 let 文字尺寸 = 文字.measure(in: 尺寸) let 文字框 = CGRect( x: 寬 - 文字尺寸.width - 10, y: 高 - 文字尺寸.height - 10, width: 文字尺寸.width, height: 文字尺寸.height) // print(文字尺寸, 文字框) 文字.shading = .color(.gray) 圖層.draw(文字, in: 文字框) } } } struct 畫布: View { var body: some View { Label("[SwiftUI]4-7a 貝茲曲線", systemImage: "swift") .font(.title) .foregroundColor(.orange) .padding() 二階貝茲曲線() .border(.red) 三階貝茲曲線(說明: "三階貝茲曲線\n(c)2022 Heman Lu") .border(.red) } } PlaygroundPage.current.setLiveView(畫布())
程式執行結果,如下圖。
* 說明
此範例程式中還介紹一個重要功能:在畫布Canvas中添加文字。
我們曾在第5課一開始,範例4-5a就用過「圖層.draw()」在畫布中顯示文字,不過那時候還不會計算座標,導致圖示與文字重疊在一起,相當不美觀,現在終於要學習改善的方法了。
在畫布(Canvas)中,如何知道文字的大小尺寸,以便精確計算擺放的位置呢?這其實是從第1課跑馬燈開始就存在的問題。
答案是用圖層的方法 resolve() 來解析,resolve 字面是解決、溶解、解析的意思。透過 resolve() 的解析,我們得以獲知文字或圖片的寬高尺寸,進而計算擺放位置的座標。
var 文字 = 圖層.resolve(Text(說明)) // 說明文字 let 文字尺寸 = 文字.measure(in: 尺寸) let 文字框 = CGRect( x: 寬 - 文字尺寸.width - 10, y: 高 - 文字尺寸.height - 10, width: 文字尺寸.width, height: 文字尺寸.height) // print(文字尺寸, 文字框) 文字.shading = .color(.gray) 圖層.draw(文字, in: 文字框)
在這段程式碼中,用圖層解析文字視圖Text(),字體大小是預設值 .body,然後用 measure() 去測量這段文字在某個「尺寸」裡所佔的寬高,為什麼還要指定「尺寸」呢?因為如果視框的尺寸太小,文字會被自動裁減(後面用...表示),得到的文字尺寸就不一樣。
此例先用整個全框尺寸來測量文字的實際大小,然後指定一個可以擺放文字的視框(CGRect),產出CGRect 物件實例需要指定左上角座標(x, y),以及寬(width)、高(height)。我們希望是擺在畫布右下角,邊緣留10點空白,如下圖:
最後塗上顏色(shading),就能用 draw() 畫出來了。在畫布(Canvas)中都用 shading 來指定色彩(或漸層),而不是用 foregroundColor,shading 的原形 shade 可以當名詞「陰影」或動詞「遮陰」,用在繪畫是指「著色」或「塗色」(以產生明暗變化)。
文字.shading = .color(.gray) 圖層.draw(文字, in: 文字框)