Skip to content

Commit

Permalink
Merge pull request #14 from spacenation/rounded-regular-polygons
Browse files Browse the repository at this point in the history
Rounded polygons
  • Loading branch information
ay42 authored Jan 25, 2022
2 parents 8cfb557 + 9a7b72e commit 66864cd
Show file tree
Hide file tree
Showing 11 changed files with 397 additions and 99 deletions.
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ let package = Package(
.library(name: "Shapes", targets: ["Shapes"])
],
targets: [
.target(name: "Shapes", dependencies: [])
.target(name: "Shapes", dependencies: []),
.testTarget(name: "ShapesTests", dependencies: ["Shapes"])
]
)
18 changes: 18 additions & 0 deletions Sources/Shapes/CGPoint+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ public extension CGPoint {
}
}

public extension CGPoint {
static func intersection(start1: CGPoint, end1: CGPoint, start2: CGPoint, end2: CGPoint) -> CGPoint {
let x1 = start1.x
let y1 = start1.y
let x2 = end1.x
let y2 = end1.y
let x3 = start2.x
let y3 = start2.y
let x4 = end2.x
let y4 = end2.y

let intersectionX: CGFloat = ((x1*y2-y1*x2)*(x3-x4) - (x1-x2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4))
let intersectionY: CGFloat = ((x1*y2-y1*x2)*(y3-y4) - (y1-y2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4))

return CGPoint(x: intersectionX, y: intersectionY)
}
}

