還記得在第1課(4-1a)一開始學習用Animation物件產生動畫效果時,需要兩個步驟,第一步是指定一個「時間曲線」來產出Animation物件實例,第二步再用全域函式 withAnimation() 或修飾語 .animation() 來啟動動畫效果。
第一步驟所用的時間曲線,背後就是以「三階貝茲曲線」來定義,根據曲線的不同,產生節奏有快有慢的動畫效果。本節就用畫布 Canvas 與 addCurve() 來畫出各種時間曲線,這需要用到一個簡單的數學技巧:正規化。
所謂正規化(Normalization)或稱一般化,其實就是將某些條件標準化。例如用比例的概念,將(x, y)座標範圍限定在 0 到 1 之間,如 (0.5, 0.5) 代表視框寬、高各一半的位置,也就是中心點。這樣不管畫布實際寬高尺寸多大,都能夠用正規化座標來表示整個畫框範圍,如下圖:
正規化的座標值(x, y)就相當於寬高比例,所以要從正規化座標還原回螢幕座標,算法就是乘以寬、高,並將正規化(數學座標)以「左下角」為原點,轉回以螢幕「左上角」為原點。如上圖所示。
這種方法在向量繪圖中特別有用,因為(x, y)座標值只需指定0到1之間的實數,就能縮放到任何尺寸的螢幕或視框之中。因此不管是 Apple 的系統圖示(SF Symbols)或是網頁SVG向量圖格式,都可使用正規化座標。
三階貝茲曲線也可以正規化,通常將起點設為(0, 0),終點設為(1, 1),所以只要再指定兩個控制點的正規化座標,就可決定一條曲線。例如 Animation.default 用的曲線稱為 easeInOut (緩入緩出),兩個控制點的座標分為是 (0.42, 0.0) 與 (0.58, 1.0),這樣是不是更方便,且通用性更高!
以下範例程式參考某網站的數據,列出12種時間曲線。不同的時間曲線,差別只在於兩個控制點的正規化座標不同而已:
// 4-7b 時間曲線
// Created by Heman, 2022/05/12
import PlaygroundSupport
import SwiftUI
struct 三階貝茲曲線: View {
var unitX1 = 0.0 // unitX = x/寬
var unitY1 = 1.0 // unitY = y/高
var unitX2 = 0.0
var unitY2 = 1.0
var 說明 = "三階貝茲曲線"
var body: some View {
Canvas { 圖層, 尺寸 in
let 寬 = 尺寸.width
let 高 = 尺寸.height
let 左上角 = CGPoint.zero
let 左下角 = CGPoint(x: 0, y: 高)
let 右上角 = CGPoint(x: 寬, y: 0)
let 控制點1 = CGPoint( // 以左下角為原點的數學座標
x: 寬 * unitX1, // 轉換為螢幕座標(左上角為原點)
y: 高 - 高 * unitY1)
let 控制點2 = CGPoint(
x: 寬 * unitX2,
y: 高 - 高 * unitY2)
var 畫筆 = Path() // 對角線
畫筆.move(to: 左下角)
畫筆.addLine(to: 右上角)
圖層.stroke(畫筆, with: .color(.gray))
畫筆 = Path()
for 圓心 in [控制點1, 控制點2, 左下角, 右上角] {
畫筆.move(to: 圓心)
畫筆.addArc( // 小圓點
center: 圓心,
radius: 5,
startAngle: .zero,
endAngle: .degrees(360),
clockwise: false)
}
圖層.fill(畫筆, with: .color(.cyan))
畫筆 = Path() // 三階貝茲曲線
畫筆.move(to: 左下角)
畫筆.addCurve(to: 右上角, control1: 控制點1, control2: 控制點2)
圖層.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: 文字框)
}
}
}
// Reference to <https://easings.net/> for parameters of timing curves.
struct 畫布: View {
let 長寬 = 125.0
var body: some View {
VStack {
Label("[SwiftUI]4-7b 時間曲線", systemImage: "swift")
.font(.title)
.foregroundColor(.orange)
.padding()
HStack {
三階貝茲曲線(unitX1: 0.42, unitY1: 0.0, unitX2: 1.0, unitY2: 1.0, 說明: "easeIn")
.frame(width: 長寬, height: 長寬)
.border(.red) // easeIn
三階貝茲曲線(unitX1: 0.0, unitY1: 0.0, unitX2: 0.58, unitY2: 1.0, 說明: "easeOut")
.frame(width: 長寬, height: 長寬)
.border(.red) // easeOut
三階貝茲曲線(unitX1: 0.42, unitY1: 0.0, unitX2: 0.58, unitY2: 1.0, 說明: "easeInOut")
.frame(width: 長寬, height: 長寬)
.border(.red) // easeInOut
}
HStack {
三階貝茲曲線(unitX1: 0.32, unitY1: 0.0, unitX2: 0.67, unitY2: 0.0, 說明: "easeInCubic")
.frame(width: 長寬, height: 長寬)
.border(.red) // easeInCubic
三階貝茲曲線(unitX1: 0.33, unitY1: 1.0, unitX2: 0.68, unitY2: 1.0, 說明: "easeOutCubic")
.frame(width: 長寬, height: 長寬)
.border(.red) // easeOutCubic
三階貝茲曲線(unitX1: 0.65, unitY1: 0.0, unitX2: 0.35, unitY2: 1.0, 說明: "easeInOutCubic")
.frame(width: 長寬, height: 長寬)
.border(.red) // easeInOutCubic
}
HStack {
三階貝茲曲線(unitX1: 0.64, unitY1: 0.0, unitX2: 0.78, unitY2: 0.0, 說明: "easeInQuint")
.frame(width: 長寬, height: 長寬)
.border(.red) // easeInQuint
三階貝茲曲線(unitX1: 0.22, unitY1: 1.0, unitX2: 0.36, unitY2: 1.0, 說明: "easeOutQuint")
.frame(width: 長寬, height: 長寬)
.border(.red) // easeOutQuint
三階貝茲曲線(unitX1: 0.83, unitY1: 0.0, unitX2: 0.17, unitY2: 1.0, 說明: "easeInOutQuint")
.frame(width: 長寬, height: 長寬)
.border(.red) // easeInOutQuint
}
HStack {
三階貝茲曲線(unitX1: 0.55, unitY1: 0.0, unitX2: 1.0, unitY2: 0.45, 說明: "easeInCirc")
.frame(width: 長寬, height: 長寬)
.border(.red) // easeInCirc
三階貝茲曲線(unitX1: 0.0, unitY1: 0.55, unitX2: 0.45, unitY2: 1.0, 說明: "easeOutCirc")
.frame(width: 長寬, height: 長寬)
.border(.red) // easeOutCirc
三階貝茲曲線(unitX1: 0.85, unitY1: 0.0, unitX2: 0.15, unitY2: 1.0, 說明: "easeInOutCirc")
.frame(width: 長寬, height: 長寬)
.border(.red) // easeInOutCirc
}
}
}
}
// PlaygroundPage.current.setLiveView(三階貝茲曲線(unitX1: 0.0, unitY1: 0.75, unitX2: 0.25, unitY2: 1.0))
PlaygroundPage.current.setLiveView(畫布())
執行結果顯示出來的時間曲線如下圖:
下次如果想自己定義 Animation 時間曲線,應該就知道怎麼做了。如下例:
let 動畫效果 = Animation.timingCurve(0.85, 0, 0.15, 1, duration: 1.0)