├── .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 |
8 |
9 |
10 |
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 |
--------------------------------------------------------------------------------