public extension Collection where Element == UnitPoint {
func points(in rect: CGRect) -> [CGPoint] {
self.map { CGPoint(unitPoint: $0, in: rect) }
Expand Down
2 changes: 1 addition & 1 deletion Sources/Shapes/Lines/QuadCurve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import SwiftUI

public struct QuadCurve: Shape {
let unitPoints: [UnitPoint]

public func path(in rect: CGRect) -> Path {
Path { path in
path.addQuadCurves(self.unitPoints.points(in: rect))
Expand Down
31 changes: 31 additions & 0 deletions Sources/Shapes/Path+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import SwiftUI

public extension Path {
mutating func addCircularCornerRadiusArc(from point: CGPoint, via viaPoint: CGPoint, to nextPoint: CGPoint, radius: CGFloat, clockwise: Bool) {
let lineAngle = atan2(viaPoint.y - point.y, viaPoint.x - point.x)
let nextLineAngle = atan2(nextPoint.y - viaPoint.y, nextPoint.x - viaPoint.x)

let lineVector = CGVector(dx: -sin(lineAngle) * radius, dy: cos(lineAngle) * radius)
let nextLineVector = CGVector(dx: -sin(nextLineAngle) * radius, dy: cos(nextLineAngle) * radius)

let offsetStart1 = CGPoint(x: point.x + lineVector.dx, y: point.y + lineVector.dy)
let offsetEnd1 = CGPoint(x: viaPoint.x + lineVector.dx, y: viaPoint.y + lineVector.dy)

let offsetStart2 = CGPoint(x: viaPoint.x + nextLineVector.dx, y: viaPoint.y + nextLineVector.dy)

let offsetEnd2 = CGPoint(x: nextPoint.x + nextLineVector.dx, y: nextPoint.y + nextLineVector.dy)

let intersection = CGPoint.intersection(start1: offsetStart1, end1: offsetEnd1, start2: offsetStart2, end2: offsetEnd2)

let startAngle = lineAngle - (.pi / 2)
let endAngle = nextLineAngle - (.pi / 2)

self.addArc(
center: intersection,
radius: radius,
startAngle: Angle(radians: startAngle),
endAngle: Angle(radians:endAngle),
clockwise: clockwise
)
}
}
62 changes: 62 additions & 0 deletions Sources/Shapes/RegularPolygons/Triangle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import SwiftUI

struct Triangle: Shape {
let radius: CGFloat

func path(in rect: CGRect) -> Path {
let width = rect.width
let height = rect.height
let sides = 3
let hypotenuse = Double(min(width, height)) / 2.0
let centerPoint = CGPoint(x: rect.midX, y: rect.midY)
var usableRadius: CGFloat = .zero

return Path { path in
guard sides > 2 else { return }
(0..<sides).forEach { index in

let angle = ((Double(index) * (360.0 / Double(sides))) - 90) * Double.pi / 180

let point = CGPoint(
x: centerPoint.x + CGFloat(cos(angle) * hypotenuse),
y: centerPoint.y + CGFloat(sin(angle) * hypotenuse)
)

let viaAngle = ((Double(index + 1) * (360.0 / Double(sides))) - 90) * Double.pi / 180

let viaPoint = CGPoint(
x: centerPoint.x + CGFloat(cos(viaAngle) * hypotenuse),
y: centerPoint.y + CGFloat(sin(viaAngle) * hypotenuse)
)

if usableRadius == 0 {
let sideLength = sqrt((point.x - viaPoint.x) * (point.x - viaPoint.x) + (point.y - viaPoint.y) * (point.y - viaPoint.y))
let inradius = sideLength / (2 * tan(.pi / CGFloat(sides)))

usableRadius = min(radius, inradius)
}

let nextAngle = ((Double(index + 2) * (360.0 / Double(sides))) - 90) * Double.pi / 180

let nextPoint = CGPoint(
x: centerPoint.x + CGFloat(cos(nextAngle) * hypotenuse),
y: centerPoint.y + CGFloat(sin(nextAngle) * hypotenuse)
)

path.addCircularCornerRadiusArc(from: point, via: viaPoint, to: nextPoint, radius: usableRadius, clockwise: false)
}
path.closeSubpath()
}
}
}

struct Triangle_Previews: PreviewProvider {
static var previews: some View {
Triangle(radius: 30)
.stroke(lineWidth: 3)
.foregroundColor(.blue)
.background(Circle())
.animation(.linear)
.previewLayout(.fixed(width: 200, height: 200))
}
}
75 changes: 62 additions & 13 deletions Sources/Shapes/RoundedRegularPolygons/RoundedRegularPolygon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,69 @@ extension RoundedRegularPolygon: InsettableShape {
}
}

extension Path {
static func roundedRegularPolygon(sides: UInt, in rect: CGRect, inset: CGFloat = 0, radius: CGFloat = 0) -> Path {
let rect = rect.insetBy(dx: inset, dy: inset)
let width = rect.width
let height = rect.height
let hypotenuse = Double(min(width, height)) / 2.0
let centerPoint = CGPoint(x: rect.midX, y: rect.midY)
var usableRadius: CGFloat = .zero

return Path { path in
guard sides > 2 else { return }
(0..<sides).forEach { index in

let angle = ((Double(index) * (360.0 / Double(sides))) - 90) * Double.pi / 180

let point = CGPoint(
x: centerPoint.x + CGFloat(cos(angle) * hypotenuse),
y: centerPoint.y + CGFloat(sin(angle) * hypotenuse)
)

let viaAngle = ((Double(index + 1) * (360.0 / Double(sides))) - 90) * Double.pi / 180

let viaPoint = CGPoint(
x: centerPoint.x + CGFloat(cos(viaAngle) * hypotenuse),
y: centerPoint.y + CGFloat(sin(viaAngle) * hypotenuse)
)

if usableRadius == 0 {
let sideLength = sqrt((point.x - viaPoint.x) * (point.x - viaPoint.x) + (point.y - viaPoint.y) * (point.y - viaPoint.y))
let inradius = sideLength / (2 * tan(.pi / CGFloat(sides)))

usableRadius = min(radius, inradius)
}

let nextAngle = ((Double(index + 2) * (360.0 / Double(sides))) - 90) * Double.pi / 180

let nextPoint = CGPoint(
x: centerPoint.x + CGFloat(cos(nextAngle) * hypotenuse),
y: centerPoint.y + CGFloat(sin(nextAngle) * hypotenuse)
)

path.addCircularCornerRadiusArc(from: point, via: viaPoint, to: nextPoint, radius: usableRadius, clockwise: false)
}
path.closeSubpath()
}
}
}


struct RoundedRegularPolygon_Previews: PreviewProvider {
static var previews: some View {
Group {
RoundedRegularPolygon(sides: 4, radius: 40)
.strokeBorder(lineWidth: 20)
.foregroundColor(.blue)
RoundedRegularPolygon(sides: 6, radius: 20)
.strokeBorder(lineWidth: 10)
.foregroundColor(.yellow)
RoundedRegularPolygon(sides: 16, radius: 10)
.strokeBorder(lineWidth: 20)
.foregroundColor(.green)
}
RoundedRegularPolygon(sides: 3, radius: 30)
.fill(LinearGradient(gradient: Gradient(colors: [.orange, .red]), startPoint: .topLeading, endPoint: .bottomTrailing))
.previewLayout(.fixed(width: 200, height: 200))

RoundedRegularPolygon(sides: 6, radius: 20)
.strokeBorder(lineWidth: 20)
.foregroundColor(.yellow)
.previewLayout(.fixed(width: 200, height: 200))

RoundedRegularPolygon(sides: 16, radius: 10)
.strokeBorder(lineWidth: 20)
.foregroundColor(.green)
.previewLayout(.fixed(width: 200, height: 200))
}
}

This file was deleted.

Loading

0 comments on commit 66864cd

Please sign in to comment.