├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md ├── ShapesLogo.svg ├── Sources └── Shapes │ ├── AdaptiveLine.swift │ ├── Arrow.swift │ ├── BeerGlassShape.swift │ ├── CartesianGrid.swift │ ├── CircularArc.swift │ ├── CubicBezierShape.swift │ ├── FlashlightShape.swift │ ├── FoldableShape.swift │ ├── HorizontalLine.swift │ ├── Infinity.swift │ ├── Lines.swift │ ├── OmniRectangle │ ├── Corner.swift │ ├── CornerStyle.swift │ ├── CornerStyles.swift │ ├── Edge.swift │ ├── EdgeStyles.swift │ └── OmniRectangle.swift │ ├── PathText.swift │ ├── Pentagon.swift │ ├── PolarGrid.swift │ ├── Polygon.swift │ ├── QuadraticBezierShape.swift │ ├── RadialTickMarks.swift │ ├── Shapes.swift │ ├── Square.swift │ ├── TangentArc.swift │ ├── TickMarks.swift │ ├── Trapezoid.swift │ ├── Triangles.swift │ └── VerticalLine.swift └── Tests ├── LinuxMain.swift └── ShapesTests ├── ShapesTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kieran Brown 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 | -------------------------------------------------------------------------------- /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: [.iOS(.v13), .macOS(.v10_15), .watchOS(.v6), .tvOS(.v13)], 9 | products: [ 10 | .library( 11 | name: "Shapes", 12 | targets: ["Shapes"]), 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/kieranb662/CGExtender.git", from: "1.0.1") 16 | ], 17 | targets: [ 18 | .target( 19 | name: "Shapes", 20 | dependencies: ["CGExtender"]), 21 | .testTarget( 22 | name: "ShapesTests", 23 | dependencies: ["Shapes"]), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 |

7 | SwiftUI 8 | Swift 5.1 9 | Swift 5.1 10 | kieranb662 followers 11 |

12 | 13 | Commonly used shapes for SwiftUI, some I found on the web [swiftui-lab](https://swiftui-lab.com) and [objc.io](https://www.objc.io/blog/2019/12/16/drawing-trees), others I made myself. 14 | I hope to create community based repo for cool animated shapes, paths, etc. If you would like to submit some of your own shapes just make a pull request and I will try to approve it ASAP. If you want to try out this package just clone the [example project](https://github.com/kieranb662/Shapes-Examples) 15 | 16 | Or create your own shapes using the [bez editor](https://apps.apple.com/us/app/bez-editor/id1508764103) app available for free on iOS 13.4 and greater. 17 | 18 |

19 | 20 | 21 | 22 |

23 | 24 | 25 |

26 | 27 |

28 | 29 | 30 | - **AnyShape**: A type erased `Shape` 31 | 32 | - **Lines** 33 | - `Line` 34 | - `HorizontalLine` 35 | - `VerticalLine` 36 | - `AdaptiveLine` 37 | 38 | - **Triangles** 39 | - `Triangle` 40 | - `OpenTriangle` 41 | - `RightTriangle` 42 | 43 | - **Graphing** 44 | - `CartesianGrid` 45 | - `TickMarks` 46 | - `PolarGrid` 47 | - `RadialTickMarks` 48 | 49 | - **Misc** 50 | - `InfinitySymbol` 51 | - `Arrow` 52 | - `Polygon` 53 | - `Pentagon` 54 | - `PathText` 55 | - `FoldableShape` 56 | 57 | 58 | ## Lines 59 | 60 | ### Line 61 | 62 | Found at [drawing trees](https://www.objc.io/blog/2019/12/16/drawing-trees). A Line defined by the from and to points. 63 | 64 | 65 |

66 | 67 |

68 | 69 | ### Horizontal 70 | A horizontal line that is the width of its container has a single parameter 71 | `offset`: A value between 0 and 1 defining the lines vertical offset in its container (**Default**: 0.5) 72 | 73 |

74 | 75 |

76 | 77 | 78 | ### Vertical 79 | 80 | A Vertical line that is the height of its container has a single parameter 81 | `offset`: A value between 0 and 1 defining the line's horizontal offset in its container (**Default**: 0.5) 82 | 83 |

84 | 85 |

86 | 87 | ### Adaptive 88 | 89 | This shape creates a line centered inside of and constrained by its bounding box. 90 | The end points of the line are the points of intersection of an infinitely long angled line and the container rectangle 91 | 92 |

93 | 94 |

95 | 96 | ## Triangles 97 | 98 | The various triangles are shown below. 99 | 100 |

101 | 102 |

103 | 104 | ## Graphing 105 | 106 | ### Cartesian Grid 107 | 108 | A Rectangular grid of vertical and horizontal lines. Has two parameters 109 | `xCount`: The number of vertical lines 110 | `yCount`: The number of horizontal lines 111 | 112 |

113 | 114 |

115 | 116 | ### Polar Grid 117 | 118 | A grid made up of concentric circles and angled lines running through their center. 119 | `rCount`: The number of Circles 120 | `thetaCount`: The number of lines 121 | 122 |

123 | 124 |

125 | 126 | ### TickMarks 127 | 128 | Tick marks spaced out evenly with varying lengths dependent on the type of tick 129 | minor, semi, or major. 130 | 131 | The shape has two parameters `spacing: CGFloat` and `ticks: Int`. The spacing is the distance between ticks while the `ticks` is the number of tick marks. 132 | 133 | An examples using `TickMarks` are shown below 134 | 135 |

136 | 137 |

138 | 139 | 140 | ## Misc 141 | 142 | ## Arrow 143 | 144 | An arrow that starts out small shaped like this |--| but as it grows larger it looks like this <----> 145 | 146 |

147 | 148 |

149 | 150 | ## Pentagon 151 | 152 |

153 | 154 |

155 | 156 | 157 | ## Foldable Shapes 158 | 159 |

160 | 161 |

162 | 163 | 164 | ## Contributing 165 | 166 | If you have an idea for a shape but don't know how to describe it, try out the `PathEditor` tool that comes packaged with [bez](https://github.com/kieranb662/bez) 167 | 168 |

169 | 170 |

171 | -------------------------------------------------------------------------------- /ShapesLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Sources/Shapes/AdaptiveLine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdaptiveLine.swift 3 | // 4 | // 5 | // Created by Kieran Brown on 10/15/20. 6 | // 7 | 8 | import SwiftUI 9 | import CGExtender 10 | 11 | public struct AdaptiveLine: Shape { 12 | public var angle: Angle 13 | public var animatableData: Angle { 14 | get { self.angle } 15 | set { self.angle = newValue } 16 | } 17 | var insetAmount: CGFloat = 0 18 | 19 | 20 | /// # Adaptive Line 21 | /// 22 | /// This shape creates a line centered inside of and constrained by its bounding box. 23 | /// The end points of the line are the points of intersection of an infinitely long angled line and the container rectangle 24 | /// - Parameters: 25 | /// - angle: The angle of the adaptive line with `.zero` pointing in the positive X direction 26 | /// 27 | /// ## Example Usage 28 | /// 29 | /// ``` 30 | /// struct AdaptiveLineExample: View { 31 | /// @State var angle: Double = 0.5 32 | /// 33 | /// var body: some View { 34 | /// ZStack { 35 | /// Color(white: 0.1).edgesIgnoringSafeArea(.all) 36 | /// VStack { 37 | /// AdaptiveLine(angle: Angle(degrees: angle*360)) 38 | /// .stroke(Color.white, 39 | /// style: StrokeStyle(lineWidth: 30, lineCap: .round)) 40 | /// Slider(value: $angle) 41 | /// } 42 | /// .padding() 43 | /// } 44 | /// } 45 | /// } 46 | /// ``` 47 | public init(angle: Angle = .zero) { 48 | self.angle = angle 49 | } 50 | 51 | public func path(in rect: CGRect) -> Path { 52 | let rect = rect.insetBy(dx: insetAmount, dy: insetAmount) 53 | let w = rect.width 54 | let h = rect.height 55 | let big: CGFloat = 5000000 56 | 57 | let x1 = w/2 + big*CGFloat(cos(self.angle.radians)) 58 | let y1 = h/2 + big*CGFloat(sin(self.angle.radians)) 59 | let x2 = w/2 - big*CGFloat(cos(self.angle.radians)) 60 | let y2 = h/2 - big*CGFloat(sin(self.angle.radians)) 61 | 62 | let points = lineRectIntersection(x1, y1, x2, y2, rect.minX, rect.minY, w, h) 63 | if points.count < 2 { 64 | return Path { p in 65 | p.move(to: CGPoint(x: rect.minX, y: rect.midY)) 66 | p.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) 67 | } 68 | } 69 | 70 | return Path { p in 71 | p.move(to: points[0]) 72 | p.addLine(to: points[1]) 73 | } 74 | } 75 | } 76 | 77 | extension AdaptiveLine: InsettableShape { 78 | public func inset(by amount: CGFloat) -> some InsettableShape { 79 | var shape = self 80 | shape.insetAmount += amount 81 | return shape 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Shapes/Arrow.swift: -------------------------------------------------------------------------------- 1 | // Created by Kieran Brown on 3/21/20. 2 | // Copyright © 2020 BrownandSons. All rights reserved. 3 | 4 | import SwiftUI 5 | 6 | public struct Arrow: Shape { 7 | public var arrowOffset: CGFloat 8 | public var length: CGFloat 9 | 10 | /// An arrow that when small is shaped like this **|--|** but when gets longer looks like this **<---->** 11 | /// - parameters: 12 | /// - arrowOffset: The percent `[0,1]` horizontal offset of the arrow in its container 13 | /// - length: The length of the arrow 14 | /// 15 | /// ## Example usage 16 | /// ``` 17 | /// struct ExpandingArrowExample: View { 18 | /// @State var val: Double = 10 19 | /// @State var isHidden: Bool = false 20 | /// 21 | /// var body: some View { 22 | /// VStack { 23 | /// HStack(spacing: 0) { 24 | /// ForEach(1..<9) { (i) in 25 | /// Arrow(arrowOffset: self.val > 100 ? 1/(2*1.414) : 0, length: CGFloat(self.val)) 26 | /// .stroke(Color("Light Green")).animation(.easeIn(duration: Double(i)/4.0)) 27 | /// .frame(width: 40) 28 | /// } 29 | /// } 30 | /// .frame(height: 300) 31 | /// Slider(value: $val, in: 1...250).padding() 32 | /// } 33 | /// } 34 | /// } 35 | /// ``` 36 | public init(arrowOffset: CGFloat, length: CGFloat) { 37 | self.arrowOffset = arrowOffset 38 | self.length = length 39 | } 40 | 41 | public var animatableData: AnimatablePair { 42 | get { AnimatablePair(arrowOffset, length) } 43 | set { 44 | arrowOffset = newValue.first 45 | length = newValue.second 46 | } 47 | } 48 | 49 | public func path(in rect: CGRect) -> Path { 50 | Path { path in 51 | let w = rect.width 52 | let h = rect.height 53 | 54 | path.move(to: CGPoint(x: w/2, y: h/2 - self.length/2)) 55 | path.addLine(to: CGPoint(x: w/2, y: h/2 + self.length/2)) 56 | 57 | path.move(to: h > 40 ? CGPoint(x: w*self.arrowOffset, y: w*self.arrowOffset + h/2 - self.length/2) : CGPoint(x: 0, y: h/2 - self.length/2)) 58 | path.addLine(to: CGPoint(x: w/2, y: h/2 - self.length/2)) 59 | path.addLine(to: h > 40 ? CGPoint(x: w-w*self.arrowOffset, y: w*self.arrowOffset + h/2 - self.length/2) : CGPoint(x: w, y: h/2 - self.length/2)) 60 | 61 | path.move(to: h > 40 ? CGPoint(x: w*self.arrowOffset, y: h/2 + self.length/2 - w*self.arrowOffset) : CGPoint(x: 0, y: h/2 + self.length/2)) 62 | path.addLine(to: CGPoint(x: w/2, y: h/2 + self.length/2)) 63 | path.addLine(to: h > 40 ? CGPoint(x: w-w*self.arrowOffset, y: h/2 + self.length/2 - w*self.arrowOffset) : CGPoint(x: w, y: h/2 + self.length/2)) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/Shapes/BeerGlassShape.swift: -------------------------------------------------------------------------------- 1 | // Swift toolchain version 5.0 2 | // Running macOS version 10.15 3 | // Created on 11/13/20. 4 | // 5 | // Author: Kieran Brown 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct BeerGlassShape: Shape { 11 | private var insetAmount: CGFloat = 0 12 | 13 | /// Creates a shape that looks like an upsidedown beer glass. 14 | public init() {} 15 | 16 | public func path(in rect: CGRect) -> Path { 17 | let insetRect: CGRect = rect.insetBy(dx: insetAmount, dy: insetAmount) 18 | let w = insetRect.width 19 | let h = insetRect.height 20 | 21 | return Path { path in 22 | path.move(to: CGPoint(x: 0, y: h)) 23 | 24 | path.addCurve(to: CGPoint(x: w/5, y: h/2), 25 | control1: CGPoint(x: 0, y: 2*h/3), 26 | control2: CGPoint(x: w/5, y: 2*h/3)) 27 | 28 | path.addLine(to: CGPoint(x: w/5, y: 0)) 29 | path.addLine(to: CGPoint(x: 4*w/5, y: 0)) 30 | path.addLine(to: CGPoint(x: 4*w/5, y: h/2)) 31 | 32 | path.addCurve(to: CGPoint(x: w, y: h), 33 | control1: CGPoint(x: 4*w/5, y: 2*h/3), 34 | control2: CGPoint(x: w, y: 2*h/3)) 35 | 36 | path.closeSubpath() 37 | } 38 | .offsetBy(dx: insetAmount, dy: insetAmount) 39 | } 40 | } 41 | 42 | extension BeerGlassShape: InsettableShape { 43 | public func inset(by amount: CGFloat) -> some InsettableShape { 44 | var shape = self 45 | shape.insetAmount += amount 46 | return shape 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/Shapes/CartesianGrid.swift: -------------------------------------------------------------------------------- 1 | // Created by Kieran Brown on 4/8/20. 2 | // Copyright © 2020 BrownandSons. All rights reserved. 3 | // 4 | 5 | import SwiftUI 6 | 7 | public struct CartesianGrid: Shape { 8 | public var xCount: Int 9 | public var yCount: Int 10 | 11 | public init(xCount: Int, yCount: Int) { 12 | self.xCount = xCount 13 | self.yCount = yCount 14 | } 15 | 16 | public func path(in rect: CGRect) -> Path { 17 | let w = rect.width 18 | let h = rect.height 19 | let rangeX = 1...xCount 20 | let rangeY = 1...yCount 21 | 22 | return Path { path in 23 | for i in rangeX { 24 | path.move(to: CGPoint(x: CGFloat(i)*w/CGFloat(self.xCount), y: 0)) 25 | path.addLine(to: CGPoint(x: CGFloat(i)*w/CGFloat(self.xCount), y: h)) 26 | } 27 | for j in rangeY { 28 | path.move(to: CGPoint(x: 0, y: CGFloat(j)*h/CGFloat(self.yCount))) 29 | path.addLine(to: CGPoint(x: w, y: CGFloat(j)*h/CGFloat(self.yCount))) 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Shapes/CircularArc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircularArc.swift 3 | // UX Masterclass 4 | // 5 | // Created by Kieran Brown on 7/11/20. 6 | // Copyright © 2020 Kieran Brown. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct CircularArc: Shape { 12 | public var start: CGPoint 13 | public var center: CGPoint 14 | public var radius: CGFloat 15 | public var startAngle: Angle 16 | public var endAngle: Angle 17 | public var clockwise: Bool 18 | 19 | public init(start: CGPoint, center: CGPoint, radius: CGFloat, startAngle: Angle, endAngle: Angle, clockwise: Bool) { 20 | self.start = start 21 | self.center = center 22 | self.radius = radius 23 | self.startAngle = startAngle 24 | self.endAngle = endAngle 25 | self.clockwise = clockwise 26 | 27 | } 28 | 29 | public var animatableData: AnimatablePair, AnimatablePair>, CGFloat> { 30 | get {AnimatablePair(AnimatablePair(AnimatablePair(start, center), AnimatablePair(startAngle.degrees, endAngle.degrees)), radius)} 31 | set { 32 | self.start = newValue.first.first.first 33 | self.center = newValue.first.first.second 34 | self.radius = newValue.second 35 | self.startAngle = .init(degrees: newValue.first.second.first) 36 | self.endAngle = .init(degrees: newValue.first.second.second) 37 | } 38 | } 39 | 40 | 41 | public func path(in rect: CGRect) -> Path { 42 | Path { path in 43 | path.move(to: self.start) 44 | path.addArc(center: self.center, 45 | radius: self.radius, 46 | startAngle: self.startAngle, 47 | endAngle: self.endAngle, 48 | clockwise: self.clockwise) 49 | 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Shapes/CubicBezierShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CubicBezierShape.swift 3 | // UX Masterclass 4 | // 5 | // Created by Kieran Brown on 7/11/20. 6 | // Copyright © 2020 Kieran Brown. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct CubicBezierShape: Shape { 12 | public var start: CGPoint 13 | public var control1: CGPoint 14 | public var control2: CGPoint 15 | public var end: CGPoint 16 | public init(start: CGPoint, control1: CGPoint, control2: CGPoint, end: CGPoint) { 17 | self.start = start 18 | self.control1 = control1 19 | self.control2 = control2 20 | self.end = end 21 | } 22 | 23 | public var animatableData: AnimatablePair, AnimatablePair> { 24 | get { AnimatablePair(AnimatablePair(start, end), AnimatablePair(control1, control2))} 25 | set { 26 | start = newValue.first.first 27 | control1 = newValue.second.first 28 | control2 = newValue.second.second 29 | end = newValue.first.second 30 | } 31 | } 32 | 33 | public func path(in rect: CGRect) -> Path { 34 | Path { path in 35 | path.move(to: self.start) 36 | path.addCurve(to: self.end,control1: self.control1, control2: self.control2) 37 | } 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Sources/Shapes/FlashlightShape.swift: -------------------------------------------------------------------------------- 1 | // Swift toolchain version 5.0 2 | // Running macOS version 10.15 3 | // Created on 11/13/20. 4 | // 5 | // Author: Kieran Brown 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct FlashlightShape: Shape { 11 | private var insetAmount: CGFloat = 0 12 | 13 | /// Creates a shape that looks like an upsidedown flashlight. 14 | public init() {} 15 | 16 | public func path(in rect: CGRect) -> Path { 17 | let insetRect: CGRect = rect.insetBy(dx: insetAmount, dy: insetAmount) 18 | let w = insetRect.width 19 | let h = insetRect.height 20 | 21 | return Path { path in 22 | path.move(to: CGPoint(x: 0, y: h)) 23 | path.addLine(to: CGPoint(x: 0, y: 4*h/5)) 24 | 25 | path.addCurve(to: CGPoint(x: w/5, y: h/2), 26 | control1: CGPoint(x: 0, y: 2*h/3), 27 | control2: CGPoint(x: w/5, y: 2*h/3)) 28 | 29 | path.addLine(to: CGPoint(x: w/5, y: 0)) 30 | path.addLine(to: CGPoint(x: 4*w/5, y: 0)) 31 | path.addLine(to: CGPoint(x: 4*w/5, y: h/2)) 32 | 33 | path.addCurve(to: CGPoint(x: w, y: 4*h/5), 34 | control1: CGPoint(x: 4*w/5, y: 2*h/3), 35 | control2: CGPoint(x: w, y: 2*h/3)) 36 | 37 | path.addLine(to: CGPoint(x: w, y: h)) 38 | 39 | path.closeSubpath() 40 | } 41 | .offsetBy(dx: insetAmount, dy: insetAmount) 42 | } 43 | } 44 | 45 | extension FlashlightShape: InsettableShape { 46 | public func inset(by amount: CGFloat) -> some InsettableShape { 47 | var shape = self 48 | shape.insetAmount += amount 49 | return shape 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Shapes/FoldableShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FoldableShape.swift 3 | // Shapes Examples 4 | // 5 | // Created by Kieran Brown on 4/14/20. 6 | // Copyright © 2020 BrownandSons. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Path { 12 | /// The array of `Path.Elements` describing the path 13 | var elements: [Path.Element] { 14 | var temp = [Path.Element]() 15 | forEach { (element) in 16 | temp.append(element) 17 | } 18 | return temp 19 | } 20 | 21 | /// Returns the starting point of the path 22 | func getStartPoint() -> CGPoint? { 23 | if isEmpty { 24 | return nil 25 | } 26 | 27 | guard let first = elements.first(where: { 28 | switch $0 { 29 | case .move(_): 30 | return true 31 | default: 32 | return false 33 | } 34 | }) else { 35 | return nil 36 | } 37 | 38 | switch first { 39 | case .move(let to): 40 | return to 41 | default: 42 | return nil 43 | } 44 | } 45 | 46 | /// Returns the last point on the path rom the last curve command 47 | func getEndPoint() -> CGPoint? { 48 | if isEmpty { 49 | return nil 50 | } 51 | 52 | guard let last = elements.reversed().first(where: { (element) in 53 | switch element { 54 | case .line(_), .quadCurve(_, _), .curve(_, _, _): 55 | return true 56 | case .move(_), .closeSubpath: 57 | return false 58 | } 59 | }) else { 60 | return nil 61 | } 62 | 63 | switch last { 64 | case .line(let to), .quadCurve(let to, _), .curve(let to, _, _): 65 | return to 66 | default: 67 | return nil 68 | 69 | } 70 | } 71 | } 72 | 73 | /// # Foldable Shape 74 | /// A Shape which can be folded over itself. 75 | public struct FoldableShape: View { 76 | var shape: S 77 | var mainColor: Color 78 | var foldColor: Color 79 | var fraction: CGFloat 80 | 81 | public init(_ shape: S, fraction: CGFloat, mainColor: Color = .yellow, foldColor: Color = .pink) { 82 | self.shape = shape 83 | self.fraction = fraction 84 | self.mainColor = mainColor 85 | self.foldColor = foldColor 86 | } 87 | 88 | /// Function reflects a point over the line that crosses between r1 and r2 89 | private func reflect(_ r1: CGPoint, _ r2: CGPoint, _ p: CGPoint) -> CGPoint { 90 | let a = (r2.y-r1.y) 91 | let b = -(r2.x-r1.x) 92 | let c = -a*r1.x - b*r1.y 93 | let magnitude = sqrt(a*a + b*b) 94 | let ai = a/magnitude 95 | let bi = b/magnitude 96 | let ci = c/magnitude 97 | 98 | let d = ai*p.x + bi*p.y + ci 99 | let x = p.x - 2*ai*d 100 | let y = p.y - 2*bi*d 101 | return CGPoint(x: x, y: y) 102 | } 103 | 104 | /// Creates the folded portion of the path given the large and small fractions which define line that the portion is folded over. 105 | /// Procedure: 106 | /// 1. Get the fraction of the path that will serve as the folded peice 107 | /// 2. Get the start and end points of that path 108 | /// 3. Create the reflected path by Iterating through all pathFractions elements, reflecting any curve across the line defined by the start and end points. 109 | private func makeFold(path: Path) -> some View { 110 | let pathFraction = path.trimmedPath(from: fraction, to: 1) 111 | let start = pathFraction.getStartPoint() ?? .zero 112 | let end = pathFraction.getEndPoint() ?? .zero 113 | 114 | 115 | return Path { path in 116 | pathFraction.forEach { element in 117 | switch element.self { 118 | case .move(let to): 119 | path.move(to: to) 120 | case .line(let to): 121 | path.addLine(to: reflect(start, end, to)) 122 | case .quadCurve(let to, let control): 123 | path.addQuadCurve(to: reflect(start, end, to), 124 | control: reflect(start, end, control)) 125 | case .curve(let to, let control1, let control2): 126 | path.addCurve(to: reflect(start, end, to), 127 | control1: reflect(start, end, control1), 128 | control2: reflect(start, end, control2)) 129 | case .closeSubpath: 130 | path.closeSubpath() 131 | } 132 | } 133 | } 134 | } 135 | 136 | public var body: some View { 137 | GeometryReader { (proxy: GeometryProxy) in 138 | ZStack { 139 | self.shape.path(in: proxy.frame(in: .global)) 140 | .trimmedPath(from: 0, to: self.fraction) 141 | .foregroundColor(self.mainColor) 142 | .opacity(0.4) 143 | self.makeFold(path: self.shape.path(in: proxy.frame(in: .global))) 144 | .foregroundColor(self.foldColor) 145 | .opacity(0.4) 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Sources/Shapes/HorizontalLine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalLine.swift 3 | // 4 | // 5 | // Created by Kieran Brown on 10/15/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct HorizontalLine: Shape { 11 | var offset: CGFloat 12 | 13 | /// A horizontal line that is the width of its container 14 | /// - parameter offset: A value between 0 and 1 defining the lines vertical offset in its container (**Default**: 0.5) 15 | public init(offset: CGFloat = 0.5) { 16 | self.offset = offset 17 | } 18 | 19 | public func path(in rect: CGRect) -> Path { 20 | Path { path in 21 | path.move(to: CGPoint(x: 0, y: rect.maxY*offset)) 22 | path.addLine(to: CGPoint(x: rect.width, y: rect.maxY*offset)) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Shapes/Infinity.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // https://swiftui-lab.com/swiftui-animations-part2/ 4 | public struct InfinitySymbol: Shape { 5 | public init() {} 6 | var insetAmount: CGFloat = 0 7 | public func path(in rect: CGRect) -> Path { 8 | createInfinityPath(in: rect) 9 | .offsetBy(dx: insetAmount, dy: insetAmount) 10 | } 11 | 12 | public func createInfinityPath(in rect: CGRect) -> Path { 13 | let rect = rect.insetBy(dx: insetAmount, dy: insetAmount) 14 | let height = rect.height 15 | let width = rect.width 16 | let heightFactor = height/4 17 | let widthFactor = width/4 18 | 19 | var path = Path() 20 | 21 | path.move(to: CGPoint(x:widthFactor, y: heightFactor * 3)) 22 | path.addCurve(to: CGPoint(x:widthFactor, y: heightFactor), 23 | control1: CGPoint(x:0, y: heightFactor * 3), 24 | control2: CGPoint(x:0, y: heightFactor)) 25 | 26 | path.move(to: CGPoint(x:widthFactor, y: heightFactor)) 27 | path.addCurve(to: CGPoint(x:widthFactor * 3, y: heightFactor * 3), 28 | control1: CGPoint(x:widthFactor * 2, y: heightFactor), 29 | control2: CGPoint(x:widthFactor * 2, y: heightFactor * 3)) 30 | 31 | path.move(to: CGPoint(x:widthFactor * 3, y: heightFactor * 3)) 32 | path.addCurve(to: CGPoint(x:widthFactor * 3, y: heightFactor), 33 | control1: CGPoint(x:widthFactor * 4 + 5, y: heightFactor * 3), 34 | control2: CGPoint(x:widthFactor * 4 + 5, y: heightFactor)) 35 | 36 | path.move(to: CGPoint(x:widthFactor * 3, y: heightFactor)) 37 | path.addCurve(to: CGPoint(x:widthFactor, y: heightFactor * 3), 38 | control1: CGPoint(x:widthFactor * 2, y: heightFactor), 39 | control2: CGPoint(x:widthFactor * 2, y: heightFactor * 3)) 40 | 41 | return path 42 | } 43 | } 44 | 45 | extension InfinitySymbol: InsettableShape { 46 | public func inset(by amount: CGFloat) -> some InsettableShape { 47 | var shape = self 48 | shape.insetAmount += amount 49 | return shape 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Sources/Shapes/Lines.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // Created by Kieran Brown on 3/21/20. 5 | // Copyright © 2020 BrownandSons. All rights reserved. 6 | 7 | import SwiftUI 8 | 9 | public struct Line: Shape { 10 | public var from: CGPoint 11 | public var to: CGPoint 12 | public init(from: CGPoint, to: CGPoint) { 13 | self.from = from 14 | self.to = to 15 | } 16 | public var animatableData: AnimatablePair { 17 | get { AnimatablePair(from, to) } 18 | set { 19 | from = newValue.first 20 | to = newValue.second 21 | } 22 | } 23 | 24 | public func path(in rect: CGRect) -> Path { 25 | Path { path in 26 | path.move(to: self.from) 27 | path.addLine(to: self.to) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Shapes/OmniRectangle/Corner.swift: -------------------------------------------------------------------------------- 1 | // Swift toolchain version 5.0 2 | // Running macOS version 10.15 3 | // Created on 11/15/20. 4 | // 5 | // Author: Kieran Brown 6 | // 7 | 8 | import Foundation 9 | 10 | extension OmniRectangle { 11 | public struct Corner : OptionSet { 12 | public var rawValue: Int 13 | public init(rawValue: Int) { 14 | self.rawValue = rawValue 15 | } 16 | public static var topLeft: Corner = Corner(rawValue: 1 << 0) 17 | public static var topRight: Corner = Corner(rawValue: 1 << 1) 18 | public static var bottomLeft: Corner = Corner(rawValue: 1 << 2) 19 | public static var bottomRight: Corner = Corner(rawValue: 1 << 3) 20 | 21 | public static var allCorners: Corner = [.topLeft, .topRight, .bottomLeft, .bottomRight] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Shapes/OmniRectangle/CornerStyle.swift: -------------------------------------------------------------------------------- 1 | // Swift toolchain version 5.0 2 | // Running macOS version 10.15 3 | // Created on 11/13/20. 4 | // 5 | // Author: Kieran Brown 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | extension OmniRectangle { 12 | public enum CornerStyle { 13 | case round(radius: CGFloat) 14 | case cut(depth: CGFloat) 15 | case square 16 | } 17 | } 18 | 19 | extension OmniRectangle.CornerStyle { 20 | 21 | func applyTopRight(_ path: inout Path, _ width: CGFloat, _ height: CGFloat) -> Void { 22 | let checkMin: (CGFloat) -> CGFloat = { min(min($0, height/2), width/2) } 23 | switch self { 24 | case .round(radius: let radius): 25 | path.move(to: CGPoint(x: width - checkMin(radius), y: 0)) 26 | path.addArc( 27 | center: CGPoint(x: width - checkMin(radius), y: checkMin(radius)), 28 | radius: checkMin(radius), 29 | startAngle: Angle(radians: -.pi/2), 30 | endAngle: .zero, 31 | clockwise: false 32 | ) 33 | 34 | case .cut(depth: let depth): 35 | path.move(to: CGPoint(x: width-checkMin(depth), y: 0)) 36 | path.addLine(to: CGPoint(x: width, y: checkMin(depth))) 37 | 38 | case .square: 39 | path.move(to: CGPoint(x: width, y: 0)) 40 | } 41 | } 42 | 43 | func applyBottomRight(_ path: inout Path, _ width: CGFloat, _ height: CGFloat, _ curvature: CGFloat) -> Void { 44 | let checkMin: (CGFloat) -> CGFloat = { min(min($0, height/2), width/2) } 45 | switch self { 46 | case .round(radius: let radius): 47 | path.addQuadCurve(to: CGPoint(x: width, y: height - checkMin(radius)), 48 | control: CGPoint(x: width - width*curvature/2, y: height/2)) 49 | path.addArc( 50 | center: CGPoint(x: width - checkMin(radius) , y: height - checkMin(radius)), 51 | radius: checkMin(radius), 52 | startAngle: .zero, 53 | endAngle: Angle(radians: .pi/2), 54 | clockwise: false 55 | ) 56 | 57 | case .cut(depth: let depth): 58 | path.addQuadCurve(to: CGPoint(x: width, y: height-checkMin(depth)), 59 | control: CGPoint(x: width - width*curvature/2, y: height/2)) 60 | path.addLine(to: CGPoint(x: width-checkMin(depth), y: height)) 61 | 62 | case .square: 63 | path.addQuadCurve(to: CGPoint(x: width, y: height), 64 | control: CGPoint(x: width - width*curvature/2, y: height/2)) 65 | } 66 | } 67 | 68 | func applyBottomLeft(_ path: inout Path, _ width: CGFloat, _ height: CGFloat, _ curvature: CGFloat) -> Void { 69 | let checkMin: (CGFloat) -> CGFloat = { min(min($0, height/2), width/2) } 70 | switch self { 71 | case .round(radius: let radius): 72 | path.addQuadCurve(to: CGPoint(x: checkMin(radius), y: height), 73 | control: CGPoint(x: width/2, y: height - height*curvature/2)) 74 | path.addArc( 75 | center: CGPoint(x: checkMin(radius), y: height - checkMin(radius)), 76 | radius: checkMin(radius), 77 | startAngle: Angle(radians: .pi/2), 78 | endAngle: Angle(radians: .pi), 79 | clockwise: false 80 | ) 81 | 82 | case .cut(depth: let depth): 83 | path.addQuadCurve(to: CGPoint(x: checkMin(depth), y: height), 84 | control: CGPoint(x: width/2, y: height - height*curvature/2)) 85 | path.addLine(to: CGPoint(x: 0, y: height-checkMin(depth))) 86 | 87 | case .square: 88 | path.addQuadCurve(to: CGPoint(x: 0, y: height), 89 | control: CGPoint(x: width/2, y: height - height*curvature/2)) 90 | } 91 | } 92 | 93 | func applyTopLeft(_ path: inout Path, _ width: CGFloat, _ height: CGFloat, _ curvature: CGFloat) -> Void { 94 | let checkMin: (CGFloat) -> CGFloat = { min(min($0, height/2), width/2) } 95 | switch self { 96 | case .round(radius: let radius): 97 | path.addQuadCurve(to: CGPoint(x: 0, y: checkMin(radius)), 98 | control: CGPoint(x: width*curvature/2, y: height/2)) 99 | path.addArc( 100 | center: CGPoint(x: checkMin(radius), y: checkMin(radius)), 101 | radius: checkMin(radius), 102 | startAngle: Angle(radians: .pi), 103 | endAngle: Angle(radians: 3*Double.pi/2), 104 | clockwise: false 105 | ) 106 | 107 | case .cut(depth: let depth): 108 | path.addQuadCurve(to: CGPoint(x: 0, y: checkMin(depth)), 109 | control: CGPoint(x: width*curvature/2, y: height/2)) 110 | path.addLine(to: CGPoint(x: checkMin(depth), y: 0)) 111 | 112 | case .square: 113 | path.addQuadCurve(to: CGPoint(x: 0, y: 0), 114 | control: CGPoint(x: width*curvature/2, y: height/2)) 115 | } 116 | } 117 | } 118 | 119 | 120 | extension OmniRectangle.CornerStyle: CustomStringConvertible { 121 | 122 | static let formatter: NumberFormatter = { 123 | let f = NumberFormatter() 124 | f.maximumFractionDigits = 3 125 | 126 | return f 127 | }() 128 | 129 | public var description: String { 130 | switch self { 131 | case .round(radius: let radius): 132 | return "radius: \(Self.formatter.string(from: radius as NSNumber) ?? "")" 133 | case .cut(depth: let depth): 134 | return "depth: \(Self.formatter.string(from: depth as NSNumber) ?? "")" 135 | case .square: 136 | return "square" 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/Shapes/OmniRectangle/CornerStyles.swift: -------------------------------------------------------------------------------- 1 | // Swift toolchain version 5.0 2 | // Running macOS version 10.15 3 | // Created on 11/14/20. 4 | // 5 | // Author: Kieran Brown 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension OmniRectangle { 11 | public struct CornerStyles { 12 | public var topLeft: CornerStyle 13 | public var topRight: CornerStyle 14 | public var bottomLeft: CornerStyle 15 | public var bottomRight: CornerStyle 16 | 17 | public init(topLeft: CornerStyle, topRight: CornerStyle, bottomLeft: CornerStyle, bottomRight: CornerStyle) { 18 | self.topLeft = topLeft 19 | self.topRight = topRight 20 | self.bottomLeft = bottomLeft 21 | self.bottomRight = bottomRight 22 | } 23 | 24 | public init(_ corners: UIRectCorner = .allCorners, style: CornerStyle) { 25 | self.topLeft = corners.contains(.topLeft) ? style : .square 26 | self.bottomLeft = corners.contains(.bottomLeft) ? style : .square 27 | self.topRight = corners.contains(.topRight) ? style : .square 28 | self.bottomRight = corners.contains(.bottomRight) ? style : .square 29 | } 30 | 31 | public static func allSquare() -> CornerStyles { 32 | CornerStyles(topLeft: .square, 33 | topRight: .square, 34 | bottomLeft: .square, 35 | bottomRight: .square) 36 | } 37 | } 38 | } 39 | 40 | 41 | extension OmniRectangle.CornerStyles: CustomStringConvertible { 42 | public var description: String { 43 | """ 44 | \(topLeft.description) 45 | \(topRight.description) 46 | \(bottomLeft.description) 47 | \(bottomRight) 48 | """ 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Shapes/OmniRectangle/Edge.swift: -------------------------------------------------------------------------------- 1 | // Swift toolchain version 5.0 2 | // Running macOS version 10.15 3 | // Created on 11/15/20. 4 | // 5 | // Author: Kieran Brown 6 | // 7 | 8 | import Foundation 9 | 10 | extension OmniRectangle { 11 | public struct Edge : OptionSet { 12 | public var rawValue: Int 13 | public init(rawValue: Int) { 14 | self.rawValue = rawValue 15 | } 16 | public static var top: Edge = Edge(rawValue: 1 << 0) 17 | public static var left: Edge = Edge(rawValue: 1 << 1) 18 | public static var bottom: Edge = Edge(rawValue: 1 << 2) 19 | public static var right: Edge = Edge(rawValue: 1 << 3) 20 | 21 | public static var all: Edge = [.left, .right, .top, .bottom] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Shapes/OmniRectangle/EdgeStyles.swift: -------------------------------------------------------------------------------- 1 | // Swift toolchain version 5.0 2 | // Running macOS version 10.15 3 | // Created on 11/14/20. 4 | // 5 | // Author: Kieran Brown 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension OmniRectangle { 11 | public struct EdgeStyles { 12 | public var leftCurvature: CGFloat 13 | public var topCurvature: CGFloat 14 | public var rightCurvature: CGFloat 15 | public var bottomCurvature: CGFloat 16 | 17 | public init(leftCurvature: CGFloat, topCurvature: CGFloat, rightCurvature: CGFloat, bottomCurvature: CGFloat) { 18 | self.leftCurvature = leftCurvature 19 | self.topCurvature = topCurvature 20 | self.rightCurvature = rightCurvature 21 | self.bottomCurvature = bottomCurvature 22 | } 23 | 24 | public init(_ edges: UIRectEdge = .all, curvature: CGFloat) { 25 | self.leftCurvature = edges.contains(.left) ? curvature : 0 26 | self.topCurvature = edges.contains(.top) ? curvature : 0 27 | self.rightCurvature = edges.contains(.right) ? curvature : 0 28 | self.bottomCurvature = edges.contains(.bottom) ? curvature : 0 29 | } 30 | 31 | public static func allFlat() -> EdgeStyles { 32 | EdgeStyles(leftCurvature: 0, 33 | topCurvature: 0, 34 | rightCurvature: 0, 35 | bottomCurvature: 0) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Shapes/OmniRectangle/OmniRectangle.swift: -------------------------------------------------------------------------------- 1 | // Swift toolchain version 5.0 2 | // Running macOS version 10.15 3 | // Created on 11/13/20. 4 | // 5 | // Author: Kieran Brown 6 | // 7 | 8 | 9 | import SwiftUI 10 | 11 | public struct OmniRectangle: Shape { 12 | private var insetAmount: CGFloat = 0 13 | private var cornerStyles: CornerStyles 14 | private var edgeStyles: EdgeStyles 15 | 16 | // TODO: Add in the animation data for the corner styles 17 | public var animatableData: AnimatablePair, 18 | AnimatablePair> { 19 | get { 20 | AnimatablePair( 21 | AnimatablePair(edgeStyles.leftCurvature, edgeStyles.topCurvature), 22 | AnimatablePair(edgeStyles.rightCurvature, edgeStyles.bottomCurvature) 23 | ) 24 | } 25 | set { 26 | edgeStyles[keyPath: \.leftCurvature] = newValue.first.first 27 | edgeStyles[keyPath: \.topCurvature] = newValue.first.second 28 | edgeStyles[keyPath: \.rightCurvature] = newValue.second.first 29 | edgeStyles[keyPath: \.bottomCurvature] = newValue.second.second 30 | } 31 | } 32 | 33 | public init(corners: CornerStyles = .allSquare(), edges: EdgeStyles = .allFlat()) { 34 | self.cornerStyles = corners 35 | self.edgeStyles = edges 36 | } 37 | 38 | public init(topLeft: CornerStyle, topRight: CornerStyle, bottomLeft: CornerStyle, bottomRight: CornerStyle) { 39 | self.cornerStyles = CornerStyles(topLeft: topLeft, 40 | topRight: topRight, 41 | bottomLeft: bottomLeft, 42 | bottomRight: bottomRight) 43 | self.edgeStyles = .allFlat() 44 | } 45 | 46 | public init(leftEdgeCurvature: CGFloat, topEdgeCurvature: CGFloat, rightEdgeCurvature: CGFloat, bottomEdgeCurvature: CGFloat) { 47 | self.edgeStyles = EdgeStyles(leftCurvature: leftEdgeCurvature, 48 | topCurvature: topEdgeCurvature, 49 | rightCurvature: rightEdgeCurvature, 50 | bottomCurvature: bottomEdgeCurvature) 51 | self.cornerStyles = .allSquare() 52 | } 53 | 54 | public func path(in rect: CGRect) -> Path { 55 | Path { path in 56 | let insetRect = rect.insetBy(dx: insetAmount, dy: insetAmount) 57 | let (width, height) = (insetRect.width, insetRect.height) 58 | let control = CGPoint(x: width/2, y: edgeStyles.topCurvature*height/2) 59 | 60 | cornerStyles.topRight.applyTopRight(&path, width, height) 61 | cornerStyles.bottomRight.applyBottomRight(&path, width, height, edgeStyles.rightCurvature) 62 | cornerStyles.bottomLeft.applyBottomLeft(&path, width, height, edgeStyles.bottomCurvature) 63 | cornerStyles.topLeft.applyTopLeft(&path, width,height, edgeStyles.leftCurvature) 64 | 65 | let checkMin: (CGFloat) -> CGFloat = { min(min($0, height/2), width/2) } 66 | 67 | switch cornerStyles.topRight { 68 | case .round(let radius): 69 | path.addQuadCurve(to: CGPoint(x: width - checkMin(radius), y: 0), control: control) 70 | case .cut(let depth): 71 | path.addQuadCurve(to: CGPoint(x: width - checkMin(depth), y: 0), control: control) 72 | case .square: 73 | path.addQuadCurve(to: CGPoint(x: width, y: 0), control: control) 74 | } 75 | 76 | path.closeSubpath() 77 | } 78 | .offsetBy(dx: insetAmount, dy: insetAmount) 79 | } 80 | } 81 | 82 | extension OmniRectangle: InsettableShape { 83 | public func inset(by amount: CGFloat) -> some InsettableShape { 84 | var shape = self 85 | shape.insetAmount += amount 86 | return shape 87 | } 88 | } 89 | 90 | // 91 | //extension OmniRectangle { 92 | // fileprivate func prepare(stroke: StrokeStyle, width: CGFloat = 100, height: CGFloat = 100) -> some View { 93 | // ZStack { 94 | // self 95 | // .strokeBorder(Color.red, style: stroke) 96 | // } 97 | // .padding() 98 | // .frame(width: width, height: height) 99 | // // .border(Color.red) 100 | // } 101 | //} 102 | // 103 | //// MARK: - This preview is meant to be used on an iPad Pro 12.9in simulator 104 | // 105 | //struct OmniRectangle_Previews: PreviewProvider { 106 | // static let roundThinStrokeStyle = StrokeStyle(lineWidth: 1, lineJoin: .round) 107 | // static let roundThickStrokeStyle = StrokeStyle(lineWidth: 30, lineJoin: .round) 108 | // static let miterThinStrokeStyle = StrokeStyle(lineWidth: 1, lineJoin: .miter) 109 | // static let miterThickStrokeStyle = StrokeStyle(lineWidth: 30, lineJoin: .miter) 110 | // static let bevelThinStrokeStyle = StrokeStyle(lineWidth: 1, lineJoin: .bevel) 111 | // static let bevelThickStrokeStyle = StrokeStyle(lineWidth: 30, lineJoin: .bevel) 112 | // 113 | // static let cornerMedley = OmniRectangle.CornerStyles( 114 | // topLeft: .round(radius: 0), 115 | // topRight: .round(radius: 10), 116 | // bottomLeft: .cut(depth: 15), 117 | // bottomRight: .cut(depth: 0) 118 | // ) 119 | // 120 | // static let maxCutDepth = OmniRectangle.CornerStyles(style: .cut(depth: 100)) 121 | // static let maxCornerRadius = OmniRectangle.CornerStyles(style: .round(radius: 100)) 122 | // 123 | // static let squareCornersFlatEdges = OmniRectangle() 124 | // static let roundCornersFlatEdges = OmniRectangle(corners: OmniRectangle.CornerStyles(style: .round(radius: 10))) 125 | // static let maxRoundCornersFlatEdges = OmniRectangle(corners: maxCornerRadius) 126 | // static let cutCornersFlatEdges = OmniRectangle(corners: OmniRectangle.CornerStyles(style: .cut(depth: 10))) 127 | // static let maxCutCornersFlatEdges = OmniRectangle(corners: maxCutDepth) 128 | // static let cornerMedleyFlatEdges = OmniRectangle(corners: cornerMedley) 129 | // 130 | // static let fullPositiveCurvature = OmniRectangle.EdgeStyles(curvature: 1) 131 | // static let halfPositiveCurvature = OmniRectangle.EdgeStyles(curvature: 0.5) 132 | // static let fullNegativeCurvature = OmniRectangle.EdgeStyles(curvature: -1) 133 | // static let halfNegativeCurvature = OmniRectangle.EdgeStyles(curvature: -0.5) 134 | // 135 | // static func makeRow(_ omniShape: OmniRectangle) -> some View { 136 | // HStack(spacing: 30) { 137 | // Text(omniShape.cornerStyles.description) 138 | // .frame(width: 100, height: 100, alignment: .leading) 139 | // 140 | // omniShape 141 | // .fill(Color.purple) 142 | // .padding() 143 | // .frame(width: 100, height: 100) 144 | // omniShape.prepare(stroke: roundThinStrokeStyle) 145 | // omniShape.prepare(stroke: roundThickStrokeStyle) 146 | // omniShape.prepare(stroke: miterThinStrokeStyle) 147 | // omniShape.prepare(stroke: miterThickStrokeStyle) 148 | // omniShape.prepare(stroke: bevelThinStrokeStyle) 149 | // omniShape.prepare(stroke: bevelThickStrokeStyle) 150 | // } 151 | // } 152 | // 153 | // static func makeCurvatureGroup(corners: OmniRectangle.CornerStyles) -> some View { 154 | // Group { 155 | // makeRow(OmniRectangle(corners: corners, edges: fullPositiveCurvature)) 156 | // makeRow(OmniRectangle(corners: corners, edges: halfPositiveCurvature)) 157 | // makeRow(OmniRectangle(corners: corners, edges: .allFlat())) 158 | // makeRow(OmniRectangle(corners: corners, edges: halfNegativeCurvature)) 159 | // makeRow(OmniRectangle(corners: corners, edges: fullNegativeCurvature)) 160 | // } 161 | // } 162 | // 163 | // static func makeLabel(lineWidth: CGFloat, lineJoin: String) -> some View { 164 | // Text("Stroked\nwidth: \(String(format: "%.0f", Double(lineWidth)))\njoin: \(lineJoin)") 165 | // .frame(width: 100, height: 100) 166 | // } 167 | // 168 | // static func header() -> some View { 169 | // HStack(alignment: .firstTextBaseline, spacing: 30) { 170 | // // Text("Top left:\nTop right:\nBottom left:\nBottom right:\n") 171 | // Text("Filled") 172 | // .frame(width: 100, height: 100) 173 | // makeLabel(lineWidth: 1, lineJoin: "round") 174 | // makeLabel(lineWidth: 30, lineJoin: "round") 175 | // makeLabel(lineWidth: 1, lineJoin: "miter") 176 | // makeLabel(lineWidth: 30, lineJoin: "miter") 177 | // makeLabel(lineWidth: 1, lineJoin: "bevel") 178 | // makeLabel(lineWidth: 30, lineJoin: "bevel") 179 | // } 180 | // } 181 | // 182 | // static var previews: some View { 183 | // ScrollView { 184 | // LazyVStack(spacing: 40, pinnedViews: [.sectionHeaders]) { 185 | // Section(header: ZStack { 186 | // Color(white: 0.05) 187 | // .edgesIgnoringSafeArea(.top) 188 | // .frame(height: 120) 189 | // header() 190 | // }) { 191 | // Group { 192 | // makeRow(squareCornersFlatEdges) 193 | // makeRow(roundCornersFlatEdges) 194 | // makeRow(maxRoundCornersFlatEdges) 195 | // makeRow(cutCornersFlatEdges) 196 | // makeRow(maxCutCornersFlatEdges) 197 | // makeRow(cornerMedleyFlatEdges) 198 | // } 199 | // makeCurvatureGroup(corners: .allSquare()) 200 | // makeCurvatureGroup(corners: cornerMedley) 201 | // 202 | // } 203 | // } 204 | // }.preferredColorScheme(.dark).edgesIgnoringSafeArea(.top) 205 | // } 206 | //} 207 | -------------------------------------------------------------------------------- /Sources/Shapes/PathText.swift: -------------------------------------------------------------------------------- 1 | // Created by Kieran Brown on 3/25/20. 2 | // Copyright © 2020 BrownandSons. All rights reserved. 3 | 4 | 5 | import SwiftUI 6 | 7 | public struct PathText: Shape { 8 | public init() {} 9 | public func path(in rect: CGRect) -> Path { 10 | Path { path in 11 | let w = rect.width/0.7 12 | let h = rect.height 13 | // P 14 | path.move(to: CGPoint(x: 0*w, y: 0.4*h)) 15 | path.addCurve(to: CGPoint(x: 0*w, y: 0.029*h), control1: CGPoint(x: 0.18*w, y: 0.508*h), control2: CGPoint(x: 0.179*w, y: -0.144*h)) 16 | path.addLine(to: CGPoint(x: 0*w, y: h)) 17 | // A 18 | path.move(to: CGPoint(x: 0.125*w, y: h)) 19 | path.addLine(to: CGPoint(x: 0.175*w, y: 0.5*h)) 20 | path.addLine(to: CGPoint(x: 0.275*w, y: 0.5*h)) 21 | path.addLine(to: CGPoint(x: 0.225*w, y: 0*h)) 22 | path.addLine(to: CGPoint(x: 0.175*w, y: 0.5*h)) 23 | path.addLine(to: CGPoint(x: 0.275*w, y: 0.5*h)) 24 | path.addLine(to: CGPoint(x: 0.325*w, y: h)) 25 | // T 26 | path.move(to: CGPoint(x: 0.425*w, y: h)) 27 | path.addLine(to: CGPoint(x: 0.425*w, y: 0.0*h)) 28 | path.addLine(to: CGPoint(x: 0.325*w, y: 0.0*h)) 29 | path.addLine(to: CGPoint(x: 0.525*w, y: 0.0*h)) 30 | // H 31 | path.move(to: CGPoint(x: 0.55*w, y: h)) 32 | path.addLine(to: CGPoint(x: 0.55*w, y: 0.0*h)) 33 | path.addLine(to: CGPoint(x: 0.55*w, y: 0.500*h)) 34 | path.addLine(to: CGPoint(x: 0.7*w, y: 0.500*h)) 35 | path.addLine(to: CGPoint(x: 0.7*w, y: 0*h)) 36 | path.addLine(to: CGPoint(x: 0.7*w, y: h)) 37 | 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Shapes/Pentagon.swift: -------------------------------------------------------------------------------- 1 | // Created by Kieran Brown on 4/8/20. 2 | // Copyright © 2020 BrownandSons. All rights reserved. 3 | 4 | 5 | import SwiftUI 6 | 7 | public struct Pentagon: Shape { 8 | /// Creates a square bottomed pentagon. 9 | public init() {} 10 | 11 | var insetAmount: CGFloat = 0 12 | 13 | public func path(in rect: CGRect) -> Path { 14 | let insetRect: CGRect = rect.insetBy(dx: insetAmount, dy: insetAmount) 15 | let w = insetRect.width 16 | let h = insetRect.height 17 | 18 | return Path { path in 19 | path.move(to: CGPoint(x: w/2, y: 0)) 20 | path.addLine(to: CGPoint(x: 0, y: h/2)) 21 | path.addLine(to: CGPoint(x: 0, y: h)) 22 | path.addLine(to: CGPoint(x: w, y: h)) 23 | path.addLine(to: CGPoint(x: w, y: h/2)) 24 | path.closeSubpath() 25 | } 26 | .offsetBy(dx: insetAmount, dy: insetAmount) 27 | } 28 | } 29 | 30 | extension Pentagon: InsettableShape { 31 | public func inset(by amount: CGFloat) -> some InsettableShape { 32 | var shape = self 33 | shape.insetAmount += amount 34 | return shape 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Shapes/PolarGrid.swift: -------------------------------------------------------------------------------- 1 | // Created by Kieran Brown on 4/8/20. 2 | // Copyright © 2020 BrownandSons. All rights reserved. 3 | 4 | import SwiftUI 5 | 6 | public struct PolarGrid: Shape { 7 | public var rCount: Int 8 | public var thetaCount: Int 9 | public init(rCount: Int, thetaCount: Int) { 10 | self.rCount = rCount 11 | self.thetaCount = thetaCount 12 | } 13 | 14 | public func path(in rect: CGRect) -> Path { 15 | let w = rect.width 16 | let h = rect.height 17 | let maxRadius = w > h ? w/2 : h/2 18 | let thetaIncrement = 2*CGFloat.pi/(CGFloat(thetaCount) + 1) 19 | let radialIncrement = maxRadius/(CGFloat(rCount) + 1) 20 | return Path { path in 21 | // for loop creates lines intersecting the center of the circles dividing the graph into subsections. 22 | for i in 0...(thetaCount + 1) { 23 | let x = 2 * maxRadius 24 | let y = 2 * maxRadius * CGFloat(tan(CGFloat(i) * thetaIncrement)) 25 | let adjustedPoint = CGPoint(x: x + rect.midX, y: y + rect.midY) 26 | let adjustedEnd = CGPoint(x: rect.midX - x, y: rect.midY - y) 27 | path.move(to: adjustedPoint) 28 | path.addLine(to: adjustedEnd) 29 | } 30 | // for loop generates circles of increasing radius. 31 | for i in 0...(self.rCount + 1) { 32 | let radius = radialIncrement * CGFloat(i) 33 | let rect = CGRect(x: rect.midX-radius, y: rect.midY-radius, width: radius*2, height: radius*2) 34 | path.addEllipse(in: rect, transform: .identity) 35 | } 36 | } 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /Sources/Shapes/Polygon.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // https://swiftui-lab.com/swiftui-animations-part1/ 4 | public struct Polygon: Shape { 5 | public var sides: Double 6 | public var scale: Double 7 | public init(sides: Double, scale: Double) { 8 | self.sides = sides 9 | self.scale = scale 10 | } 11 | public var animatableData: AnimatablePair { 12 | get { AnimatablePair(sides, scale) } 13 | set { 14 | sides = newValue.first 15 | scale = newValue.second 16 | } 17 | } 18 | 19 | public func path(in rect: CGRect) -> Path { 20 | // hypotenuse 21 | let hypotenuse = Double(min(rect.size.width, rect.size.height)) / 2.0 * scale 22 | 23 | // center 24 | let center = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) 25 | 26 | var path = Path() 27 | 28 | let extra: Int = sides != Double(Int(sides)) ? 1 : 0 29 | 30 | for i in 0.., CGPoint> { 22 | get { AnimatablePair(AnimatablePair(start, end), control)} 23 | set { 24 | start = newValue.first.first 25 | control = newValue.second 26 | end = newValue.first.second 27 | } 28 | } 29 | 30 | public func path(in rect: CGRect) -> Path { 31 | Path { path in 32 | path.move(to: self.start) 33 | path.addQuadCurve(to: self.end, control: self.control) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Shapes/RadialTickMarks.swift: -------------------------------------------------------------------------------- 1 | // Created by Kieran Brown on 4/8/20. 2 | // Copyright © 2020 BrownandSons. All rights reserved. 3 | 4 | 5 | import SwiftUI 6 | 7 | public struct RadialTickMarks: Shape { 8 | func determineScale(_ i: Int) -> CGFloat { 9 | switch i%6 { 10 | case 1,2,4,5: return 1.05 11 | case 0: return 1.15 12 | case 3: return 1.1 13 | default: return 0 14 | } 15 | } 16 | 17 | public init() {} 18 | 19 | public func path(in rect: CGRect) -> Path { 20 | Path { path in 21 | let r = min(rect.width, rect.height)/2 22 | let mid = CGPoint(x: rect.midX, y: rect.midY) 23 | let values = 0..<24 24 | for i in values { 25 | let angle = .pi*2*CGFloat(i)/24 26 | let x = cos(angle) 27 | let y = sin(angle) 28 | path.move(to: CGPoint(x: 0.95*r*x, y: 0.95*r*y) + mid) 29 | path.addLine(to: CGPoint(x: self.determineScale(i)*r*x, y: self.determineScale(i)*r*y) + mid) 30 | } 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Shapes/Shapes.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A type erased `Shape` 4 | public struct AnyShape: Shape { 5 | private let _makePath: (CGRect) -> Path 6 | 7 | public init(_ shape: S) { 8 | self._makePath = shape.path 9 | } 10 | 11 | public func path(in rect: CGRect) -> Path { 12 | self._makePath(rect) 13 | } 14 | } 15 | 16 | public extension Shape { 17 | func eraseToAnyShape() -> AnyShape { 18 | AnyShape(self) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Shapes/Square.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct Square: Shape { 4 | var insetAmount: CGFloat = 0 5 | /// Creates the largest square that will fit in the containing `CGRect` 6 | public init() {} 7 | 8 | public func path(in rect: CGRect) -> Path { 9 | Path { path in 10 | let rect = rect.insetBy(dx: insetAmount, dy: insetAmount) 11 | let isWidthSmaller = rect.width < rect.height 12 | let length = min(rect.width, rect.height) 13 | let x = isWidthSmaller ? 0 : (rect.width - length)/2 14 | let y = isWidthSmaller ? (rect.height - length)/2 : 0 15 | 16 | path.addRect( 17 | CGRect(x: x, y: y, width: length, height: length) 18 | .offsetBy(dx: rect.minX, dy: rect.minY) 19 | ) 20 | } 21 | } 22 | } 23 | 24 | extension Square: InsettableShape { 25 | public func inset(by amount: CGFloat) -> some InsettableShape { 26 | var shape = self 27 | shape.insetAmount += amount 28 | return shape 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Shapes/TangentArc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TangentArc.swift 3 | // UX Masterclass 4 | // 5 | // Created by Kieran Brown on 7/11/20. 6 | // Copyright © 2020 Kieran Brown. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public struct TangentArc: Shape { 12 | public var start: CGPoint 13 | public var tangent1End: CGPoint 14 | public var tangent2End: CGPoint 15 | public var radius: CGFloat 16 | 17 | public init(start: CGPoint, tangent1End: CGPoint, tangent2End: CGPoint, radius: CGFloat) { 18 | self.start = start 19 | self.tangent1End = tangent1End 20 | self.tangent2End = tangent2End 21 | self.radius = radius 22 | } 23 | 24 | public var animatableData: AnimatablePair, 25 | AnimatablePair> { 26 | get { 27 | AnimatablePair( 28 | AnimatablePair(self.start, self.radius), 29 | AnimatablePair(self.tangent1End, self.tangent2End) 30 | ) 31 | } 32 | set { 33 | self.start = newValue.first.first 34 | self.radius = newValue.first.second 35 | self.tangent1End = newValue.second.first 36 | self.tangent2End = newValue.second.second 37 | } 38 | } 39 | 40 | public func path(in rect: CGRect) -> Path { 41 | Path { path in 42 | path.move(to: start) 43 | path.addArc(tangent1End: tangent1End, tangent2End: tangent2End, radius: radius) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Shapes/TickMarks.swift: -------------------------------------------------------------------------------- 1 | // Created by Kieran Brown on 4/8/20. 2 | // Copyright © 2020 BrownandSons. All rights reserved. 3 | 4 | 5 | import SwiftUI 6 | 7 | public struct TickMarks: Shape { 8 | public var spacing: CGFloat 9 | public var ticks: Int 10 | public var isVertical: Bool 11 | 12 | /// Creates `ticks` number of tickmarks of varying sizes each spaced apart with the given `spacing` 13 | /// 14 | /// ## Example Usage 15 | /// ``` 16 | /// struct Ruler: View { 17 | /// var length: CGFloat = 500 18 | /// var tickSpacing: CGFloat = 10 19 | /// var tickColor: Color = Color(white: 0.9) 20 | /// var majorTickLength: CGFloat = 30 21 | /// var tickBackgroundColor: Color = Color(white: 0.1) 22 | /// var nonTickBackgroundColor: Color = Color(white: 0.2) 23 | /// var rulerWidth: CGFloat = 80 24 | /// var cornerRadius: CGFloat = 5 25 | /// 26 | /// var body: some View { 27 | /// HStack(spacing: 0) { 28 | /// TickMarks(spacing: tickSpacing, 29 | /// ticks: Int(length/tickSpacing), 30 | /// isVertical: true) 31 | /// .stroke(tickColor) 32 | /// .frame(width: majorTickLength) 33 | /// .padding(.vertical, 6) 34 | /// .padding(.leading, 4) 35 | /// .padding(.trailing, 5) 36 | /// .background( 37 | /// OmniRectangle(topLeft: .round(radius: cornerRadius), 38 | /// topRight: .square, 39 | /// bottomLeft: .round(radius: cornerRadius), 40 | /// bottomRight: .square) 41 | /// .fill(tickBackgroundColor) 42 | /// ) 43 | /// OmniRectangle(topLeft: .square, 44 | /// topRight: .round(radius: cornerRadius), 45 | /// bottomLeft: .square, 46 | /// bottomRight: .round(radius: cornerRadius)) 47 | /// .fill(nonTickBackgroundColor) 48 | /// .frame(width: rulerWidth - majorTickLength) 49 | /// } 50 | /// .frame(height: length) 51 | /// .clipped() 52 | /// } 53 | /// } 54 | ///``` 55 | public init(spacing: CGFloat, ticks: Int, isVertical: Bool = false) { 56 | self.spacing = spacing 57 | self.ticks = ticks 58 | self.isVertical = isVertical 59 | } 60 | 61 | func determineHeight(_ i: Int) -> CGFloat { 62 | if i%100 == 0 { return 1 } 63 | if i%10 == 0 { return 0.75 } 64 | if i%5 == 0 { return 0.5 } 65 | return 0.25 66 | } 67 | 68 | private func verticalPath(in rect: CGRect) -> Path { 69 | Path { path in 70 | for i in 0...ticks { 71 | path.move(to: CGPoint(x: 0, y: CGFloat(i)*spacing)) 72 | path.addLine(to: CGPoint(x: rect.width*self.determineHeight(i), y: CGFloat(i)*spacing)) 73 | } 74 | } 75 | } 76 | 77 | private func horizontalPath(in rect: CGRect) -> Path { 78 | Path { path in 79 | for i in 0...ticks { 80 | path.move(to: CGPoint(x: CGFloat(i)*spacing, y: 0)) 81 | path.addLine(to: CGPoint(x: CGFloat(i)*spacing, y: rect.height*self.determineHeight(i))) 82 | } 83 | } 84 | } 85 | 86 | public func path(in rect: CGRect) -> Path { 87 | isVertical ? verticalPath(in: rect) : horizontalPath(in: rect) 88 | } 89 | } 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /Sources/Shapes/Trapezoid.swift: -------------------------------------------------------------------------------- 1 | // Swift toolchain version 5.0 2 | // Running macOS version 10.15 3 | // Created on 11/14/20. 4 | // 5 | // Author: Kieran Brown 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct Trapezoid: Shape { 11 | private var insetAmount: CGFloat = 0 12 | private var baseRatio: CGFloat 13 | private var curvature: CGFloat 14 | 15 | public var animatableData: AnimatablePair { 16 | get { AnimatablePair(baseRatio, curvature) } 17 | set { 18 | self.baseRatio = newValue.first 19 | self.curvature = newValue.second 20 | } 21 | } 22 | 23 | /// Creates a trapezoid with a large bottom and small top 24 | /// - Parameters: 25 | /// - baseRatio: The ratio of the top's length/bottom's length. Any number less than 0 will be treated as 0, which looks like a triangle. 26 | /// - curvature: a postive value makes the left and right edges curve inward. a negative value makes them curve outward. 0 is a straight line. 27 | public init(baseRatio: CGFloat, curvature: CGFloat = 0) { 28 | self.baseRatio = baseRatio 29 | self.curvature = curvature 30 | } 31 | 32 | public func path(in rect: CGRect) -> Path { 33 | // let ratio = max(min(baseRatio, 1), 0) 34 | let ratio = max(baseRatio, 0) 35 | let insetRect: CGRect = rect.insetBy(dx: insetAmount, dy: insetAmount) 36 | let w = insetRect.width 37 | let h = insetRect.height 38 | let d = (w-w*ratio)/2 39 | 40 | return Path { path in 41 | path.move(to: CGPoint(x: 0, y: h)) 42 | path.addQuadCurve(to: CGPoint(x: d, y: 0), 43 | control: CGPoint(x: (w/2 - d/2)*curvature + d/2, y: h/2)) 44 | path.addLine(to: CGPoint(x: w-d, y: 0)) 45 | path.addQuadCurve(to: CGPoint(x: w, y: h), 46 | control: CGPoint(x: (d/2 - w/2)*curvature + w - d/2, y: h/2)) 47 | path.closeSubpath() 48 | } 49 | .offsetBy(dx: insetAmount, dy: insetAmount) 50 | } 51 | } 52 | 53 | extension Trapezoid: InsettableShape { 54 | public func inset(by amount: CGFloat) -> some InsettableShape { 55 | var shape = self 56 | shape.insetAmount += amount 57 | return shape 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Shapes/Triangles.swift: -------------------------------------------------------------------------------- 1 | // Created by Kieran Brown on 3/21/20. 2 | // Copyright © 2020 BrownandSons. All rights reserved. 3 | 4 | import SwiftUI 5 | 6 | public struct Triangle: Shape { 7 | private var insetAmount: CGFloat = 0 8 | var leftEdgeCurvature: CGFloat 9 | var rightEdgeCurvature: CGFloat 10 | var bottomEdgeCurvature: CGFloat 11 | 12 | /// Creates a Triangle with congreunt left and right edges. 13 | /// If the containing rectangle is a square then the Triangle will be equilateral, 14 | /// 15 | /// - Parameters: 16 | /// - leftEdgeCurvature: The curvature value of the left edge positive values curve the edge inwards while postive values curve outwards 17 | /// - rightEdgeCurvature: The curvature value of the right edge positive values curve the edge inwards while postive values curve outwards 18 | /// - bottomEdgeCurvature: The curvature value of the top edge positive values curve the edge inwards while postive values curve outwards 19 | public init(leftEdgeCurvature: CGFloat = 0, 20 | rightEdgeCurvature: CGFloat = 0, 21 | bottomEdgeCurvature: CGFloat = 0) { 22 | self.leftEdgeCurvature = leftEdgeCurvature 23 | self.rightEdgeCurvature = rightEdgeCurvature 24 | self.bottomEdgeCurvature = bottomEdgeCurvature 25 | } 26 | 27 | public func path(in rect: CGRect) -> Path { 28 | let insetRect: CGRect = rect.insetBy(dx: insetAmount, dy: insetAmount) 29 | let w = insetRect.width 30 | let h = insetRect.height 31 | 32 | return Path { path in 33 | path.move(to: CGPoint(x: 0, y: h)) 34 | path.addQuadCurve(to: CGPoint(x: w/2, y: 0), 35 | control: CGPoint(x: w*leftEdgeCurvature/4 + w/4, y: h/2)) 36 | path.addQuadCurve(to: CGPoint(x: w, y: h), 37 | control: CGPoint(x: 3*w/4 - w*rightEdgeCurvature/4, y: h/2)) 38 | path.addQuadCurve(to: CGPoint(x: 0, y: h), 39 | control: CGPoint(x: w/2, y: h - h*bottomEdgeCurvature/4)) 40 | path.closeSubpath() 41 | } 42 | .offsetBy(dx: insetAmount, dy: insetAmount) 43 | } 44 | } 45 | 46 | extension Triangle: InsettableShape { 47 | public func inset(by amount: CGFloat) -> some InsettableShape { 48 | var shape = self 49 | shape.insetAmount += amount 50 | return shape 51 | } 52 | } 53 | 54 | extension Triangle { 55 | public init(curvature: CGFloat = 0) { 56 | self.leftEdgeCurvature = curvature 57 | self.rightEdgeCurvature = curvature 58 | self.bottomEdgeCurvature = curvature 59 | } 60 | } 61 | 62 | extension Triangle { 63 | /// A Triangle that is curved to look like the tip of a bullet 64 | public static func bulletTip() -> Triangle { 65 | Triangle(leftEdgeCurvature: -1, 66 | rightEdgeCurvature: -1, 67 | bottomEdgeCurvature: 0) 68 | } 69 | } 70 | 71 | public struct OpenTriangle: Shape { 72 | public init() {} 73 | public func path(in rect: CGRect) -> Path { 74 | Path { (path) in 75 | let w = rect.width 76 | let h = rect.height 77 | path.move(to: CGPoint(x: 0, y: 0)) 78 | path.addLine(to: CGPoint(x: w, y: h/2)) 79 | path.addLine(to: CGPoint(x: 0, y: h)) 80 | } 81 | } 82 | } 83 | 84 | public struct RightTriangle: Shape { 85 | public init() {} 86 | public func path(in rect: CGRect) -> Path { 87 | Path { (path) in 88 | let w = rect.width 89 | let h = rect.height 90 | 91 | path.move(to: CGPoint(x: 0, y: 0)) 92 | path.addLine(to: CGPoint(x: 0, y: h)) 93 | path.addLine(to: CGPoint(x: w, y: h)) 94 | path.closeSubpath() 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/Shapes/VerticalLine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerticalLine.swift 3 | // 4 | // 5 | // Created by Kieran Brown on 10/15/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct VerticalLine: Shape { 11 | var offset: CGFloat 12 | 13 | /// A Vertical line that is the height of its container 14 | /// - parameter offset: A value between 0 and 1 defining the line's horizontal offset in its container (**Default**: 0.5) 15 | public init(offset: CGFloat = 0.5) { 16 | self.offset = offset 17 | } 18 | 19 | public func path(in rect: CGRect) -> Path { 20 | Path { path in 21 | path.move(to: CGPoint(x: rect.maxX*offset, y: 0)) 22 | path.addLine(to: CGPoint(x: rect.maxX*offset, y: rect.height)) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import ShapesTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += ShapesTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/ShapesTests/ShapesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Shapes 3 | 4 | final class ShapesTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ShapesTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(ShapesTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------