一篇學(xué)會使用 SwiftUI 創(chuàng)建萬花尺
本文轉(zhuǎn)載自微信公眾號「Swift社區(qū)」,作者韋弦Zhy。轉(zhuǎn)載本文請聯(lián)系Swift社區(qū)公眾號。
為了完成一些真正意義上的繪圖工作,我將帶您通過創(chuàng)建一個(gè)簡單的帶 SwiftUI 的 spirograph。“Spirograph”是一種玩具的商標(biāo)名稱,你把一支鉛筆放在一個(gè)圓圈里,然后繞著另一個(gè)圓圈的圓周旋轉(zhuǎn),創(chuàng)造出各種幾何圖案,稱為輪盤賭——就像賭場游戲一樣。
這段代碼包含一個(gè)非常具體的公式。我會解釋的,但是如果你不感興趣的話,跳過這一章是完全可以的——這只是為了好玩,這里沒有介紹新的 Swift 或 SwiftUI。
我們的算法有四個(gè)輸入:
- 內(nèi)圈的半徑。
- 外圈的半徑。
- 虛擬筆與外圓中心的距離。
- 要畫多少輪盤賭。這是可選的,但我認(rèn)為它確實(shí)有助于顯示算法工作時(shí)發(fā)生的情況。
因此,讓我們開始吧:
- struct Spirograph: Shape {
- let innerRadius: Int
- let outerRadius: Int
- let distance: Int
- let amount: CGFloat
- }
然后,我們從數(shù)據(jù)中準(zhǔn)備三個(gè)值,從內(nèi)半徑和外半徑的最大公約數(shù)(GCD)開始。計(jì)算兩個(gè)數(shù)字的GCD通常是用Euclid算法完成的,它的形式稍微簡化如下:
- func gcd(_ a: Int, _ b: Int) -> Int {
- var a = a
- var b = b
- while b != 0 {
- let temp = b
- b = a % b
- a = temp
- }
- return a
- }
把這個(gè)方法添加到Spirograph結(jié)構(gòu)體中。
另外兩個(gè)值是內(nèi)半徑和外半徑之間的差異,以及我們需要執(zhí)行多少步驟來繪制輪盤——這是360度乘以外半徑除以最大公約數(shù),再乘以我們的數(shù)量輸入。我們所有的輸入以整數(shù)形式提供時(shí)效果最好,但是在繪制輪盤賭時(shí),我們需要使用CGFloat,因此我們還將創(chuàng)建輸入的CGFloat副本。
現(xiàn)在將這個(gè)path(in:)方法添加到Spirograph結(jié)構(gòu)體:
- func path(in rect: CGRect) -> Path {
- let divisor = gcd(innerRadius, outerRadius)
- let outerRadius = CGFloat(self.outerRadius)
- let innerRadius = CGFloat(self.innerRadius)
- let distance = CGFloat(self.distance)
- let difference = innerRadius - outerRadius
- let endPoint = ceil(2 * CGFloat.pi * outerRadius / CGFloat(divisor)) * amount
- // more code to come
- }
最后,我們可以通過循環(huán)從 0 到我們的終點(diǎn)來畫輪盤賭,并放置在精確的 X/Y 坐標(biāo)點(diǎn)。計(jì)算循環(huán)中給定點(diǎn)的 X/Y 坐標(biāo)(稱為“theta:θ”)是真正的數(shù)學(xué)來源,但老實(shí)說,我只是把維基百科上的標(biāo)準(zhǔn)方程式轉(zhuǎn)換成 Swift ——這不是我夢寐以求的記憶!
- X等于半徑差乘以 θ 的余弦,再乘以半徑差的余弦除以外半徑乘以θ的距離。
- Y等于半徑差乘以 θ 的正弦,減去距離乘以半徑差的正弦除以外半徑乘以 θ。
這是核心算法,但我們要做兩個(gè)小的改變:我們要分別將繪圖矩形的一半寬度或高度添加到X和Y,使其在繪圖空間中居中;如果 θ 為 0,即如果這是輪盤中繪制的第一個(gè)點(diǎn),我們將我們的路徑中調(diào)用move(to:)而不是addLine(to:)。
以下是path(in:)方法的最后一個(gè)代碼——用以下內(nèi)容替換// more code to come注釋:
- var path = Path()
- for theta in stride(from: 0, through: endPoint, by: 0.01) {
- var x = difference * cos(theta) + distance * cos(difference / outerRadius * theta)
- var y = difference * sin(theta) - distance * sin(difference / outerRadius * theta)
- x += rect.width / 2
- y += rect.height / 2
- if theta == 0 {
- path.move(to: CGPoint(x: x, y: y))
- } else {
- path.addLine(to: CGPoint(x: x, y: y))
- }
- }
- return path
我意識到這有很多繁重的數(shù)學(xué),但回報(bào)即將到來:我們現(xiàn)在可以在視圖中使用該形狀,添加各種滑塊來控制內(nèi)半徑、外半徑、距離、數(shù)量,甚至顏色:
- struct ContentView: View {
- @State private var innerRadius = 125.0
- @State private var outerRadius = 75.0
- @State private var distance = 25.0
- @State private var amount: CGFloat = 1.0
- @State private var hue = 0.6
- var body: some View {
- VStack(spacing: 0) {
- Spacer()
- Spirograph(innerRadius: Int(innerRadius), outerRadius: Int(outerRadius), distance: Int(distance), amount: amount)
- .stroke(Color(hue: hue, saturation: 1, brightness: 1), lineWidth: 1)
- .frame(width: 300, height: 300)
- Spacer()
- Group {
- Text("Inner radius: \(Int(innerRadius))")
- Slider(value: $innerRadius, in: 10...150, step: 1)
- .padding([.horizontal, .bottom])
- Text("Outer radius: \(Int(outerRadius))")
- Slider(value: $outerRadius, in: 10...150, step: 1)
- .padding([.horizontal, .bottom])
- Text("Distance: \(Int(distance))")
- Slider(value: $distance, in: 1...150, step: 1)
- .padding([.horizontal, .bottom])
- Text("Amount: \(amount, specifier: "%.2f")")
- Slider(value: $amount)
- .padding([.horizontal, .bottom])
- Text("Color")
- Slider(value: $hue)
- .padding(.horizontal)
- }
- }
- }
- }
這是很多代碼,但我希望你花時(shí)間運(yùn)行應(yīng)用程序,并欣賞有多么美麗的輪盤。你所看到的其實(shí)只是一種輪盤賭形式,被稱為 hypotrochoid ——通過對算法的小調(diào)整,你可以生成 epitrochoids 等,它們以不同的方式很漂亮。
在我結(jié)束之前,我想提醒你,這里使用的參數(shù)方程是數(shù)學(xué)標(biāo)準(zhǔn),而不是我剛剛發(fā)明的東西——我真的去百度了關(guān)于 hypotrochoids[1] 的頁面,并將它們轉(zhuǎn)換為 Swift。
參考資料
[1]hypotrochoids: http://www.durangobill.com/Trochoids.html