如同圓周運動可以衍伸出很多漂亮又有趣的圖案,滾動也是如此。如果將滾動時,頂點的軌跡記錄下來,會是什麼形狀呢?

這種奇特的軌跡稱為「擺線」(cycloid):

4-6c4.gif

不過,本節要利用滾動軌跡來畫另一個軌跡 — 「餘弦函數」(cosine, 簡寫為cos),因為cos是三角函數中,最具代表性的函數,只要熟悉cos函數,其他函數都可以用cos導出。甚至著名的傅立葉轉換,還可將任意波形轉換為餘弦函數的序列組合,換句話說,只用餘弦函數,就能夠表達所有的波形,厲害吧。

要形成餘弦曲線,我們要記錄的是下圖中的「垂足」軌跡,這是頂點至圓心垂直線的垂足,頂點、垂足、圓心形成一個直角三角形,如下圖:

截圖 2022-05-08 下午9.18.54.png

知道如何計算垂足座標之後,怎麼畫出垂足的軌跡呢?應該是從上次垂足座標到目前垂足座標之間畫一線段,但問題是怎麼保存上次垂足座標呢?這個問題困擾筆者好幾天,試過幾種方法都不成功,最後不得不使用一個全域變數來解決。

完整範例程式如下:

// 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個元素:

  1. 大圓
  2. 頂點小圓
  3. 垂足小圓
  4. 直角三角形