如同圓周運動可以衍伸出很多漂亮又有趣的圖案,滾動也是如此。如果將滾動時,頂點的軌跡記錄下來,會是什麼形狀呢?
這種奇特的軌跡稱為「擺線」(cycloid):
不過,本節要利用滾動軌跡來畫另一個軌跡 — 「餘弦函數」(cosine, 簡寫為cos),因為cos是三角函數中,最具代表性的函數,只要熟悉cos函數,其他函數都可以用cos導出。甚至著名的傅立葉轉換,還可將任意波形轉換為餘弦函數的序列組合,換句話說,只用餘弦函數,就能夠表達所有的波形,厲害吧。
要形成餘弦曲線,我們要記錄的是下圖中的「垂足」軌跡,這是頂點至圓心垂直線的垂足,頂點、垂足、圓心形成一個直角三角形,如下圖:
知道如何計算垂足座標之後,怎麼畫出垂足的軌跡呢?應該是從上次垂足座標到目前垂足座標之間畫一線段,但問題是怎麼保存上次垂足座標呢?這個問題困擾筆者好幾天,試過幾種方法都不成功,最後不得不使用一個全域變數來解決。
完整範例程式如下:
// 4-6c 滾動軌跡(餘弦函數) Canvas + TimelineView
// Updated by Heman, 2022/05/05
import PlaygroundSupport
import SwiftUI
var 軌跡: [CGPoint] = []
struct 滾動軌跡: View {
let 時間: Date
@State var 圓心角 = 0.0
var body: some View {
Canvas { 圖層, 尺寸 in
var 軌跡圖層 = 圖層
let 寬 = 尺寸.width
let 高 = 尺寸.height
let 半徑 = min(寬, 高) / 2
let 中心 = CGPoint(x: 寬/2, y: 高/2)
let 左中 = CGPoint(x: 0, y: 高/2)
let 滾動距離 = 半徑 * 圓心角 * .pi / 180
let 位移 = 滾動距離.truncatingRemainder(dividingBy: 寬)
// print(位移, 軌跡.count)
圖層.translateBy(x: 位移, y: 0)
var 畫筆 = Path()
畫筆.addArc( // 大圓
center: 左中,
radius: 半徑,
startAngle: .zero,
endAngle: .degrees(360),
clockwise: false
)
圖層.stroke(畫筆, with: .color(.green), lineWidth: 2)
畫筆 = Path()
let 頂點 = CGPoint(
x: 左中.x + 半徑 * sin(圓心角 * .pi / 180),
y: 左中.y - 半徑 * cos(圓心角 * .pi / 180))
畫筆.addArc( // 頂點小圓
center: 頂點,
radius: 5,
startAngle: .zero,
endAngle: .degrees(360),
clockwise: false
)
let 垂足 = CGPoint(
x: 左中.x,
y: 左中.y - 半徑 * cos(圓心角 * .pi / 180))
畫筆.addArc( // 垂足小圓
center: 垂足,
radius: 5,
startAngle: .zero,
endAngle: .degrees(360),
clockwise: false
)
圖層.fill(畫筆, with: .color(.cyan))
畫筆 = Path() // 直角三角形
畫筆.addLines([頂點, 垂足, 左中, 頂點])
圖層.stroke(畫筆, with: .color(.gray), lineWidth: 1)
var 軌跡筆 = Path() // 軌跡(餘弦曲線)
let 垂足座標 = CGPoint(
x: 垂足.x + 位移,
y: 垂足.y)
軌跡 = 軌跡 + [垂足座標]
軌跡筆.addLines(軌跡)
軌跡圖層.stroke(軌跡筆, with: .color(.cyan), lineWidth: 3)
}
.onChange(of: 時間) { _ in
圓心角 = 圓心角 + 2.0
if 圓心角 >= 360 * 99 {
圓心角 = 0
軌跡 = []
}
}
}
}
struct 餘弦函數: View {
var body: some View {
Canvas { 圖層, 尺寸 in
let 寬 = 尺寸.width
let 高 = 尺寸.height
let 中心 = CGPoint(x: 寬/2, y: 高/2)
let 半徑 = min(寬, 高) / 2
var x = 0.0
var 餘弦曲線: [CGPoint] = []
while x < 寬 {
let y = cos(x/半徑) * 半徑
x += 2.0
餘弦曲線 = 餘弦曲線 + [CGPoint(x: x, y: 中心.y - y)]
}
var 畫筆 = Path()
畫筆.addLines(餘弦曲線)
圖層.stroke(畫筆, with: .color(.primary), lineWidth: 1)
}
}
}
struct 畫布: View {
var body: some View {
Label("[SwiftUI] 4-6c 滾動軌跡(餘弦函數)", systemImage: "swift")
.font(.title)
.foregroundColor(.orange)
.padding()
TimelineView(.animation) { 時間參數 in
滾動軌跡(時間: 時間參數.date)
// .background(座標軸())
.frame(height: 200)
.border(Color.red)
Text("By Heman.\\n\\(時間參數.date)")
.multilineTextAlignment(.center)
.font(.callout)
.foregroundColor(.gray)
.padding()
}
餘弦函數()
// .background(座標軸(y: false))
.frame(height: 200)
.border(Color.orange)
}
}
PlaygroundPage.current.setLiveView(畫布())
在「滾動軌跡」的Canvas中,我們一共畫了5個元素: