├── .github └── FUNDING.yml ├── .gitignore ├── Resources ├── lines.png ├── patterns.png └── regularRectangles.png ├── Sources └── Shapes │ ├── Path │ ├── Path+AddLineFromTo.swift │ └── Path+QuadCurves.swift │ ├── RegularPolygons │ ├── RegularPolygonPath.swift │ └── RegularPolygon.swift │ ├── Lines │ ├── Line.swift │ └── QuadCurve.swift │ ├── StarPolygons │ ├── StarPolygonPath.swift │ └── StarPolygon.swift │ ├── Other │ └── HalfCapsule.swift │ ├── RoundedRegularPolygons │ ├── RoundedRegularPolygon.swift │ └── RoundedRegularPolygonPath.swift │ ├── CGPoint+Extensions.swift │ ├── Path+Extensions.swift │ ├── Patterns │ └── GridPattern.swift │ └── RoundedStarPolygons │ └── RoundedStarPolygon.swift ├── Package.swift ├── Tests └── ShapesTests │ └── ShapesTests.swift ├── LICENSE └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [spacenation] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | .swiftpm 7 | -------------------------------------------------------------------------------- /Resources/lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacenation/swiftui-shapes/HEAD/Resources/lines.png -------------------------------------------------------------------------------- /Resources/patterns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacenation/swiftui-shapes/HEAD/Resources/patterns.png -------------------------------------------------------------------------------- /Resources/regularRectangles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacenation/swiftui-shapes/HEAD/Resources/regularRectangles.png -------------------------------------------------------------------------------- /Sources/Shapes/Path/Path+AddLineFromTo.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension Path { 4 | mutating func addLine(from p1: CGPoint, to p2: CGPoint) { 5 | self.move(to: p1) 6 | self.addLine(to: p2) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Shapes", 8 | platforms: [ 9 | .iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6) 10 | ], 11 | products: [ 12 | .library(name: "Shapes", targets: ["Shapes"]) 13 | ], 14 | targets: [ 15 | .target(name: "Shapes", dependencies: []), 16 | .testTarget(name: "ShapesTests", dependencies: ["Shapes"]) 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /Tests/ShapesTests/ShapesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Shapes 3 | 4 | final class ShapesTests: XCTestCase { 5 | 6 | func testSlope() { 7 | let start = CGPoint(x: 0.0, y: 0.0) 8 | let end = CGPoint(x: 0, y: 100.0) 9 | let angle = atan2(end.y - start.y, end.x - start.x) 10 | 11 | XCTAssertEqual(angle, .pi / 2) 12 | } 13 | 14 | func testIntersection() { 15 | let start1 = CGPoint(x: 10.0, y: 0.0) 16 | let end1 = CGPoint(x: 10, y: 100.0) 17 | let start2 = CGPoint(x: 0.0, y: 20.0) 18 | let end2 = CGPoint(x: 100, y: 20) 19 | 20 | XCTAssertEqual( 21 | CGPoint.intersection(start1: start1, end1: end1, start2: start2, end2: end2), 22 | CGPoint(x: 10, y: 20) 23 | ) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Shapes/RegularPolygons/RegularPolygonPath.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Path { 4 | static func regularPolygon(sides: UInt, in rect: CGRect, inset: CGFloat = 0) -> Path { 5 | let width = rect.size.width - inset * 2 6 | let height = rect.size.height - inset * 2 7 | let hypotenuse = Double(min(width, height)) / 2.0 8 | let centerPoint = CGPoint(x: width / 2.0, y: height / 2.0) 9 | 10 | return Path { path in 11 | (0...sides).forEach { index in 12 | let angle = ((Double(index) * (360.0 / Double(sides))) - 90) * Double.pi / 180 13 | let point = CGPoint( 14 | x: centerPoint.x + CGFloat(cos(angle) * hypotenuse), 15 | y: centerPoint.y + CGFloat(sin(angle) * hypotenuse) 16 | ) 17 | if index == 0 { 18 | path.move(to: point) 19 | } else { 20 | path.addLine(to: point) 21 | } 22 | } 23 | path.closeSubpath() 24 | } 25 | .offsetBy(dx: inset, dy: inset) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SpaceNation Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/Shapes/Lines/Line.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct Line: Shape { 4 | private let unitPoints: [UnitPoint] 5 | 6 | public func path(in rect: CGRect) -> Path { 7 | Path { path in 8 | path.addLines(self.unitPoints.points(in: rect)) 9 | } 10 | } 11 | 12 | public init(unitPoints: [UnitPoint]) { 13 | self.unitPoints = unitPoints 14 | } 15 | } 16 | 17 | public extension Line { 18 | init(unitData: Data) where Data.Element : BinaryFloatingPoint { 19 | let step: CGFloat = unitData.count > 1 ? 1.0 / CGFloat(unitData.count - 1) : 1.0 20 | self.unitPoints = unitData.enumerated().map { (index, dataPoint) in UnitPoint(x: step * CGFloat(index), y: CGFloat(dataPoint)) } 21 | } 22 | } 23 | 24 | struct Line_Previews: PreviewProvider { 25 | static var previews: some View { 26 | Line(unitPoints: [ 27 | UnitPoint(x: 0.1, y: 0.1), 28 | UnitPoint(x: 0.5, y: 0.9), 29 | UnitPoint(x: 0.9, y: 0.1) 30 | ]) 31 | .stroke(Color.red, style: .init(lineWidth: 4, lineCap: .round)) 32 | .background(Color.black) 33 | .drawingGroup() 34 | .previewLayout(.fixed(width: 400, height: 300)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Shapes/StarPolygons/StarPolygonPath.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Path { 4 | static func starPolygon(points: UInt, in rect: CGRect, inset: CGFloat = 0, smoothness: CGFloat) -> Path { 5 | let sides = points * 2 6 | let width = rect.size.width - inset * 2 7 | let height = rect.size.height - inset * 2 8 | let hypotenuse = Double(min(width, height)) / 2.0 9 | let centerPoint = CGPoint(x: width / 2.0, y: height / 2.0) 10 | 11 | return Path { path in 12 | (0...sides).forEach { index in 13 | let isOdd = index.isMultiple(of: 2) 14 | let angle = ((Double(index) * (360.0 / Double(sides))) - 90) * Double.pi / 180 15 | let point = CGPoint( 16 | x: centerPoint.x + CGFloat(cos(angle) * (hypotenuse * (isOdd ? 1.0 : smoothness))), 17 | y: centerPoint.y + CGFloat(sin(angle) * (hypotenuse * (isOdd ? 1.0 : smoothness))) 18 | ) 19 | if index == 0 { 20 | path.move(to: point) 21 | } else { 22 | path.addLine(to: point) 23 | } 24 | } 25 | path.closeSubpath() 26 | } 27 | .offsetBy(dx: inset, dy: inset) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Shapes/Lines/QuadCurve.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct QuadCurve: Shape { 4 | let unitPoints: [UnitPoint] 5 | 6 | public func path(in rect: CGRect) -> Path { 7 | Path { path in 8 | path.addQuadCurves(self.unitPoints.points(in: rect)) 9 | } 10 | } 11 | 12 | public init(unitPoints: [UnitPoint]) { 13 | self.unitPoints = unitPoints 14 | } 15 | } 16 | 17 | public extension QuadCurve { 18 | init(unitData: Data) where Data.Element : BinaryFloatingPoint { 19 | let step: CGFloat = unitData.count > 1 ? 1.0 / CGFloat(unitData.count - 1) : 1.0 20 | self.unitPoints = unitData.enumerated().map { (index, dataPoint) in UnitPoint(x: step * CGFloat(index), y: CGFloat(dataPoint)) } 21 | } 22 | } 23 | 24 | struct QuadCurve_Previews: PreviewProvider { 25 | static var previews: some View { 26 | QuadCurve(unitPoints: [ 27 | UnitPoint(x: 0.1, y: 0.1), 28 | UnitPoint(x: 0.5, y: 0.9), 29 | UnitPoint(x: 0.9, y: 0.1) 30 | ]) 31 | .stroke(Color.red, style: .init(lineWidth: 4, lineCap: .round)) 32 | .background(Color.black) 33 | .drawingGroup() 34 | .previewLayout(.fixed(width: 400, height: 300)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Shapes/RegularPolygons/RegularPolygon.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct RegularPolygon: Shape { 4 | let sides: UInt 5 | private let inset: CGFloat 6 | 7 | public func path(in rect: CGRect) -> Path { 8 | Path.regularPolygon(sides: self.sides, in: rect, inset: inset) 9 | } 10 | 11 | public init(sides: UInt) { 12 | self.init(sides: sides, inset: 0) 13 | } 14 | 15 | init(sides: UInt, inset: CGFloat) { 16 | self.sides = sides 17 | self.inset = inset 18 | } 19 | } 20 | 21 | extension RegularPolygon: InsettableShape { 22 | public func inset(by amount: CGFloat) -> RegularPolygon { 23 | RegularPolygon(sides: self.sides, inset: self.inset + amount) 24 | } 25 | } 26 | 27 | struct RegularPolygon_Previews: PreviewProvider { 28 | static var previews: some View { 29 | Group { 30 | RegularPolygon(sides: 4) 31 | .strokeBorder(lineWidth: 20) 32 | .foregroundColor(.blue) 33 | 34 | RegularPolygon(sides: 6) 35 | .strokeBorder(lineWidth: 20) 36 | .foregroundColor(.red) 37 | 38 | RegularPolygon(sides: 16) 39 | .strokeBorder(lineWidth: 10) 40 | .foregroundColor(.purple) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## SwiftUI Shapes 2 | Collection of custom shapes 3 | 4 | ## Regular Polygons 5 |
6 | 7 |
8 | 9 | ```swift 10 | RegularPolygon(sides: 32) 11 | RoundedRegularPolygon(sides: 6, radius: 20) 12 | ``` 13 | 14 | ## Lines and Curves 15 |
16 | 17 |
18 | 19 | ```swift 20 | QuadCurve(unitPoints: [ 21 | UnitPoint(x: 0.1, y: 0.1), 22 | UnitPoint(x: 0.5, y: 0.9), 23 | UnitPoint(x: 0.9, y: 0.1) 24 | ]) 25 | .stroke(Color.blue, style: .init(lineWidth: 2, lineCap: .round)) 26 | .frame(height: 200) 27 | ``` 28 | 29 | ## Patterns 30 |
31 | 32 |
33 | 34 | ```swift 35 | GridPattern(horizontalLines: 20, verticalLines: 40) 36 | .stroke(Color.white.opacity(0.3), style: .init(lineWidth: 1, lineCap: .round)) 37 | .frame(height: 200) 38 | .background(Color.blue) 39 | .padding() 40 | ``` 41 | 42 | ## Install 43 | Add `Shapes` to your project with Swift Package Manager 44 | 45 | ```swift 46 | // swift-tools-version:5.3 47 | import PackageDescription 48 | 49 | let package = Package( 50 | name: "YOUR_PROJECT", 51 | dependencies: [ 52 | .package(url: "https://github.com/spacenation/swiftui-shapes.git", from: "1.1.0"), 53 | ] 54 | ) 55 | ``` 56 | 57 | ## Code Contributions 58 | 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. 59 | -------------------------------------------------------------------------------- /Sources/Shapes/StarPolygons/StarPolygon.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct StarPolygon: Shape { 4 | let points: UInt 5 | let smoothness: CGFloat 6 | private let inset: CGFloat 7 | 8 | public func path(in rect: CGRect) -> Path { 9 | Path.starPolygon(points: self.points, in: rect, inset: inset, smoothness: smoothness) 10 | } 11 | 12 | public init(points: UInt, smoothness: CGFloat = 0.36) { 13 | self.init(points: points, smoothness: smoothness, inset: 0) 14 | } 15 | 16 | init(points: UInt, smoothness: CGFloat, inset: CGFloat) { 17 | self.points = points 18 | self.smoothness = smoothness 19 | self.inset = inset 20 | } 21 | } 22 | 23 | extension StarPolygon: InsettableShape { 24 | public func inset(by amount: CGFloat) -> StarPolygon { 25 | StarPolygon(points: self.points, smoothness: self.smoothness, inset: self.inset + amount) 26 | } 27 | } 28 | 29 | struct StarPolygon_Previews: PreviewProvider { 30 | static var previews: some View { 31 | 32 | StarPolygon(points: 5) 33 | .foregroundColor(.blue) 34 | .background(Circle()) 35 | .previewLayout(.fixed(width: 200, height: 200)) 36 | 37 | StarPolygon(points: 3) 38 | .foregroundColor(.red) 39 | .background(Circle()) 40 | .previewLayout(.fixed(width: 200, height: 200)) 41 | 42 | StarPolygon(points: 32) 43 | .foregroundColor(.purple) 44 | .background(Circle()) 45 | .previewLayout(.fixed(width: 200, height: 200)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Shapes/Other/HalfCapsule.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | public struct HalfCapsule: View, InsettableShape { 5 | private let inset: CGFloat 6 | 7 | public func inset(by amount: CGFloat) -> HalfCapsule { 8 | HalfCapsule(inset: self.inset + amount) 9 | } 10 | 11 | public func path(in rect: CGRect) -> Path { 12 | let width = rect.size.width - inset * 2 13 | let height = rect.size.height - inset * 2 14 | let heightRadius = height / 2 15 | let widthRadius = width / 2 16 | let minRadius = min(heightRadius, widthRadius) 17 | return Path { path in 18 | path.move(to: CGPoint(x: width, y: 0)) 19 | path.addArc(center: CGPoint(x: minRadius, y: minRadius), radius: minRadius, startAngle: Angle(degrees: 270), endAngle: Angle(degrees: 180), clockwise: true) 20 | path.addArc(center: CGPoint(x: minRadius, y: height - minRadius), radius: minRadius, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 90), clockwise: true) 21 | path.addLine(to: CGPoint(x: width, y: height)) 22 | path.closeSubpath() 23 | }.offsetBy(dx: inset, dy: inset) 24 | } 25 | 26 | public var body: some View { 27 | GeometryReader { geometry in 28 | self.path(in: CGRect(x: 0, y: 0, width: geometry.size.width, height: geometry.size.height)) 29 | } 30 | } 31 | 32 | public init(inset: CGFloat = 0) { 33 | self.inset = inset 34 | } 35 | } 36 | 37 | struct HalfCapsule_Previews: PreviewProvider { 38 | static var previews: some View { 39 | HalfCapsule() 40 | .previewLayout(.fixed(width: 400, height: 300)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Shapes/RoundedRegularPolygons/RoundedRegularPolygon.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct RoundedRegularPolygon: Shape { 4 | let sides: UInt 5 | let radius: CGFloat 6 | private let inset: CGFloat 7 | 8 | public func path(in rect: CGRect) -> Path { 9 | Path.roundedRegularPolygon(sides: self.sides, in: rect, inset: inset, radius: radius) 10 | } 11 | 12 | public init(sides: UInt, radius: CGFloat) { 13 | self.init(sides: sides, radius: radius, inset: 0) 14 | } 15 | 16 | init(sides: UInt, radius: CGFloat, inset: CGFloat) { 17 | self.sides = sides 18 | self.radius = radius 19 | self.inset = inset 20 | } 21 | } 22 | 23 | extension RoundedRegularPolygon: InsettableShape { 24 | public func inset(by amount: CGFloat) -> RoundedRegularPolygon { 25 | RoundedRegularPolygon(sides: self.sides, radius: radius, inset: self.inset + amount) 26 | } 27 | } 28 | 29 | struct RoundedRegularPolygon_Previews: PreviewProvider { 30 | static var previews: some View { 31 | RoundedRegularPolygon(sides: 3, radius: 30) 32 | .fill(LinearGradient(gradient: Gradient(colors: [.orange, .red]), startPoint: .topLeading, endPoint: .bottomTrailing)) 33 | .previewLayout(.fixed(width: 200, height: 200)) 34 | 35 | RoundedRegularPolygon(sides: 6, radius: 20) 36 | .strokeBorder(lineWidth: 20) 37 | .foregroundColor(.yellow) 38 | .previewLayout(.fixed(width: 200, height: 200)) 39 | 40 | RoundedRegularPolygon(sides: 16, radius: 10) 41 | .strokeBorder(lineWidth: 20) 42 | .foregroundColor(.green) 43 | .previewLayout(.fixed(width: 200, height: 200)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Shapes/Path/Path+QuadCurves.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension Path { 4 | mutating func addQuadCurves(_ points: [CGPoint]) { 5 | guard points.count > 0 else { return } 6 | 7 | if let currentPoint = self.currentPoint { 8 | var lastPoint = currentPoint 9 | 10 | (0.. CGPoint { 14 | CGPoint(x: (self.x + point.x) * 0.5, y: (self.y + point.y) * 0.5) 15 | } 16 | 17 | func quadCurveControlPoint(with point: CGPoint) -> CGPoint { 18 | let halfwayPoint = self.halfway(to: point) 19 | let absoluteDistance = abs(point.y - halfwayPoint.y) 20 | 21 | if self.y < point.y { 22 | return CGPoint(x: halfwayPoint.x, y: halfwayPoint.y + absoluteDistance) 23 | } else if self.y > point.y { 24 | return CGPoint(x: halfwayPoint.x, y: halfwayPoint.y - absoluteDistance) 25 | } else { 26 | return halfwayPoint 27 | } 28 | } 29 | } 30 | 31 | public extension CGPoint { 32 | static func intersection(start1: CGPoint, end1: CGPoint, start2: CGPoint, end2: CGPoint) -> CGPoint? { 33 | let denominator = (end1.x - start1.x) * (end2.y - start2.y) - (end1.y - start1.y) * (end2.x - start2.x) 34 | if denominator == 0 { 35 | // Lines are parallel or coincident 36 | return nil 37 | } 38 | 39 | let ua = ((end2.x - start2.x) * (start1.y - start2.y) - (end2.y - start2.y) * (start1.x - start2.x)) / denominator 40 | 41 | let x = start1.x + ua * (end1.x - start1.x) 42 | let y = start1.y + ua * (end1.y - start1.y) 43 | 44 | return CGPoint(x: x, y: y) 45 | } 46 | } 47 | 48 | public extension Collection where Element == UnitPoint { 49 | func points(in rect: CGRect) -> [CGPoint] { 50 | self.map { CGPoint(unitPoint: $0, in: rect) } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Shapes/Path+Extensions.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension Path { 4 | mutating func addCircularCornerRadiusArc(from point: CGPoint, via viaPoint: CGPoint, to nextPoint: CGPoint, radius: CGFloat) { 5 | // Calculate the cross product to determine clockwise direction 6 | let v1 = CGVector(dx: viaPoint.x - point.x, dy: viaPoint.y - point.y) 7 | let v2 = CGVector(dx: nextPoint.x - viaPoint.x, dy: nextPoint.y - viaPoint.y) 8 | 9 | let crossProduct = v1.dx * v2.dy - v1.dy * v2.dx 10 | 11 | // Determine if the arc should be clockwise based on the cross product 12 | let isClockwise = crossProduct < 0 13 | 14 | let radius = isClockwise ? -radius : radius 15 | 16 | let lineAngle = atan2(viaPoint.y - point.y, viaPoint.x - point.x) 17 | let nextLineAngle = atan2(nextPoint.y - viaPoint.y, nextPoint.x - viaPoint.x) 18 | 19 | let lineVector = CGVector(dx: -sin(lineAngle) * radius, dy: cos(lineAngle) * radius) 20 | let nextLineVector = CGVector(dx: -sin(nextLineAngle) * radius, dy: cos(nextLineAngle) * radius) 21 | 22 | let offsetStart1 = CGPoint(x: point.x + lineVector.dx, y: point.y + lineVector.dy) 23 | let offsetEnd1 = CGPoint(x: viaPoint.x + lineVector.dx, y: viaPoint.y + lineVector.dy) 24 | 25 | let offsetStart2 = CGPoint(x: viaPoint.x + nextLineVector.dx, y: viaPoint.y + nextLineVector.dy) 26 | 27 | let offsetEnd2 = CGPoint(x: nextPoint.x + nextLineVector.dx, y: nextPoint.y + nextLineVector.dy) 28 | 29 | guard let intersection = CGPoint.intersection(start1: offsetStart1, end1: offsetEnd1, start2: offsetStart2, end2: offsetEnd2) else { 30 | return 31 | } 32 | 33 | let startAngle = lineAngle - (.pi / 2) 34 | let endAngle = nextLineAngle - (.pi / 2) 35 | 36 | self.addArc( 37 | center: intersection, 38 | radius: radius, 39 | startAngle: Angle(radians: startAngle), 40 | endAngle: Angle(radians: endAngle), 41 | clockwise: isClockwise 42 | ) 43 | } 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /Sources/Shapes/RoundedRegularPolygons/RoundedRegularPolygonPath.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Path { 4 | static func roundedRegularPolygon(sides: UInt, in rect: CGRect, inset: CGFloat = 0, radius: CGFloat = 0) -> Path { 5 | let rect = rect.insetBy(dx: inset, dy: inset) 6 | let width = rect.width 7 | let height = rect.height 8 | let hypotenuse = Double(min(width, height)) / 2.0 9 | let centerPoint = CGPoint(x: rect.midX, y: rect.midY) 10 | var usableRadius: CGFloat = .zero 11 | 12 | return Path { path in 13 | guard sides > 2 else { return } 14 | (0.. GridPattern { 10 | GridPattern(inset: self.inset + amount, horizontalLines: self.horizontalLines, verticalLines: self.verticalLines) 11 | } 12 | 13 | public func path(in rect: CGRect) -> Path { 14 | let rect = rect.insetBy(dx: self.inset, dy: self.inset) 15 | 16 | return Path { path in 17 | /// Horizontal Lines 18 | if horizontalLines > 1 { 19 | let unitDistanceBetweenHorizontalLines = 1.0 / CGFloat(horizontalLines - 1) 20 | 21 | (0.. 1 { 30 | let unitDistanceBetweenVerticalLines = 1.0 / CGFloat(verticalLines - 1) 31 | 32 | (0.. Path { 11 | Path.roundedStarPolygon(points: points, in: rect, smoothness: smoothness, convexRadius: convexRadius, concaveRadius: concaveRadius, inset: inset) 12 | } 13 | 14 | public init(points: UInt, smoothness: CGFloat, radius: CGFloat) { 15 | self.init(points: points, smoothness: smoothness, convexRadius: radius, concaveRadius: radius, inset: 0) 16 | } 17 | 18 | public init(points: UInt, smoothness: CGFloat, convexRadius: CGFloat , concaveRadius: CGFloat) { 19 | self.init(points: points, smoothness: smoothness, convexRadius: convexRadius, concaveRadius: concaveRadius, inset: 0) 20 | } 21 | 22 | init(points: UInt, smoothness: CGFloat, convexRadius: CGFloat, concaveRadius: CGFloat, inset: CGFloat) { 23 | self.points = points 24 | self.smoothness = smoothness 25 | self.convexRadius = convexRadius 26 | self.concaveRadius = concaveRadius 27 | self.inset = inset 28 | } 29 | } 30 | 31 | extension RoundedStarPolygon: InsettableShape { 32 | public func inset(by amount: CGFloat) -> RoundedStarPolygon { 33 | RoundedStarPolygon(points: points, smoothness: smoothness, convexRadius: convexRadius, concaveRadius: concaveRadius, inset: inset + amount) 34 | } 35 | } 36 | 37 | extension Path { 38 | static func roundedStarPolygon(points: UInt, in rect: CGRect, smoothness: CGFloat, convexRadius: CGFloat, concaveRadius: CGFloat, inset: CGFloat) -> Path { 39 | let sides = points * 2 40 | let width = rect.size.width - inset * 2 41 | let height = rect.size.height - inset * 2 42 | let hypotenuse = Double(min(width, height)) / 2.0 43 | let centerPoint = CGPoint(x: width / 2.0, y: height / 2.0) 44 | 45 | var usableConvexRadius: CGFloat = .zero 46 | var usableConcaveRadius: CGFloat = .zero 47 | 48 | func getPoint(from centerPoint: CGPoint, angle: Double, hypotenuse: Double) -> CGPoint { 49 | CGPoint( 50 | x: centerPoint.x + CGFloat(cos(angle) * hypotenuse), 51 | y: centerPoint.y + CGFloat(sin(angle) * hypotenuse) 52 | ) 53 | } 54 | 55 | func getViaPoint(from centerPoint: CGPoint, angle: Double, hypotenuse: Double, smoothness: CGFloat) -> CGPoint { 56 | CGPoint( 57 | x: centerPoint.x + CGFloat(cos(angle) * hypotenuse * smoothness), 58 | y: centerPoint.y + CGFloat(sin(angle) * hypotenuse * smoothness) 59 | ) 60 | } 61 | 62 | func getLastPoint(from centerPoint: CGPoint, angle: Double, hypotenuse: Double, smoothness: CGFloat) -> CGPoint { 63 | CGPoint( 64 | x: centerPoint.x + CGFloat(cos(angle) * hypotenuse * smoothness), 65 | y: centerPoint.y + CGFloat(sin(angle) * hypotenuse * smoothness) 66 | ) 67 | } 68 | 69 | return Path { path in 70 | for index in 0 ..< points { 71 | let angle: Double = ((Double(index * 2) * (360.0 / Double(sides))) - 90) * Double.pi / 180 72 | 73 | let point: CGPoint = getPoint(from: centerPoint, angle: angle, hypotenuse: hypotenuse) 74 | 75 | let viaAngle: Double = ((Double(index * 2 + 1) * (360.0 / Double(sides))) - 90) * Double.pi / 180 76 | 77 | let viaPoint: CGPoint = getViaPoint(from: centerPoint, angle: viaAngle, hypotenuse: hypotenuse, smoothness: smoothness) 78 | 79 | let px: CGFloat = point.x - viaPoint.x 80 | let py: CGFloat = point.y - viaPoint.y 81 | 82 | let sideLength: CGFloat = sqrt(px * px + py * py) 83 | 84 | let inradius: CGFloat = sideLength / (2 * tan(.pi / CGFloat(points * 2))) 85 | 86 | if usableConvexRadius == 0 { 87 | usableConvexRadius = min(convexRadius, inradius) 88 | } 89 | 90 | if usableConcaveRadius == 0 { 91 | usableConcaveRadius = min(concaveRadius, inradius - convexRadius) 92 | } 93 | 94 | let nextAngle: Double = ((Double(index * 2 + 2) * (360.0 / Double(sides))) - 90) * Double.pi / 180 95 | 96 | let nextPoint = CGPoint( 97 | x: centerPoint.x + CGFloat(cos(nextAngle) * hypotenuse), 98 | y: centerPoint.y + CGFloat(sin(nextAngle) * hypotenuse) 99 | ) 100 | 101 | let lastAngle: Double = ((Double(index * 2 + 3) * (360.0 / Double(sides))) - 90) * Double.pi / 180 102 | 103 | let lastPoint: CGPoint = getLastPoint(from: centerPoint, angle: lastAngle, hypotenuse: hypotenuse, smoothness: smoothness) 104 | 105 | path.addCircularCornerRadiusArc(from: point, via: viaPoint, to: nextPoint, radius: usableConcaveRadius) 106 | 107 | path.addCircularCornerRadiusArc(from: viaPoint, via: nextPoint, to: lastPoint, radius: usableConvexRadius) 108 | } 109 | path.closeSubpath() 110 | } 111 | .offsetBy(dx: inset, dy: inset) 112 | } 113 | } 114 | 115 | struct RoundedStarPolygon_Previews: PreviewProvider { 116 | static var previews: some View { 117 | 118 | RoundedStarPolygon(points: 5, smoothness: 0.5, convexRadius: 10, concaveRadius: 10) 119 | .fill(Color.orange) 120 | .background(Circle()) 121 | .previewLayout(.fixed(width: 400, height: 400)) 122 | 123 | RoundedStarPolygon(points: 12, smoothness: 0.87, radius: 20) .strokeBorder(lineWidth: 1) 124 | .foregroundColor(.yellow) 125 | .background(Circle()) 126 | .previewLayout(.fixed(width: 400, height: 400)) 127 | 128 | RoundedStarPolygon(points: 3, smoothness: 0.87, radius: 40) 129 | .fill(Color.orange) 130 | .background(Circle()) 131 | .previewLayout(.fixed(width: 400, height: 400)) 132 | 133 | RoundedStarPolygon(points: 5, smoothness: 0.5, radius: 20) 134 | .strokeBorder(lineWidth: 1) 135 | .foregroundColor(.yellow) 136 | .background(Circle()) 137 | .previewLayout(.fixed(width: 200, height: 200)) 138 | 139 | RoundedStarPolygon(points: 5, smoothness: 0.36, radius: 0) 140 | //.strokeBorder(lineWidth: 20) 141 | .foregroundColor(.green) 142 | .background(Circle()) 143 | .previewLayout(.fixed(width: 200, height: 200)) 144 | } 145 | } 146 | --------------------------------------------------------------------------------