Skip to content

Commit 78db318

Browse files
committed
Add basic BezierPath
1 parent e588e79 commit 78db318

4 files changed

Lines changed: 370 additions & 0 deletions

File tree

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
2+
public struct BezierPath: Hashable, BezierPathRepresentable, Codable, Sendable {
3+
public var isEmpty: Bool { elements.isEmpty }
4+
5+
public func enumerate(_ block: (Element) -> Void) {
6+
for element in elements {
7+
block(element)
8+
}
9+
}
10+
11+
private static let circleCoefficient = 0.55228475
12+
13+
public enum Element: Hashable, Codable, Sendable {
14+
case move(to: Point)
15+
case line(to: Point)
16+
case curve(to: Point, control1: Point, control2: Point)
17+
case closeSubpath
18+
}
19+
20+
public private(set) var elements: [Element]
21+
22+
public init() {
23+
elements = []
24+
}
25+
26+
public init(elements: [Element]) {
27+
self.elements = elements
28+
}
29+
30+
public init<B: BezierPathRepresentable>(_ representable: B) {
31+
elements = []
32+
representable.enumerate { element in
33+
elements.append(element)
34+
}
35+
}
36+
37+
public init(rect: Rect) {
38+
elements = [
39+
.move(to: .init(x: rect.minX, y: rect.minY)),
40+
.line(to: .init(x: rect.maxX, y: rect.minY)),
41+
.line(to: .init(x: rect.maxX, y: rect.maxY)),
42+
.line(to: .init(x: rect.minX, y: rect.maxY)),
43+
.closeSubpath
44+
]
45+
}
46+
47+
public init(ellipseIn rect: Rect) {
48+
let xControlOffset = Self.circleCoefficient * (rect.width / 2.0)
49+
let yControlOffset = Self.circleCoefficient * (rect.height / 2.0)
50+
elements = [
51+
.move(to: .init(x: rect.maxX, y: rect.midY)),
52+
.curve(to: .init(x: rect.midX, y: rect.maxY),
53+
control1: .init(x: rect.maxX, y: rect.midY + yControlOffset),
54+
control2: .init(x: rect.midX + xControlOffset, y: rect.maxY)),
55+
.curve(to: .init(x: rect.minX, y: rect.midY),
56+
control1: .init(x: rect.midX - xControlOffset, y: rect.maxY),
57+
control2: .init(x: rect.minX, y: rect.midY + yControlOffset)),
58+
.curve(to: .init(x: rect.midX, y: rect.minY),
59+
control1: .init(x: rect.minX, y: rect.midY - yControlOffset),
60+
control2: .init(x: rect.midX - xControlOffset, y: rect.minY)),
61+
.curve(to: .init(x: rect.maxX, y: rect.midY),
62+
control1: .init(x: rect.midX + xControlOffset, y: rect.minY),
63+
control2: .init(x: rect.maxX, y: rect.midY - yControlOffset)),
64+
.closeSubpath
65+
]
66+
}
67+
68+
public init(roundedRect rect: Rect, cornerRadius: Real) {
69+
self.init(roundedRect: rect, cornerSize: .init(width: cornerRadius, height: cornerRadius))
70+
}
71+
72+
public init(roundedRect rect: Rect, cornerSize: Size) {
73+
let controlOffset = cornerSize * Self.circleCoefficient
74+
let flatSideLength = (rect.size - (cornerSize * 2.0)) / 2.0
75+
76+
elements = [
77+
.move(to: .init(x: rect.maxX, y: rect.midY)),
78+
.line(to: .init(x: rect.maxX, y: rect.midY + flatSideLength.height)),
79+
.curve(to: .init(x: rect.maxX - cornerSize.width, y: rect.maxY),
80+
control1: .init(x: rect.maxX, y: rect.midY + flatSideLength.height + controlOffset.height),
81+
control2: .init(x: rect.maxX - cornerSize.width + controlOffset.width, y: rect.maxY )),
82+
.line(to: .init(x: rect.minX + cornerSize.width, y: rect.maxY)),
83+
.curve(to: .init(x: rect.minX, y: rect.midY + flatSideLength.height),
84+
control1: .init(x: rect.minX + cornerSize.width - controlOffset.width, y: rect.maxY),
85+
control2: .init(x: rect.minX, y: rect.midY + flatSideLength.height + controlOffset.height)),
86+
.line(to: .init(x: rect.minX, y: rect.midY - flatSideLength.height)),
87+
.curve(to: .init(x: rect.minX + cornerSize.width, y: rect.minY),
88+
control1: .init(x: rect.minX, y: rect.midY - flatSideLength.height - controlOffset.height),
89+
control2: .init(x: rect.minX + cornerSize.width - controlOffset.width, y: rect.minY)),
90+
.line(to: .init(x: rect.maxX - cornerSize.width, y: rect.minY)),
91+
.curve(to: .init(x: rect.maxX, y: rect.midY - flatSideLength.height),
92+
control1: .init(x: rect.maxX - cornerSize.width + controlOffset.width, y: rect.minY),
93+
control2: .init(x: rect.maxX, y: rect.midY - flatSideLength.height - controlOffset.height)),
94+
.closeSubpath
95+
]
96+
}
97+
98+
99+
public init(end1: Point, control1: Point, control2: Point, end2: Point) {
100+
elements = [
101+
.move(to: end1),
102+
.curve(to: end2, control1: control1, control2: control2)
103+
]
104+
}
105+
106+
public init(_ builder: (inout BezierPath) -> Void) {
107+
var empty = BezierPath()
108+
builder(&empty)
109+
self.elements = empty.elements
110+
}
111+
112+
public mutating func move(to point: Point) {
113+
elements.append(.move(to: point))
114+
}
115+
116+
public mutating func addCurve(to point: Point, controlPoint1: Point, controlPoint2: Point) {
117+
elements.append(.curve(to: point, control1: controlPoint1, control2: controlPoint2))
118+
}
119+
120+
public mutating func addLine(to point: Point) {
121+
elements.append(.line(to: point))
122+
}
123+
124+
public mutating func closeSubpath() {
125+
elements.append(.closeSubpath)
126+
}
127+
128+
public mutating func append<B: BezierPathRepresentable>(contentsOf bezier: B) {
129+
bezier.enumerate { element in
130+
elements.append(element)
131+
}
132+
}
133+
134+
public var count: Int {
135+
elements.count
136+
}
137+
138+
public mutating func transform(_ transform: BaseKit.Transform) {
139+
elements = elements.map { $0.transform(transform) }
140+
}
141+
142+
public func reversed() -> BezierPath {
143+
var subpaths = [[BezierPath.Element]]()
144+
var currentSubpath = [BezierPath.Element]()
145+
for element in elements {
146+
switch element {
147+
case .move:
148+
if !currentSubpath.isEmpty {
149+
subpaths.append(currentSubpath)
150+
currentSubpath = []
151+
}
152+
currentSubpath.append(element)
153+
case .line, .curve:
154+
currentSubpath.append(element)
155+
case .closeSubpath:
156+
currentSubpath.append(element)
157+
subpaths.append(currentSubpath)
158+
currentSubpath = []
159+
}
160+
}
161+
162+
if !currentSubpath.isEmpty {
163+
subpaths.append(currentSubpath)
164+
currentSubpath = []
165+
}
166+
167+
var result = BezierPath()
168+
for subpath in subpaths {
169+
result.appendReverse(subpath)
170+
}
171+
return result
172+
}
173+
174+
public static let easeInEaseOut = BezierPath(end1: .init(x: 0, y: 0),
175+
control1: .init(x: 0.2, y: 0),
176+
control2: .init(x: 0.8, y: 1),
177+
end2: .init(x: 1, y: 1))
178+
179+
}
180+
181+
extension BezierPath.Element {
182+
func transform(_ transform: BaseKit.Transform) -> BezierPath.Element {
183+
switch self {
184+
case let .move(to: point):
185+
return .move(to: point.applying(transform))
186+
case let .line(to: point):
187+
return .line(to: point.applying(transform))
188+
case let .curve(to: point, control1: controlPoint1, control2: controlPoint2):
189+
return .curve(to: point.applying(transform),
190+
control1: controlPoint1.applying(transform),
191+
control2: controlPoint2.applying(transform))
192+
case .closeSubpath:
193+
return .closeSubpath
194+
}
195+
}
196+
}
197+
198+
public extension BezierPath.Element {
199+
var endPoint: Point? {
200+
switch self {
201+
case let .move(to: point):
202+
return point
203+
case let .line(to: point):
204+
return point
205+
case let .curve(to: point, control1: _, control2: _):
206+
return point
207+
case .closeSubpath:
208+
return nil
209+
}
210+
}
211+
212+
var controlPoint1: Point? {
213+
if case let .curve(to: _, control1: control1, control2: _) = self {
214+
return control1
215+
} else {
216+
return nil
217+
}
218+
}
219+
220+
var controlPoint2: Point? {
221+
if case let .curve(to: _, control1: _, control2: control2) = self {
222+
return control2
223+
} else {
224+
return nil
225+
}
226+
}
227+
}
228+
229+
extension BezierPath: Swift.Sequence {
230+
public func makeIterator() -> AnyIterator<Element> {
231+
var i = elements.startIndex
232+
return AnyIterator { () -> Element? in
233+
guard i >= elements.startIndex && i < elements.endIndex else {
234+
return nil
235+
}
236+
let value = elements[i]
237+
i += 1
238+
return value
239+
}
240+
}
241+
}
242+
243+
func convertQuadToCubic(from currentPoint: Point, controlPoint: Point, to endPoint: Point) -> (controlPoint1: Point, controlPoint2: Point, endPoint: Point) {
244+
// Create a cubic curve representation of the quadratic curve from
245+
let: Real = 2.0 / 3.0
246+
247+
// lastPoint + twoThirds * (via - lastPoint)
248+
let controlPoint1 = currentPoint + ((controlPoint - currentPoint) *)
249+
// toPt + twoThirds * (via - toPt)
250+
let controlPoint2 = endPoint + ((controlPoint - endPoint) *)
251+
252+
return (controlPoint1: controlPoint1, controlPoint2: controlPoint2, endPoint: endPoint)
253+
}
254+
255+
private extension BezierPath {
256+
257+
mutating func appendReverse(_ subpath: [BezierPath.Element]) {
258+
let isClosed = subpath.last == .closeSubpath
259+
260+
let reversedElements = (isClosed ? subpath.dropLast() : subpath).reversed()
261+
var previousElement: BezierPath.Element?
262+
for element in reversedElements {
263+
guard let elementToProcess = previousElement else {
264+
if let endPoint = element.endPoint {
265+
move(to: endPoint)
266+
}
267+
previousElement = element
268+
continue
269+
}
270+
switch elementToProcess {
271+
case .move:
272+
break // TODO: can we get here?
273+
274+
case .line:
275+
if let endPoint = element.endPoint {
276+
addLine(to: endPoint)
277+
}
278+
279+
case let .curve(to: _, control1: control1, control2: control2):
280+
if let endPoint = element.endPoint {
281+
addCurve(to: endPoint, controlPoint1: control2, controlPoint2: control1)
282+
}
283+
284+
case .closeSubpath:
285+
break // Shouldn't get here
286+
}
287+
288+
previousElement = element
289+
}
290+
291+
if isClosed {
292+
closeSubpath()
293+
}
294+
}
295+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
/// This protocol contains displayable attributes like NSBezierPath or UIBezierPath
3+
/// might have.
4+
public protocol BezierPathRenderable: BezierPathRepresentable {
5+
var fillRule: FillRule { get set }
6+
var strokeLineWidth: Real { get set }
7+
var strokeLineCap: LineCap { get set }
8+
var strokeLineJoin: LineJoin { get set }
9+
var miterLimit: Real { get set }
10+
var flatness: Real { get set }
11+
12+
mutating func setLineDash(_ pattern: [Real], phase: Real)
13+
}
14+
15+
extension BezierPathRenderable {
16+
mutating func copyAttributes<B: BezierPathRenderable>(from other: B) {
17+
fillRule = other.fillRule
18+
strokeLineWidth = other.strokeLineWidth
19+
strokeLineCap = other.strokeLineCap
20+
strokeLineJoin = other.strokeLineJoin
21+
miterLimit = other.miterLimit
22+
flatness = other.flatness
23+
}
24+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
2+
public protocol BezierPathRepresentable {
3+
init()
4+
5+
var isEmpty: Bool { get }
6+
7+
mutating func move(to point: Point)
8+
mutating func addCurve(to point: Point, controlPoint1: Point, controlPoint2: Point)
9+
mutating func addLine(to point: Point)
10+
mutating func closeSubpath()
11+
12+
func enumerate(_ block: (BezierPath.Element) -> Void)
13+
14+
mutating func transform(_ transform: BaseKit.Transform)
15+
}
16+
17+
extension BezierPathRepresentable {
18+
19+
mutating func append<B: BezierPathRepresentable>(_ other: B) {
20+
other.enumerate { element in
21+
switch element {
22+
case let .move(to: point):
23+
move(to: point)
24+
case let .line(to: point):
25+
addLine(to: point)
26+
case let .curve(to: point, control1: control1, control2: control2):
27+
addCurve(to: point, controlPoint1: control1, controlPoint2: control2)
28+
case .closeSubpath:
29+
closeSubpath()
30+
}
31+
}
32+
}
33+
}
34+

Sources/BaseKit/Geometry/Point+Utils.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,23 @@ public extension Point {
8282
static prefix func - (point: Point) -> Point {
8383
Point(x: -point.x, y: -point.y)
8484
}
85+
86+
func applying(_ t: Transform) -> Point {
87+
// result[row, column] = p[row][0]*m[0][column] + p[row][1] * m[1][column] + p[row][2] * m[2][column]
88+
89+
let p = [[x, y, 1.0]]
90+
let m = [[t.a, t.b, 0.0],
91+
[t.c, t.d, 0.0],
92+
[t.translateX,t.translateY,1.0]]
93+
94+
let result = [
95+
p[0][0] * m[0][0] + p[0][1] * m[1][0] + p[0][2] * m[2][0],
96+
p[0][0] * m[0][1] + p[0][1] * m[1][1] + p[0][2] * m[2][1],
97+
p[0][0] * m[0][2] + p[0][1] * m[1][2] + p[0][2] * m[2][2],
98+
]
99+
100+
return Point(x: result[0], y: result[1])
101+
}
85102
}
86103

87104
/// The three points are a counter-clockwise turn if the return value is greater than 0,

0 commit comments

Comments
 (0)