一、基本术语
- 三角是关于直角三角形的边和角之间的关系,可以给它们取个随意的名字,以确保我们都能理解对方,这些名称并不相关,可以选择任意名称,本文名称定义如下:
- 直角三角形:或者简单的直角三角形,它是一个三角形,其中有一个角是 90 度;
-
- Hypotenuse(斜边):它是直角三角形中最大的边,也是对着直角的边;
-
-
- Opposed Leg:相对于其中一个角度,它是没有“接触”它的那个,在上图中, leg(a) 与 (alpha) 相反,leg(b) 与 (beta) 相反;
-
- Adjacent Leg:相对于其中一个角,它是接触它的那个角,在上图中,leg(a) 毗邻 (beta),leg(g) 毗邻(alpha)。
二、度和弧度
- 我们都知道度,如果我告诉你,给我一个 90 度的角,你可能马上就知道该怎么做。但如果我说,给一个 1.5708 弧度的角,那么你还知道怎么做吗?其实,它们都指向同一个角,角度和弧度是测量它们的两种不同尺度,这两个单元之间的转换非常简单:
- π 是 Pi 的符号,如果需要从角度转换到弧度,那么需要:
- 如下所示,是一个可以创建的扩展,以便在两个单位之间转换:
extension Double {
var asDegrees: Double { return self * 180 / .pi }
var asRadians: Double { return self * .pi / 180 }
}
let radAngle: Double = 2.0
print("radAngle radians= \(radAngle.asDegrees) degrees")
let degAngle: Double = 180
print("degAngle degrees = \(degAngle.asRadians) radians")
- SwiftUI 有一个名为 Angle 的类型,带有一些方便的初始化式和计算属性:
let a = Angle(degrees: 180)
let b = Angle(radians: 2.3456)
print("\(a.radians) radians = \(a.degrees) degrees")
print("\(b.radians) radians = \(b.degrees) degrees")
- 为什么会复杂化呢?degrees 不是更容易使用吗?也许对我们来说是这样,但数学上,弧度是有意义的,如果你有兴趣了解原因,可以浏览参考:Why Radians?。我们需要知道弧度,因为在 Swift 中的三角函数需要用弧度来指定角度。
三、三角函数的作用
- 正如我们看到的,给定一个直角三角形,可以从其他三角形推导出一些值。例如,如果知道斜边和其中一个角,就可以得到腿和其他角的大小。如果知道两条边,你就能得到斜边和角等。
- 我们为什么需要这个呢?如果开始考虑三角形的顶点(A, B 和 C),在你的视图中作为 CGPoints,那么一切都比较清晰了。给定两个 CGPoint,可以计算从一个到另一个的方向(角度)(例如,对一个视图旋转效果很有用)。给定两个点的 x、y 坐标,可以得到它们之间的距离(斜边),给定一个 CGPoint 的距离和方向,可以获得第二个 CGPoint 坐标等。
- 三角函数的另一个应用,是当你需要一个函数来平滑一个效果,一个距离,一个颜色,或者任何可以用数字表示的东西。
四、正弦,余弦和正切是什么?
- 除了三个基本的三角函数之外,还应该知道反函数(反正弦,反余弦和反正切)。例如,如果一个角度 的正弦值为 x,那么 x 的反正弦值为 :
- 由这些公式,我们可以推断出其余的一切,当需要某个没有的值时,看看其他知道的值,然后选择正确的公式就行了。对于任何值,需要知道两条边,或者一条边和一个角,所有的计算组合都在下表中,如下图所示:
- 这就是我们需要的全部数学,让我们看看如何获得两个 CGPoint 之间的距离和方向,然后给定一个点坐标,一个距离和一个方向的情况下,如何计算第二个点。我们要用它们来画一个多边形,来看看到如何应用相同的想法,以绘制一个形状,如花朵显示在文章的顶部,最后使用 sin() 函数来平滑值的输入或输出。
五、角度和方向
- 如果你有两个任意的点,我们称它们为 pt1 和 pt2,我们将得到这两个点之间的距离和方向:
- 使用 SwiftUI,这段代码将得到两点之间的方向和距离:
func getDistanceAndDirection(_ pt1: CGPoint, _ pt2: CGPoint) -> (distance: CGFloat, angle: Angle) {
let a = pt2.y - pt1.y
let b = pt2.x - pt1.x
var alpha = atan2(a, b)
let s = sin(alpha)
let h = (a == 0 ? abs(b) : (a / s))
alpha = alpha < 0 ? alpha + (.pi * 2) : alpha
return (h, Angle(radians: Double(alpha)))
}
- 看这两点构成的三角形,距离与三角形的斜边相匹配,用 arctan 计算角度。
- 在给定的方向和角度下获得第二个点:
-
- 如果知道一个点的坐标 (pt1),给定一个方向和长度,那么如何获得第二个点 (pt2)?
-
- 在这个例子中,创建一个形状来绘制一条线,给定一个 CGPoint,一个角度和一个距离:
Line(pt1: CGPoint(x: 100, y: 300), direction: Angle(degrees: 25), length: 300)
.stroke(Color.blue, lineWidth: 2)
.frame(width: 400, height: 400)
struct Line: Shape {
let pt1: CGPoint
let direction: Angle
let length: CGFloat
func path(in rect: CGRect) -> Path {
let x = pt1.x + length * CGFloat(cos(direction.radians))
let y = pt1.y - length * CGFloat(sin(direction.radians))
let pt2 = CGPoint(x: x, y: y)
var p = Path()
p.move(to: pt1)
p.addLine(to: pt2)
return p
}
}
六、绘制一个多边形
- 接下来,来创建一个形状来绘制一个正多边形,这里将使用一个七边多边形,代码几乎类似,可以创建任意数量的边:
- 一个多边形有许多顶点,想要得到相应的坐标,这样就可以画出连接它们的线。如下图所示,所有顶点到圆周中心的距离都相同:
- 如前所述,在两点之间,您总是可以创建一个直角三角形。在某些情况下,会形成一个三角形,其中一条边的长度为 0,而另一条边的长度为斜边,想象一个三角形,其中一条边收缩,直到它的长度为零。在这种情况下,正弦值为 0,余弦值为 1,反之亦然。三角函数可以很好地处理这个问题,其中一种情况,就是顶部的顶点(90 度角),cos(90) = 0 sin(90) = 1:
- 如果我们定义了多边形的中心和周长的半径,就可以得到所有的顶点,每个顶点的角度由多边形的边数决定。三角函数的美妙之处在于,它们可以处理大于 90 度的角,(在某些情况下)返回负值,这很好地满足了我们的绘图要求。例如,在上面的第二个三角形中,余弦值是负的,这意味着顶点的 x 坐标将小于圆周中心的 x 坐标,正是需要的:
struct PolygonShape: Shape {
var sides: Int
func path(in rect: CGRect) -> Path {
let h = Double(min(rect.size.width, rect.size.height)) / 2.0
let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0)
var path = Path()
for i in 0..<sides {
let angle = (Double(i) * (360.0 / Double(sides))) * Double.pi / 180
let pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h))
if i == 0 {
path.move(to: pt)
} else {
path.addLine(to: pt)
}
}
path.closeSubpath()
return path
}
}
- 我们随时都可以看到三角形,例如,在下面的花中,每个花瓣都由两条曲线组成;要画一条曲线,需要一个起点、一个终点和一个控制点,我们能用这三个点做什么呢?可以用来制作半瓣的三个点,另一半是对称的:
七、平滑进,平滑出
- 正弦(或余弦)函数有一个特点,我们看它的图形,可以注意到图形的形状重复,它的最小值是 -1,最大值是 1,f(x) 开始缓慢增长,然后稳定,然后再次缓慢增长:
- 如果稍微改变函数,为了移动和压缩图,我们得到一对理想的波,平滑地增加和减少一个任意值:
- 当然,还有其他方法可以实现平滑值,但这是一个值得提及的简单方法。可以使用这个功能淡入淡出几乎任何东西:声音音量、定位、移动、颜色、缩放等。如下所示,创建具有渐进缩放值的文本:
struct ContentView: View {
var body: some View {
ProgressiveText(text: "AAAAAAAA")
}
}
struct ProgressiveText: View {
let text: String
var body: some View {
HStack(spacing: 10) {
ForEach(Array(text.enumerated()), id: \.0) { (n, ch) in
Text(String(ch)).font(.largeTitle).fontWeight(.bold).scaleEffect(self.scaleValue(n, self.text.count))
}
}
}
func scaleValue(_ idx: Int, _ totalCharacters: Int) -> CGFloat {
let x = Double(idx) / Double(totalCharacters)
let y = (sin(2 * .pi * x - (.pi / 2)) + 1) / 2.0
return 1 + 2 * CGFloat(y)
}
}