IV-6a 圓周運動

上一課介紹的Canvas把螢幕當作畫布來作圖,如果再結合第4課的時間軸視圖TimeilneView會如何呢?答案就是更有創意、更精細的動畫。本課利用這兩個功能強大的物件,讓螢幕的畫素動起來。

一開始,仍從簡單的做起,先做一個小球的圓周運動。想法很簡單,先畫一個大圓,再畫一個實心小圓,在大圓圓周上移動,在物理上稱為圓周運動,就像人造衛星繞著地球轉一樣。

上一課已學過如何在Canvas中畫圓,所以問題就是如何讓小球繞圓周運動?答案就是靠時間軸視圖TimelineView,仿照第4課4-4b的做法,在 Canvas 中加上 onChange 修飾語,每次收到時間軸的更新,就移動1°角。

.onChange(of: 時間) { _ in
    圓心角 = 圓心角 + 1.0
}

所以接下來的問題,是如何計算小球的座標位置?這就需要用到最基本的三角函數:正弦 sin 與餘弦 cos,要利用三角函數來解問題,秘訣就在於找出「直角」,圓周運動哪裡可以找到直角呢?請參考下圖,半徑剛好是一個直角三角形的斜邊:

截圖 2022-05-03 上午8.27.43.png

我們從正上方(12點鐘方向)為起點,順時針移動,已知中心點座標(x, y)、圓心角𝛉、半徑r,如何求得頂點座標呢?很簡單,從中心點往上移動dy, 再往右移動dx,就可得到頂點座標,而 dy, dx 長度分別等於 cos𝛉, sin𝛉 乘以半徑r(斜邊)。

其中圓心角𝛉要換算成弧度,弧度1𝜋等於180°,所以換算公式為:

弧度 = 角度 * 𝜋 / 180

在 Swift 中,凡是實數類型,包括Float, Double, CGFloat等,都會有一個類型屬性 .pi,代表 𝜋。程式中寫 CGFloat.pi 或 .pi 來引用,

我們讓圓心角每次變化1°,不管移動多少度,頂點座標都可以按照上面公式計算出來。然後以頂點為圓心畫一個實心小圓,經過TimelineView的驅動,小圓自然而然就移動起來了。

完整程式碼如下:

// 4-6a 圓周運動 Canvas + TimelineView
// Created by Heman, 2022/04/23
import PlaygroundSupport
import SwiftUI

struct 圓周運動: View {
    let 時間: Date
    @State var 圓心角 = 0.0
    var body: some View {
        Canvas { 圖層, 尺寸 in
            let 寬 = 尺寸.width
            let 高 = 尺寸.height
            let 中心 = CGPoint(x: 寬/2, y: 高/2)
            let 半徑 = min(寬, 高) / 2
            
            var 畫筆 = Path()
            畫筆.addArc(        // 大圓
                center: 中心,
                radius: 半徑,
                startAngle: .zero,
                endAngle: .degrees(360),
                clockwise: false
            )
            圖層.stroke(畫筆, with: .color(.blue), lineWidth: 2)
            
            畫筆 = Path()
            let 頂點 = CGPoint(
                x: 中心.x + 半徑 * sin(圓心角 * .pi / 180),
                y: 中心.y - 半徑 * cos(圓心角 * .pi / 180))
            畫筆.move(to: 頂點)
            畫筆.addArc(        // 小圓
                center: 頂點,
                radius: 5,
                startAngle: .zero,
                endAngle: .degrees(360),
                clockwise: false
            )
            圖層.fill(畫筆, with: .color(.cyan))
            
            畫筆.move(to: 頂點)
            畫筆.addLine(to: 中心)
            圖層.stroke(畫筆, with: .color(.brown), lineWidth: 1)
        } 
        .onChange(of: 時間) { _ in
            圓心角 = 圓心角 + 1.0
        }
    }
}

struct 畫布: View {
    var body: some View {
        Label("[SwiftUI] 4-6a 圓周運動", systemImage: "swift")
            .font(.largeTitle)
            .foregroundColor(.orange)
            .padding()
        TimelineView(.animation) { 時間參數 in
            圓周運動(時間: 時間參數.date)
                .border(Color.red)
            // .frame(width: 100)
            Text("現在時間\\(時間參數.date)")
                .padding()
        }
    }
}

PlaygroundPage.current.setLiveView(畫布())