Skip to content

Commit 66864cd

Browse files
authored
Merge pull request #14 from spacenation/rounded-regular-polygons
Rounded polygons
2 parents 8cfb557 + 9a7b72e commit 66864cd

File tree

11 files changed

+397
-99
lines changed

11 files changed

+397
-99
lines changed

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ let package = Package(
1212
.library(name: "Shapes", targets: ["Shapes"])
1313
],
1414
targets: [
15-
.target(name: "Shapes", dependencies: [])
15+
.target(name: "Shapes", dependencies: []),
16+
.testTarget(name: "ShapesTests", dependencies: ["Shapes"])
1617
]
1718
)

Sources/Shapes/CGPoint+Extensions.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@ public extension CGPoint {
2828
}
2929
}
3030

31+
public extension CGPoint {
32+
static func intersection(start1: CGPoint, end1: CGPoint, start2: CGPoint, end2: CGPoint) -> CGPoint {
33+
let x1 = start1.x
34+
let y1 = start1.y
35+
let x2 = end1.x
36+
let y2 = end1.y
37+
let x3 = start2.x
38+
let y3 = start2.y
39+
let x4 = end2.x
40+
let y4 = end2.y
41+
42+
let intersectionX: CGFloat = ((x1*y2-y1*x2)*(x3-x4) - (x1-x2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4))
43+
let intersectionY: CGFloat = ((x1*y2-y1*x2)*(y3-y4) - (y1-y2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4))
44+
45+
return CGPoint(x: intersectionX, y: intersectionY)
46+
}
47+
}
48+
3149
public extension Collection where Element == UnitPoint {
3250
func points(in rect: CGRect) -> [CGPoint] {
3351
self.map { CGPoint(unitPoint: $0, in: rect) }

Sources/Shapes/Lines/QuadCurve.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import SwiftUI
22

33
public struct QuadCurve: Shape {
44
let unitPoints: [UnitPoint]
5-
5+
66
public func path(in rect: CGRect) -> Path {
77
Path { path in
88
path.addQuadCurves(self.unitPoints.points(in: rect))

Sources/Shapes/Path+Extensions.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import SwiftUI
2+
3+
public extension Path {
4+
mutating func addCircularCornerRadiusArc(from point: CGPoint, via viaPoint: CGPoint, to nextPoint: CGPoint, radius: CGFloat, clockwise: Bool) {
5+
let lineAngle = atan2(viaPoint.y - point.y, viaPoint.x - point.x)
6+
let nextLineAngle = atan2(nextPoint.y - viaPoint.y, nextPoint.x - viaPoint.x)
7+
8+
let lineVector = CGVector(dx: -sin(lineAngle) * radius, dy: cos(lineAngle) * radius)
9+
let nextLineVector = CGVector(dx: -sin(nextLineAngle) * radius, dy: cos(nextLineAngle) * radius)
10+
11+
let offsetStart1 = CGPoint(x: point.x + lineVector.dx, y: point.y + lineVector.dy)
12+
let offsetEnd1 = CGPoint(x: viaPoint.x + lineVector.dx, y: viaPoint.y + lineVector.dy)
13+
14+
let offsetStart2 = CGPoint(x: viaPoint.x + nextLineVector.dx, y: viaPoint.y + nextLineVector.dy)
15+
16+
let offsetEnd2 = CGPoint(x: nextPoint.x + nextLineVector.dx, y: nextPoint.y + nextLineVector.dy)
17+
18+
let intersection = CGPoint.intersection(start1: offsetStart1, end1: offsetEnd1, start2: offsetStart2, end2: offsetEnd2)
19+
20+
let startAngle = lineAngle - (.pi / 2)
21+
let endAngle = nextLineAngle - (.pi / 2)
22+
23+
self.addArc(
24+
center: intersection,
25+
radius: radius,
26+
startAngle: Angle(radians: startAngle),
27+
endAngle: Angle(radians:endAngle),
28+
clockwise: clockwise
29+
)
30+
}
31+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import SwiftUI
2+
3+
struct Triangle: Shape {
4+
let radius: CGFloat
5+
6+
func path(in rect: CGRect) -> Path {
7+
let width = rect.width
8+
let height = rect.height
9+
let sides = 3
10+
let hypotenuse = Double(min(width, height)) / 2.0
11+
let centerPoint = CGPoint(x: rect.midX, y: rect.midY)
12+
var usableRadius: CGFloat = .zero
13+
14+
return Path { path in
15+
guard sides > 2 else { return }
16+
(0..<sides).forEach { index in
17+
18+
let angle = ((Double(index) * (360.0 / Double(sides))) - 90) * Double.pi / 180
19+
20+
let point = CGPoint(
21+
x: centerPoint.x + CGFloat(cos(angle) * hypotenuse),
22+
y: centerPoint.y + CGFloat(sin(angle) * hypotenuse)
23+
)
24+
25+
let viaAngle = ((Double(index + 1) * (360.0 / Double(sides))) - 90) * Double.pi / 180
26+
27+
let viaPoint = CGPoint(
28+
x: centerPoint.x + CGFloat(cos(viaAngle) * hypotenuse),
29+
y: centerPoint.y + CGFloat(sin(viaAngle) * hypotenuse)
30+
)
31+
32+
if usableRadius == 0 {
33+
let sideLength = sqrt((point.x - viaPoint.x) * (point.x - viaPoint.x) + (point.y - viaPoint.y) * (point.y - viaPoint.y))
34+
let inradius = sideLength / (2 * tan(.pi / CGFloat(sides)))
35+
36+
usableRadius = min(radius, inradius)
37+
}
38+
39+
let nextAngle = ((Double(index + 2) * (360.0 / Double(sides))) - 90) * Double.pi / 180
40+
41+
let nextPoint = CGPoint(
42+
x: centerPoint.x + CGFloat(cos(nextAngle) * hypotenuse),
43+
y: centerPoint.y + CGFloat(sin(nextAngle) * hypotenuse)
44+
)
45+
46+
path.addCircularCornerRadiusArc(from: point, via: viaPoint, to: nextPoint, radius: usableRadius, clockwise: false)
47+
}
48+
path.closeSubpath()
49+
}
50+
}
51+
}
52+
53+
struct Triangle_Previews: PreviewProvider {
54+
static var previews: some View {
55+
Triangle(radius: 30)
56+
.stroke(lineWidth: 3)
57+
.foregroundColor(.blue)
58+
.background(Circle())
59+
.animation(.linear)
60+
.previewLayout(.fixed(width: 200, height: 200))
61+
}
62+
}

Sources/Shapes/RoundedRegularPolygons/RoundedRegularPolygon.swift

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,69 @@ extension RoundedRegularPolygon: InsettableShape {
2626
}
2727
}
2828

29+
extension Path {
30+
static func roundedRegularPolygon(sides: UInt, in rect: CGRect, inset: CGFloat = 0, radius: CGFloat = 0) -> Path {
31+
let rect = rect.insetBy(dx: inset, dy: inset)
32+
let width = rect.width
33+
let height = rect.height
34+
let hypotenuse = Double(min(width, height)) / 2.0
35+
let centerPoint = CGPoint(x: rect.midX, y: rect.midY)
36+
var usableRadius: CGFloat = .zero
37+
38+
return Path { path in
39+
guard sides > 2 else { return }
40+
(0..<sides).forEach { index in
41+
42+
let angle = ((Double(index) * (360.0 / Double(sides))) - 90) * Double.pi / 180
43+
44+
let point = CGPoint(
45+
x: centerPoint.x + CGFloat(cos(angle) * hypotenuse),
46+
y: centerPoint.y + CGFloat(sin(angle) * hypotenuse)
47+
)
48+
49+
let viaAngle = ((Double(index + 1) * (360.0 / Double(sides))) - 90) * Double.pi / 180
50+
51+
let viaPoint = CGPoint(
52+
x: centerPoint.x + CGFloat(cos(viaAngle) * hypotenuse),
53+
y: centerPoint.y + CGFloat(sin(viaAngle) * hypotenuse)
54+
)
55+
56+
if usableRadius == 0 {
57+
let sideLength = sqrt((point.x - viaPoint.x) * (point.x - viaPoint.x) + (point.y - viaPoint.y) * (point.y - viaPoint.y))
58+
let inradius = sideLength / (2 * tan(.pi / CGFloat(sides)))
59+
60+
usableRadius = min(radius, inradius)
61+
}
62+
63+
let nextAngle = ((Double(index + 2) * (360.0 / Double(sides))) - 90) * Double.pi / 180
64+
65+
let nextPoint = CGPoint(
66+
x: centerPoint.x + CGFloat(cos(nextAngle) * hypotenuse),
67+
y: centerPoint.y + CGFloat(sin(nextAngle) * hypotenuse)
68+
)
69+
70+
path.addCircularCornerRadiusArc(from: point, via: viaPoint, to: nextPoint, radius: usableRadius, clockwise: false)
71+
}
72+
path.closeSubpath()
73+
}
74+
}
75+
}
76+
77+
2978
struct RoundedRegularPolygon_Previews: PreviewProvider {
3079
static var previews: some View {
31-
Group {
32-
RoundedRegularPolygon(sides: 4, radius: 40)
33-
.strokeBorder(lineWidth: 20)
34-
.foregroundColor(.blue)
35-
36-
RoundedRegularPolygon(sides: 6, radius: 20)
37-
.strokeBorder(lineWidth: 10)
38-
.foregroundColor(.yellow)
39-
40-
RoundedRegularPolygon(sides: 16, radius: 10)
41-
.strokeBorder(lineWidth: 20)
42-
.foregroundColor(.green)
43-
}
80+
RoundedRegularPolygon(sides: 3, radius: 30)
81+
.fill(LinearGradient(gradient: Gradient(colors: [.orange, .red]), startPoint: .topLeading, endPoint: .bottomTrailing))
82+
.previewLayout(.fixed(width: 200, height: 200))
83+
84+
RoundedRegularPolygon(sides: 6, radius: 20)
85+
.strokeBorder(lineWidth: 20)
86+
.foregroundColor(.yellow)
87+
.previewLayout(.fixed(width: 200, height: 200))
88+
89+
RoundedRegularPolygon(sides: 16, radius: 10)
90+
.strokeBorder(lineWidth: 20)
91+
.foregroundColor(.green)
92+
.previewLayout(.fixed(width: 200, height: 200))
4493
}
4594
}

Sources/Shapes/RoundedRegularPolygons/RoundedRegularPolygonPath.swift

Lines changed: 0 additions & 84 deletions
This file was deleted.

0 commit comments

Comments
 (0)