diff --git a/Package.swift b/Package.swift index c2e2592..1ef13fa 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.3 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/README.md b/README.md index 2d54b85..a1241a1 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,8 @@ Collection of custom shapes ```swift -Pentagon() -Hexagon() RegularPolygon(sides: 32) +RoundedRegularPolygon(sides: 6, radius: 20) ``` ## Lines and Curves @@ -42,18 +41,10 @@ GridPattern(horizontalLines: 20, verticalLines: 40) ## How to use -Add this swift package to your project +Add this swift package to your project with SPM ``` -git@github.com:swift-extensions/swiftui-shapes.git +git@github.com:spacenation/swiftui-shapes.git ``` -For more examples open `/Examples/ShapesExamples.xcodeproj` - -## Roadmap -- Rounded regular polygons - ## Code Contributions Feel free to contribute via fork/pull request to master branch. If you want to request a feature or report a bug please start a new issue. - -## Coffee Contributions -If you find this project useful please consider becoming my GitHub sponsor. diff --git a/Sources/Shapes/RegularPolygons/Decagon.swift b/Sources/Shapes/RegularPolygons/Decagon.swift deleted file mode 100644 index 09ade6e..0000000 --- a/Sources/Shapes/RegularPolygons/Decagon.swift +++ /dev/null @@ -1,37 +0,0 @@ -import SwiftUI - -public struct Decagon: InsettableShape { - let inset: CGFloat - let radius: CGFloat - - public func inset(by amount: CGFloat) -> Decagon { - Decagon(inset: self.inset + amount, radius: radius) - } - - public func path(in rect: CGRect) -> Path { - Path.regularPolygon(sides: 10, in: rect, inset: inset, radius: radius) - } - - public init() { - inset = 0 - radius = 0 - } - - public init(radius: CGFloat) { - self.inset = 0 - self.radius = radius - } -} - -extension Decagon { - init(inset: CGFloat, radius: CGFloat) { - self.inset = inset - self.radius = radius - } -} - -struct Decagon_Previews: PreviewProvider { - static var previews: some View { - Decagon() - } -} diff --git a/Sources/Shapes/RegularPolygons/Heptagon.swift b/Sources/Shapes/RegularPolygons/Heptagon.swift deleted file mode 100644 index 826efa9..0000000 --- a/Sources/Shapes/RegularPolygons/Heptagon.swift +++ /dev/null @@ -1,37 +0,0 @@ -import SwiftUI - -public struct Heptagon: InsettableShape { - let inset: CGFloat - let radius: CGFloat - - public func inset(by amount: CGFloat) -> Heptagon { - Heptagon(inset: self.inset + amount, radius: radius) - } - - public func path(in rect: CGRect) -> Path { - Path.regularPolygon(sides: 7, in: rect, inset: inset, radius: radius) - } - - public init() { - inset = 0 - radius = 0 - } - - public init(radius: CGFloat) { - self.inset = 0 - self.radius = radius - } -} - -extension Heptagon { - init(inset: CGFloat, radius: CGFloat) { - self.inset = inset - self.radius = radius - } -} - -struct Heptagon_Previews: PreviewProvider { - static var previews: some View { - Heptagon() - } -} diff --git a/Sources/Shapes/RegularPolygons/Hexagon.swift b/Sources/Shapes/RegularPolygons/Hexagon.swift deleted file mode 100644 index beb0933..0000000 --- a/Sources/Shapes/RegularPolygons/Hexagon.swift +++ /dev/null @@ -1,37 +0,0 @@ -import SwiftUI - -public struct Hexagon: InsettableShape { - let inset: CGFloat - let radius: CGFloat - - public func inset(by amount: CGFloat) -> Hexagon { - Hexagon(inset: self.inset + amount, radius: radius) - } - - public func path(in rect: CGRect) -> Path { - Path.regularPolygon(sides: 6, in: rect, inset: inset, radius: radius) - } - - public init() { - inset = 0 - radius = 0 - } - - public init(radius: CGFloat) { - self.inset = 0 - self.radius = radius - } -} - -extension Hexagon { - init(inset: CGFloat, radius: CGFloat) { - self.inset = inset - self.radius = radius - } -} - -struct Hexagon_Previews: PreviewProvider { - static var previews: some View { - Hexagon() - } -} diff --git a/Sources/Shapes/RegularPolygons/Nonagon.swift b/Sources/Shapes/RegularPolygons/Nonagon.swift deleted file mode 100644 index aadae2a..0000000 --- a/Sources/Shapes/RegularPolygons/Nonagon.swift +++ /dev/null @@ -1,37 +0,0 @@ -import SwiftUI - -public struct Nonagon: InsettableShape { - let inset: CGFloat - let radius: CGFloat - - public func inset(by amount: CGFloat) -> Nonagon { - Nonagon(inset: self.inset + amount, radius: radius) - } - - public func path(in rect: CGRect) -> Path { - Path.regularPolygon(sides: 9, in: rect, inset: inset, radius: radius) - } - - public init() { - inset = 0 - radius = 0 - } - - public init(radius: CGFloat) { - self.inset = 0 - self.radius = radius - } -} - -extension Nonagon { - init(inset: CGFloat, radius: CGFloat) { - self.inset = inset - self.radius = radius - } -} - -struct Nonagon_Previews: PreviewProvider { - static var previews: some View { - Nonagon() - } -} diff --git a/Sources/Shapes/RegularPolygons/Octagon.swift b/Sources/Shapes/RegularPolygons/Octagon.swift deleted file mode 100644 index b8a909b..0000000 --- a/Sources/Shapes/RegularPolygons/Octagon.swift +++ /dev/null @@ -1,37 +0,0 @@ -import SwiftUI - -public struct Octagon: InsettableShape { - let inset: CGFloat - let radius: CGFloat - - public func inset(by amount: CGFloat) -> Octagon { - Octagon(inset: self.inset + amount, radius: radius) - } - - public func path(in rect: CGRect) -> Path { - Path.regularPolygon(sides: 8, in: rect, inset: inset, radius: radius) - } - - public init() { - inset = 0 - radius = 0 - } - - public init(radius: CGFloat) { - self.inset = 0 - self.radius = radius - } -} - -extension Octagon { - init(inset: CGFloat, radius: CGFloat) { - self.inset = inset - self.radius = radius - } -} - -struct Octagon_Previews: PreviewProvider { - static var previews: some View { - Octagon() - } -} diff --git a/Sources/Shapes/RegularPolygons/Pentagon.swift b/Sources/Shapes/RegularPolygons/Pentagon.swift deleted file mode 100644 index 8fb56df..0000000 --- a/Sources/Shapes/RegularPolygons/Pentagon.swift +++ /dev/null @@ -1,37 +0,0 @@ -import SwiftUI - -public struct Pentagon: InsettableShape { - let inset: CGFloat - let radius: CGFloat - - public func inset(by amount: CGFloat) -> Pentagon { - Pentagon(inset: self.inset + amount, radius: radius) - } - - public func path(in rect: CGRect) -> Path { - Path.regularPolygon(sides: 5, in: rect, inset: inset, radius: radius) - } - - public init() { - inset = 0 - radius = 0 - } - - public init(radius: CGFloat) { - self.inset = 0 - self.radius = radius - } -} - -extension Pentagon { - init(inset: CGFloat, radius: CGFloat) { - self.inset = inset - self.radius = radius - } -} - -struct Pentagon_Previews: PreviewProvider { - static var previews: some View { - Pentagon() - } -} diff --git a/Sources/Shapes/RegularPolygons/RegularPolygon.swift b/Sources/Shapes/RegularPolygons/RegularPolygon.swift index 88a4aa0..55844ed 100644 --- a/Sources/Shapes/RegularPolygons/RegularPolygon.swift +++ b/Sources/Shapes/RegularPolygons/RegularPolygon.swift @@ -1,64 +1,43 @@ import SwiftUI -public struct RegularPolygon: InsettableShape { - let sides: Int - let inset: CGFloat - let radius: CGFloat - - public func inset(by amount: CGFloat) -> RegularPolygon { - RegularPolygon(sides: self.sides, inset: self.inset + amount, radius: radius) +public struct RegularPolygon: Shape { + let sides: UInt + private let inset: CGFloat + + public func path(in rect: CGRect) -> Path { + Path.regularPolygon(sides: self.sides, in: rect, inset: inset) } - public func path(in rect: CGRect) -> Path { - Path.regularPolygon(sides: self.sides, in: rect, inset: inset, radius: radius) + public init(sides: UInt) { + self.init(sides: sides, inset: 0) } - public init(sides: Int, radius: CGFloat = 0) { + init(sides: UInt, inset: CGFloat) { self.sides = sides - self.inset = 0 - self.radius = radius - } - - public init(sides: Double, radius: CGFloat = 0) { - self.sides = Int(sides.rounded(.down)) - self.inset = 0 - self.radius = radius + self.inset = inset } } -extension RegularPolygon { - init(sides: Int, inset: CGFloat, radius: CGFloat = 0) { - self.sides = sides - self.inset = inset - self.radius = radius +extension RegularPolygon: InsettableShape { + public func inset(by amount: CGFloat) -> RegularPolygon { + RegularPolygon(sides: self.sides, inset: self.inset + amount) } } struct RegularPolygon_Previews: PreviewProvider { static var previews: some View { Group { - RegularPolygon(sides: 4, radius: 5) + RegularPolygon(sides: 4) .strokeBorder(lineWidth: 20) .foregroundColor(.blue) - Pentagon(radius: 5) + RegularPolygon(sides: 6) .strokeBorder(lineWidth: 20) - .foregroundColor(.yellow) - - Hexagon() - .foregroundColor(.orange) - - Heptagon(radius: 5) - .foregroundColor(.blue) - - Octagon() - .foregroundColor(.pink) - - Nonagon() .foregroundColor(.red) - Decagon() - .foregroundColor(.green) + RegularPolygon(sides: 16) + .strokeBorder(lineWidth: 10) + .foregroundColor(.purple) } } } diff --git a/Sources/Shapes/RegularPolygons/RegularPolygonPath.swift b/Sources/Shapes/RegularPolygons/RegularPolygonPath.swift index 85d88bd..26a01fb 100644 --- a/Sources/Shapes/RegularPolygons/RegularPolygonPath.swift +++ b/Sources/Shapes/RegularPolygons/RegularPolygonPath.swift @@ -1,80 +1,23 @@ import SwiftUI extension Path { - static func regularPolygon(sides: Int, in rect: CGRect, inset: CGFloat = 0, radius: CGFloat = 0) -> Path { + static func regularPolygon(sides: UInt, in rect: CGRect, inset: CGFloat = 0) -> Path { let width = rect.size.width - inset * 2 let height = rect.size.height - inset * 2 let hypotenuse = Double(min(width, height)) / 2.0 let centerPoint = CGPoint(x: width / 2.0, y: height / 2.0) - var testDistance: CGFloat = .zero - var usableRadius: CGFloat = .zero - - + return Path { path in (0...sides).forEach { index in let angle = ((Double(index) * (360.0 / Double(sides))) - 90) * Double.pi / 180 - - //control point let point = CGPoint( x: centerPoint.x + CGFloat(cos(angle) * hypotenuse), y: centerPoint.y + CGFloat(sin(angle) * hypotenuse) ) - - //the angle from the target control point to the next control point - let nextAngle = ((Double(index + 1) * (360.0 / Double(sides))) - 90) * Double.pi / 180 - - //coordinates of the next control point - let nextPoint = CGPoint( - x: centerPoint.x + CGFloat(cos(nextAngle) * hypotenuse), - y: centerPoint.y + CGFloat(sin(nextAngle) * hypotenuse) - ) - - if testDistance == .zero { - //The distance between two neighboring endpoints on your polygon - testDistance = sqrt(pow(( nextPoint.x - point.x ), 2) + pow(( nextPoint.y - point.y ), 2)) - - //Ensures that our 'radius' won't exceed a length of half our polygonside - usableRadius = radius > testDistance / 2 ? testDistance / 2 : radius - } - - //source point - let currentPoint = index == 0 ? point : path.currentPoint! - - //distance from source point to target control point - let distance = sqrt(pow(( point.x - currentPoint.x ), 2) + pow(( point.y - currentPoint.y ), 2)) - - //distance from target control point to the start of the curve we want to draw - let distanceToCurveStart = index == 0 ? usableRadius : distance - usableRadius - - //angle from current point to the target control point - let angleToCurveStart = index == 0 ? 0 : atan2((point.y - currentPoint.y), (point.x - currentPoint.x)) - - //coordinates of where to start the curve - let curveStartPoint = CGPoint( - x: currentPoint.x + (distanceToCurveStart * CGFloat(cos(angleToCurveStart))), - y: currentPoint.y + (distanceToCurveStart * CGFloat(sin(angleToCurveStart))) - ) - - //angle from current control point to next control point - let angleToCurveEnd = atan2((nextPoint.y - point.y), (nextPoint.x - point.x)) - - //coordinates of where the curve shuold end - let curveEndPoint = CGPoint( - x: point.x + (usableRadius * CGFloat(cos(angleToCurveEnd))), - y: point.y + (usableRadius * CGFloat(sin(angleToCurveEnd))) - ) - if index == 0 { - let altStartPoint = CGPoint( - x: point.x + (usableRadius * CGFloat(cos(angleToCurveEnd))), - y: point.y + (usableRadius * CGFloat(sin(angleToCurveEnd))) - ) - path.move(to: radius == 0 ? point : altStartPoint) + path.move(to: point) } else { - path.addLine(to: radius > 0 ? curveStartPoint : point) - if radius > 0 { - path.addQuadCurve(to: curveEndPoint, control: point) - } + path.addLine(to: point) } } path.closeSubpath() diff --git a/Sources/Shapes/RoundedRegularPolygons/RoundedRegularPolygon.swift b/Sources/Shapes/RoundedRegularPolygons/RoundedRegularPolygon.swift new file mode 100644 index 0000000..3bfc185 --- /dev/null +++ b/Sources/Shapes/RoundedRegularPolygons/RoundedRegularPolygon.swift @@ -0,0 +1,45 @@ +import SwiftUI + +public struct RoundedRegularPolygon: Shape { + let sides: UInt + let radius: CGFloat + private let inset: CGFloat + + public func path(in rect: CGRect) -> Path { + Path.roundedRegularPolygon(sides: self.sides, in: rect, inset: inset, radius: radius) + } + + public init(sides: UInt, radius: CGFloat) { + self.init(sides: sides, radius: radius, inset: 0) + } + + init(sides: UInt, radius: CGFloat, inset: CGFloat) { + self.sides = sides + self.radius = radius + self.inset = inset + } +} + +extension RoundedRegularPolygon: InsettableShape { + public func inset(by amount: CGFloat) -> RoundedRegularPolygon { + RoundedRegularPolygon(sides: self.sides, radius: radius, inset: self.inset + amount) + } +} + +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) + } + } +} diff --git a/Sources/Shapes/RoundedRegularPolygons/RoundedRegularPolygonPath.swift b/Sources/Shapes/RoundedRegularPolygons/RoundedRegularPolygonPath.swift new file mode 100644 index 0000000..365188f --- /dev/null +++ b/Sources/Shapes/RoundedRegularPolygons/RoundedRegularPolygonPath.swift @@ -0,0 +1,84 @@ +import SwiftUI + +extension Path { + static func roundedRegularPolygon(sides: UInt, in rect: CGRect, inset: CGFloat = 0, radius: CGFloat = 0) -> Path { + let width = rect.size.width - inset * 2 + let height = rect.size.height - inset * 2 + let hypotenuse = Double(min(width, height)) / 2.0 + let centerPoint = CGPoint(x: width / 2.0, y: height / 2.0) + var testDistance: CGFloat = .zero + var usableRadius: CGFloat = .zero + + + return Path { path in + (0...sides).forEach { index in + let angle = ((Double(index) * (360.0 / Double(sides))) - 90) * Double.pi / 180 + + //control point + let point = CGPoint( + x: centerPoint.x + CGFloat(cos(angle) * hypotenuse), + y: centerPoint.y + CGFloat(sin(angle) * hypotenuse) + ) + + //the angle from the target control point to the next control point + let nextAngle = ((Double(index + 1) * (360.0 / Double(sides))) - 90) * Double.pi / 180 + + //coordinates of the next control point + let nextPoint = CGPoint( + x: centerPoint.x + CGFloat(cos(nextAngle) * hypotenuse), + y: centerPoint.y + CGFloat(sin(nextAngle) * hypotenuse) + ) + + if testDistance == .zero { + //The distance between two neighboring endpoints on your polygon + testDistance = sqrt(pow(( nextPoint.x - point.x ), 2) + pow(( nextPoint.y - point.y ), 2)) + + //Ensures that our 'radius' won't exceed a length of half our polygonside + usableRadius = radius > testDistance / 2 ? testDistance / 2 : radius + } + + //source point + let currentPoint = index == 0 ? point : path.currentPoint! + + //distance from source point to target control point + let distance = sqrt(pow(( point.x - currentPoint.x ), 2) + pow(( point.y - currentPoint.y ), 2)) + + //distance from target control point to the start of the curve we want to draw + let distanceToCurveStart = index == 0 ? usableRadius : distance - usableRadius + + //angle from current point to the target control point + let angleToCurveStart = index == 0 ? 0 : atan2((point.y - currentPoint.y), (point.x - currentPoint.x)) + + //coordinates of where to start the curve + let curveStartPoint = CGPoint( + x: currentPoint.x + (distanceToCurveStart * CGFloat(cos(angleToCurveStart))), + y: currentPoint.y + (distanceToCurveStart * CGFloat(sin(angleToCurveStart))) + ) + + //angle from current control point to next control point + let angleToCurveEnd = atan2((nextPoint.y - point.y), (nextPoint.x - point.x)) + + //coordinates of where the curve shuold end + let curveEndPoint = CGPoint( + x: point.x + (usableRadius * CGFloat(cos(angleToCurveEnd))), + y: point.y + (usableRadius * CGFloat(sin(angleToCurveEnd))) + ) + + if index == 0 { + let altStartPoint = CGPoint( + x: point.x + (usableRadius * CGFloat(cos(angleToCurveEnd))), + y: point.y + (usableRadius * CGFloat(sin(angleToCurveEnd))) + ) + path.move(to: radius == 0 ? point : altStartPoint) + } else { + path.addLine(to: radius > 0 ? curveStartPoint : point) + if radius > 0 { + path.addQuadCurve(to: curveEndPoint, control: point) + } + } + } + path.closeSubpath() + } + .offsetBy(dx: inset, dy: inset) + } +}