ArcPath.swift
7.0 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
//
// ArcPath.swift
// CustomArcApiDemo
//
// Created by DR_Kun on 2020/5/16.
// Copyright © 2020 kun. All rights reserved.
//
import Foundation
import UIKit
extension UIBezierPath {
func addHumenArc(withCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) {
let rotationAdjustment = CGFloat(Double.pi * 0.5)
let modifiedStart = startAngle - rotationAdjustment
let modifiedEnd = endAngle - rotationAdjustment
addArc(withCenter: center, radius: radius, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: clockwise)
}
func addRotationArc(withCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool, rotationAngle: CGFloat = .pi / 2) {
let modifiedStart = startAngle - rotationAngle
let modifiedEnd = endAngle - rotationAngle
addArc(withCenter: center, radius: radius, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: clockwise)
}
}
extension UIBezierPath {
/// Internal principle: Determine the position of the center of the circle through the `start` point, `end` point and `angle`.
/// And then draw the arc according to `clockwise`
///
/// Note: After the arc is drawn, the `move(to:)` method will be called to move to the `end` point.
/// - Parameters:
/// - start: The start point of the arc. (Note: This may not be the start point for drawing)
/// - end: The end point of the arc. (Note: This may not be the end point of the drawing)
/// - angle: The number of radians of the arc corresponding to the start point and end point;value must be between 0...2π
/// - clockwise: When the value is true, draw from start to end, when the value is false, draw from end to start
func addArc(startPoint start: CGPoint, endPoint end: CGPoint, angle: Double, clockwise: Bool) {
guard start != end && (angle >= 0 && angle <= 2 * Double.pi) else {
return
}
if angle == 0 {
move(to: start)
addLine(to: end)
return
}
var tmpStart = start, tmpEnd = end, tmpAngle = angle
// Note: 保证计算圆心时是从 start 到 end 小于 π 的角
if tmpAngle > Double.pi {
tmpAngle = 2 * Double.pi - tmpAngle
(tmpStart, tmpEnd) = (tmpEnd, tmpStart)
}
if !clockwise {
(tmpStart, tmpEnd) = (tmpEnd, tmpStart)
}
let center = calculateCenterFor(startPoint: tmpStart, endPoint: tmpEnd, radian: tmpAngle)
let radius = calculateLineLength(start, center)
var startAngle = calculateAngle(point: start, origin: center)
var endAngle = calculateAngle(point: end, origin: center)
// 如果逆时针绘制,则交换 startAngle 和 endAngle
if !clockwise {
(startAngle, endAngle) = (endAngle, startAngle)
move(to: end)
}
addArc(withCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true)
move(to: end)
}
}
// MARK: 计算
extension UIBezierPath {
// Woring: 只计算从start到end **顺时针** 计算对应的 **小于π** 圆弧对应的圆心
// Note: 计算逆时针(end到start)可以看做将传入的start和end对调后计算顺时针时的圆心位置
// Note: 计算大于π的叫相当于将end和start对换后计算2π-angle的顺时针圆心位置
func calculateCenterFor(startPoint start: CGPoint, endPoint end: CGPoint, radian: Double) -> CGPoint {
guard radian <= Double.pi else {
fatalError("Does not support radian calculations greater than π!")
}
guard start != end else {
fatalError("Start position and end position cannot be equal!")
}
if radian == Double.pi {
let centerX = (end.x - start.x) * 0.5 + start.x
let centerY = (end.y - start.y) * 0.5 + start.y
return CGPoint(x: centerX, y: centerY)
}
let lineAB = calculateLineLength(start, end)
// 平行 Y 轴
if start.x == end.x {
let centerY = (end.y - start.y) * 0.5 + start.y
let tanResult = CGFloat(tan(radian * 0.5))
let offsetX = lineAB * 0.5 / tanResult
let centerX = start.x + offsetX * (start.y > end.y ? 1.0 : -1.0)
return CGPoint(x: centerX, y: centerY)
}
// 平行 X 轴
if start.y == end.y {
let centerX = (end.x - start.x) * 0.5 + start.x
let tanResult = CGFloat(tan(radian * 0.5))
let offsetY = lineAB * 0.5 / tanResult
let centerY = start.y + offsetY * (start.x < end.x ? 1.0 : -1.0)
return CGPoint(x: centerX, y: centerY)
}
// 普通情况
// 计算半径
let radius = lineAB * 0.5 / CGFloat(sin(radian * 0.5))
// 计算与 Y 轴的夹角
let angleToYAxis = atan(abs(start.x - end.x) / abs(start.y - end.y))
let cacluteAngle = CGFloat(Double.pi - radian) * 0.5 - angleToYAxis
// 偏移量
let offsetX = radius * sin(cacluteAngle)
let offsetY = radius * cos(cacluteAngle)
var centetX = end.x
var centerY = end.y
// 以 start 为原点判断象限区间
if end.x > start.x && end.y < start.y {
// 第一象限
centetX = end.x + offsetX
centerY = end.y + offsetY
} else if end.x > start.x && end.y > start.y {
// 第二象限
centetX = start.x - offsetX
centerY = start.y + offsetY
} else if end.x < start.x && end.y > start.y {
// 第三象限
centetX = end.x - offsetX
centerY = end.y - offsetY
} else if end.x < start.x && end.y < start.y {
// 第四象限
centetX = start.x + offsetX
centerY = start.y - offsetY
}
return CGPoint(x: centetX, y: centerY)
}
func calculateAngle(point: CGPoint, origin: CGPoint) -> Double {
if point.y == origin.y {
return point.x > origin.x ? 0.0 : -Double.pi
}
if point.x == origin.x {
return point.y > origin.y ? Double.pi * 0.5 : Double.pi * -0.5
}
// Note: 修正标准坐标系角度到 iOS 坐标系
let rotationAdjustment = Double.pi * 0.5
let offsetX = point.x - origin.x
let offsetY = point.y - origin.y
// 使用 -offsetY 是因为 iOS 坐标系与标准坐标系的区别
if offsetY > 0 {
return Double(atan(offsetX / -offsetY)) + rotationAdjustment
} else {
return Double(atan(offsetX / -offsetY)) - rotationAdjustment
}
}
func calculateLineLength(_ point1: CGPoint, _ point2: CGPoint) -> CGFloat {
let w = point1.x - point2.x
let h = point1.y - point2.y
return sqrt(w * w + h * h)
}
}