diff --git a/Package.swift b/Package.swift index 7100611..daae3c6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,24 +1,20 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.3 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Shapes", + platforms: [.iOS(.v13), .macOS(.v10_15), .watchOS(.v6), .tvOS(.v13)], products: [ - // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "Shapes", targets: ["Shapes"]), ], dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), .package(url: "https://github.com/kieranb662/CGExtender.git", from: "1.0.1") ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "Shapes", dependencies: ["CGExtender"]), diff --git a/Sources/Shapes/AdaptiveLine.swift b/Sources/Shapes/AdaptiveLine.swift new file mode 100644 index 0000000..efeeae3 --- /dev/null +++ b/Sources/Shapes/AdaptiveLine.swift @@ -0,0 +1,83 @@ +// +// AdaptiveLine.swift +// +// +// Created by Kieran Brown on 10/15/20. +// + +import SwiftUI +import CGExtender + +public struct AdaptiveLine: Shape { + public var angle: Angle + public var animatableData: Angle { + get { self.angle } + set { self.angle = newValue } + } + var insetAmount: CGFloat = 0 + + + /// # Adaptive Line + /// + /// This shape creates a line centered inside of and constrained by its bounding box. + /// The end points of the line are the points of intersection of an infinitely long angled line and the container rectangle + /// - Parameters: + /// - angle: The angle of the adaptive line with `.zero` pointing in the positive X direction + /// + /// ## Example Usage + /// + /// ``` + /// struct AdaptiveLineExample: View { + /// @State var angle: Double = 0.5 + /// + /// var body: some View { + /// ZStack { + /// Color(white: 0.1).edgesIgnoringSafeArea(.all) + /// VStack { + /// AdaptiveLine(angle: Angle(degrees: angle*360)) + /// .stroke(Color.white, + /// style: StrokeStyle(lineWidth: 30, lineCap: .round)) + /// Slider(value: $angle) + /// } + /// .padding() + /// } + /// } + /// } + /// ``` + public init(angle: Angle = .zero) { + self.angle = angle + } + + public func path(in rect: CGRect) -> Path { + let rect = rect.insetBy(dx: insetAmount, dy: insetAmount) + let w = rect.width + let h = rect.height + let big: CGFloat = 5000000 + + let x1 = w/2 + big*CGFloat(cos(self.angle.radians)) + let y1 = h/2 + big*CGFloat(sin(self.angle.radians)) + let x2 = w/2 - big*CGFloat(cos(self.angle.radians)) + let y2 = h/2 - big*CGFloat(sin(self.angle.radians)) + + let points = lineRectIntersection(x1, y1, x2, y2, rect.minX, rect.minY, w, h) + if points.count < 2 { + return Path { p in + p.move(to: CGPoint(x: rect.minX, y: rect.midY)) + p.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) + } + } + + return Path { p in + p.move(to: points[0]) + p.addLine(to: points[1]) + } + } +} + +extension AdaptiveLine: InsettableShape { + public func inset(by amount: CGFloat) -> some InsettableShape { + var shape = self + shape.insetAmount += amount + return shape + } +} diff --git a/Sources/Shapes/Arrow.swift b/Sources/Shapes/Arrow.swift index b3068ba..7923681 100644 --- a/Sources/Shapes/Arrow.swift +++ b/Sources/Shapes/Arrow.swift @@ -3,22 +3,41 @@ import SwiftUI - - -/// An arrow that when small is shaped like this **|--|** but when gets longer looks like this **<---->** -/// - parameters: -/// - arrowOffset: The percent `[0,1]` horizontal offset of the arrow in its container -/// - length: The length of the arrow -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) public struct Arrow: Shape { public var arrowOffset: CGFloat public var length: CGFloat - + + /// An arrow that when small is shaped like this **|--|** but when gets longer looks like this **<---->** + /// - parameters: + /// - arrowOffset: The percent `[0,1]` horizontal offset of the arrow in its container + /// - length: The length of the arrow + /// + /// ## Example usage + /// ``` + /// struct ExpandingArrowExample: View { + /// @State var val: Double = 10 + /// @State var isHidden: Bool = false + /// + /// var body: some View { + /// VStack { + /// HStack(spacing: 0) { + /// ForEach(1..<9) { (i) in + /// Arrow(arrowOffset: self.val > 100 ? 1/(2*1.414) : 0, length: CGFloat(self.val)) + /// .stroke(Color("Light Green")).animation(.easeIn(duration: Double(i)/4.0)) + /// .frame(width: 40) + /// } + /// } + /// .frame(height: 300) + /// Slider(value: $val, in: 1...250).padding() + /// } + /// } + /// } + /// ``` public init(arrowOffset: CGFloat, length: CGFloat) { self.arrowOffset = arrowOffset self.length = length } - + public var animatableData: AnimatablePair { get { AnimatablePair(arrowOffset, length) } set { @@ -26,48 +45,22 @@ public struct Arrow: Shape { length = newValue.second } } - + public func path(in rect: CGRect) -> Path { Path { path in let w = rect.width let h = rect.height - + path.move(to: CGPoint(x: w/2, y: h/2 - self.length/2)) path.addLine(to: CGPoint(x: w/2, y: h/2 + self.length/2)) - + 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)) path.addLine(to: CGPoint(x: w/2, y: h/2 - self.length/2)) 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)) - + 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)) path.addLine(to: CGPoint(x: w/2, y: h/2 + self.length/2)) 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)) - - - } - } - - -} -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) -public struct ExpandingArrowExample: View { - @State var val: Double = 10 - @State var isHidden: Bool = false - - public init() { - - } - public var body: some View { - VStack { - HStack(spacing: 0) { - ForEach(1..<9) { (i) in - Arrow(arrowOffset: self.val > 100 ? 1/(2*1.414) : 0, length: CGFloat(self.val)) - .stroke(Color("Light Green")).animation(.easeIn(duration: Double(i)/4.0)) - .frame(width: 40) - } - }.frame(height: 300) - Slider(value: $val, in: 1...250).padding() - } } } diff --git a/Sources/Shapes/CartesianGrid.swift b/Sources/Shapes/CartesianGrid.swift index 7f5419e..9bc5af6 100644 --- a/Sources/Shapes/CartesianGrid.swift +++ b/Sources/Shapes/CartesianGrid.swift @@ -4,23 +4,21 @@ import SwiftUI - -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) public struct CartesianGrid: Shape { public var xCount: Int public var yCount: Int - + public init(xCount: Int, yCount: Int) { self.xCount = xCount self.yCount = yCount } - + public func path(in rect: CGRect) -> Path { let w = rect.width let h = rect.height let rangeX = 1...xCount let rangeY = 1...yCount - + return Path { path in for i in rangeX { path.move(to: CGPoint(x: CGFloat(i)*w/CGFloat(self.xCount), y: 0)) @@ -33,4 +31,3 @@ public struct CartesianGrid: Shape { } } } - diff --git a/Sources/Shapes/CircularArc.swift b/Sources/Shapes/CircularArc.swift index b42ff83..da897ae 100644 --- a/Sources/Shapes/CircularArc.swift +++ b/Sources/Shapes/CircularArc.swift @@ -15,15 +15,15 @@ public struct CircularArc: Shape { public var startAngle: Angle public var endAngle: Angle public var clockwise: Bool - + public init(start: CGPoint, center: CGPoint, radius: CGFloat, startAngle: Angle, endAngle: Angle, clockwise: Bool) { self.start = start self.center = center - self.radius = radius + self.radius = radius self.startAngle = startAngle self.endAngle = endAngle self.clockwise = clockwise - + } public var animatableData: AnimatablePair, AnimatablePair>, CGFloat> { diff --git a/Sources/Shapes/CubicBezierShape.swift b/Sources/Shapes/CubicBezierShape.swift index 1ecf539..4287718 100644 --- a/Sources/Shapes/CubicBezierShape.swift +++ b/Sources/Shapes/CubicBezierShape.swift @@ -33,9 +33,7 @@ public struct CubicBezierShape: Shape { public func path(in rect: CGRect) -> Path { Path { path in path.move(to: self.start) - path.addCurve(to: self.end, - control1: self.control1, - control2: self.control2) + path.addCurve(to: self.end,control1: self.control1, control2: self.control2) } } } diff --git a/Sources/Shapes/FoldableShape.swift b/Sources/Shapes/FoldableShape.swift index 6dc71f3..d2d2295 100644 --- a/Sources/Shapes/FoldableShape.swift +++ b/Sources/Shapes/FoldableShape.swift @@ -8,8 +8,6 @@ import SwiftUI - -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) extension Path { /// The array of `Path.Elements` describing the path var elements: [Path.Element] { @@ -19,13 +17,13 @@ extension Path { } return temp } - + /// Returns the starting point of the path func getStartPoint() -> CGPoint? { if isEmpty { return nil } - + guard let first = elements.first(where: { switch $0 { case .move(_): @@ -36,7 +34,7 @@ extension Path { }) else { return nil } - + switch first { case .move(let to): return to @@ -44,13 +42,13 @@ extension Path { return nil } } - + /// Returns the last point on the path rom the last curve command func getEndPoint() -> CGPoint? { if isEmpty { return nil } - + guard let last = elements.reversed().first(where: { (element) in switch element { case .line(_), .quadCurve(_, _), .curve(_, _, _): @@ -61,40 +59,32 @@ extension Path { }) else { return nil } - + switch last { case .line(let to), .quadCurve(let to, _), .curve(let to, _, _): return to default: return nil - + } } } - - /// # Foldable Shape /// A Shape which can be folded over itself. -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) public struct FoldableShape: View { var shape: S - var mainColor: Color = .yellow - var foldColor: Color = .pink + var mainColor: Color + var foldColor: Color var fraction: CGFloat - - public init(_ shape: S, fraction: CGFloat) { - self.shape = shape - self.fraction = fraction - } - - public init(_ shape: S, fraction: CGFloat, mainColor: Color, foldColor: Color) { + + public init(_ shape: S, fraction: CGFloat, mainColor: Color = .yellow, foldColor: Color = .pink) { self.shape = shape self.fraction = fraction self.mainColor = mainColor self.foldColor = foldColor } - + /// Function reflects a point over the line that crosses between r1 and r2 private func reflect(_ r1: CGPoint, _ r2: CGPoint, _ p: CGPoint) -> CGPoint { let a = (r2.y-r1.y) @@ -104,13 +94,13 @@ public struct FoldableShape: View { let ai = a/magnitude let bi = b/magnitude let ci = c/magnitude - + let d = ai*p.x + bi*p.y + ci let x = p.x - 2*ai*d let y = p.y - 2*bi*d return CGPoint(x: x, y: y) } - + /// Creates the folded portion of the path given the large and small fractions which define line that the portion is folded over. /// Procedure: /// 1. Get the fraction of the path that will serve as the folded peice @@ -120,8 +110,8 @@ public struct FoldableShape: View { let pathFraction = path.trimmedPath(from: fraction, to: 1) let start = pathFraction.getStartPoint() ?? .zero let end = pathFraction.getEndPoint() ?? .zero - - + + return Path { path in pathFraction.forEach { element in switch element.self { @@ -142,11 +132,12 @@ public struct FoldableShape: View { } } } - + public var body: some View { GeometryReader { (proxy: GeometryProxy) in ZStack { - self.shape.path(in: proxy.frame(in: .global)).trimmedPath(from: 0, to: self.fraction) + self.shape.path(in: proxy.frame(in: .global)) + .trimmedPath(from: 0, to: self.fraction) .foregroundColor(self.mainColor) .opacity(0.4) self.makeFold(path: self.shape.path(in: proxy.frame(in: .global))) diff --git a/Sources/Shapes/HorizontalLine.swift b/Sources/Shapes/HorizontalLine.swift new file mode 100644 index 0000000..659d4f1 --- /dev/null +++ b/Sources/Shapes/HorizontalLine.swift @@ -0,0 +1,25 @@ +// +// HorizontalLine.swift +// +// +// Created by Kieran Brown on 10/15/20. +// + +import SwiftUI + +public struct HorizontalLine: Shape { + var offset: CGFloat + + /// A horizontal line that is the width of its container + /// - parameter offset: A value between 0 and 1 defining the lines vertical offset in its container (**Default**: 0.5) + public init(offset: CGFloat = 0.5) { + self.offset = offset + } + + public func path(in rect: CGRect) -> Path { + Path { path in + path.move(to: CGPoint(x: 0, y: rect.maxY*offset)) + path.addLine(to: CGPoint(x: rect.width, y: rect.maxY*offset)) + } + } +} diff --git a/Sources/Shapes/Infinity.swift b/Sources/Shapes/Infinity.swift index bbf1874..0dcb7d6 100644 --- a/Sources/Shapes/Infinity.swift +++ b/Sources/Shapes/Infinity.swift @@ -1,33 +1,52 @@ import SwiftUI // https://swiftui-lab.com/swiftui-animations-part2/ -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) public struct InfinitySymbol: Shape { public init() {} + var insetAmount: CGFloat = 0 public func path(in rect: CGRect) -> Path { - return InfinitySymbol.createInfinityPath(in: rect) + createInfinityPath(in: rect) + .offsetBy(dx: insetAmount, dy: insetAmount) } - public static func createInfinityPath(in rect: CGRect) -> Path { - let height = rect.size.height - let width = rect.size.width + public func createInfinityPath(in rect: CGRect) -> Path { + let rect = rect.insetBy(dx: insetAmount, dy: insetAmount) + let height = rect.height + let width = rect.width let heightFactor = height/4 let widthFactor = width/4 var path = Path() path.move(to: CGPoint(x:widthFactor, y: heightFactor * 3)) - path.addCurve(to: CGPoint(x:widthFactor, y: heightFactor), control1: CGPoint(x:0, y: heightFactor * 3), control2: CGPoint(x:0, y: heightFactor)) + path.addCurve(to: CGPoint(x:widthFactor, y: heightFactor), + control1: CGPoint(x:0, y: heightFactor * 3), + control2: CGPoint(x:0, y: heightFactor)) path.move(to: CGPoint(x:widthFactor, y: heightFactor)) - path.addCurve(to: CGPoint(x:widthFactor * 3, y: heightFactor * 3), control1: CGPoint(x:widthFactor * 2, y: heightFactor), control2: CGPoint(x:widthFactor * 2, y: heightFactor * 3)) + path.addCurve(to: CGPoint(x:widthFactor * 3, y: heightFactor * 3), + control1: CGPoint(x:widthFactor * 2, y: heightFactor), + control2: CGPoint(x:widthFactor * 2, y: heightFactor * 3)) path.move(to: CGPoint(x:widthFactor * 3, y: heightFactor * 3)) - path.addCurve(to: CGPoint(x:widthFactor * 3, y: heightFactor), control1: CGPoint(x:widthFactor * 4 + 5, y: heightFactor * 3), control2: CGPoint(x:widthFactor * 4 + 5, y: heightFactor)) + path.addCurve(to: CGPoint(x:widthFactor * 3, y: heightFactor), + control1: CGPoint(x:widthFactor * 4 + 5, y: heightFactor * 3), + control2: CGPoint(x:widthFactor * 4 + 5, y: heightFactor)) path.move(to: CGPoint(x:widthFactor * 3, y: heightFactor)) - path.addCurve(to: CGPoint(x:widthFactor, y: heightFactor * 3), control1: CGPoint(x:widthFactor * 2, y: heightFactor), control2: CGPoint(x:widthFactor * 2, y: heightFactor * 3)) + path.addCurve(to: CGPoint(x:widthFactor, y: heightFactor * 3), + control1: CGPoint(x:widthFactor * 2, y: heightFactor), + control2: CGPoint(x:widthFactor * 2, y: heightFactor * 3)) return path } } + +extension InfinitySymbol: InsettableShape { + public func inset(by amount: CGFloat) -> some InsettableShape { + var shape = self + shape.insetAmount += amount + return shape + } +} + diff --git a/Sources/Shapes/Lines.swift b/Sources/Shapes/Lines.swift index 0705bf9..52ee8e5 100644 --- a/Sources/Shapes/Lines.swift +++ b/Sources/Shapes/Lines.swift @@ -5,109 +5,26 @@ // Copyright © 2020 BrownandSons. All rights reserved. import SwiftUI -import CGExtender -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) public struct Line: Shape { public var from: CGPoint public var to: CGPoint public init(from: CGPoint, to: CGPoint) { self.from = from - self.to = to + self.to = to } public var animatableData: AnimatablePair { get { AnimatablePair(from, to) } - set { + set { from = newValue.first to = newValue.second } } - public func path(in rect: CGRect) -> Path { - Path { p in - p.move(to: self.from) - p.addLine(to: self.to) - - } - } -} -// MARK: - Horizontal Line -/// A horizontal line that is the width of its container -/// - parameter offset: A value between 0 and 1 defining the lines vertical offset in its container (**Default**: 0.5) -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) -public struct HorizontalLine: Shape { - var offset: CGFloat = 0.5 - public init() {} - public init(offset: CGFloat) { - self.offset = offset - } - public func path(in rect: CGRect) -> Path { - Path { path in - path.move(to: CGPoint(x: 0, y: rect.maxY*offset)) - path.addLine(to: CGPoint(x: rect.width, y: rect.maxY*offset)) - } - } -} -// MARK: - Vertical Line -/// A Vertical line that is the height of its container -/// - parameter offset: A value between 0 and 1 defining the line's horizontal offset in its container (**Default**: 0.5) -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) -public struct VerticalLine: Shape { - var offset: CGFloat = 0.5 - public init() {} - public init(offset: CGFloat) { - self.offset = offset - } public func path(in rect: CGRect) -> Path { Path { path in - path.move(to: CGPoint(x: rect.maxX*offset, y: 0)) - path.addLine(to: CGPoint(x: rect.maxX*offset, y: rect.height)) - } - } -} - - -// MARK: - Adaptive Line -/// # Adaptive Line -/// -/// This shape creates a line centered inside of and constrained by its bounding box. -/// The end points of the line are the points of intersection of an infinitely long angled line and the container rectangle -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) -public struct AdaptiveLine: Shape { - public var angle: Angle - public var animatableData: Angle { - get { - self.angle - } - set { - self.angle = newValue - } - } - public init(angle: Angle) { - self.angle = angle - } - - public func path(in rect: CGRect) -> Path { - let w = rect.width - let h = rect.height - let big: CGFloat = 5000000 - - let x1 = w/2 + big*CGFloat(cos(self.angle.radians)) - let y1 = h/2 + big*CGFloat(sin(self.angle.radians)) - let x2 = w/2 - big*CGFloat(cos(self.angle.radians)) - let y2 = h/2 - big*CGFloat(sin(self.angle.radians)) - - let points = lineRectIntersection(x1, y1, x2, y2, 0, 0, w, h) - if points.count < 2 { - return Path { p in - p.move(to: CGPoint(x: 0, y: rect.midY)) - p.addLine(to: CGPoint(x: rect.width, y: rect.midY)) - } - } - - return Path { p in - p.move(to: points[0]) - p.addLine(to: points[1]) + path.move(to: self.from) + path.addLine(to: self.to) } } } diff --git a/Sources/Shapes/PathText.swift b/Sources/Shapes/PathText.swift index 7237b9b..4da1b03 100644 --- a/Sources/Shapes/PathText.swift +++ b/Sources/Shapes/PathText.swift @@ -4,7 +4,6 @@ import SwiftUI -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) public struct PathText: Shape { public init() {} public func path(in rect: CGRect) -> Path { @@ -39,4 +38,3 @@ public struct PathText: Shape { } } } - diff --git a/Sources/Shapes/Pentagon.swift b/Sources/Shapes/Pentagon.swift index 0c753d9..e01d1bc 100644 --- a/Sources/Shapes/Pentagon.swift +++ b/Sources/Shapes/Pentagon.swift @@ -4,22 +4,28 @@ import SwiftUI -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) public struct Pentagon: Shape { public init() {} + var insetAmount: CGFloat = 0 public func path(in rect: CGRect) -> Path { let w = rect.width let h = rect.height return Path { path in - path.move(to: CGPoint(x: w/2, y: 0)) - path.addLine(to: CGPoint(x: 0, y: h/2)) - path.addLine(to: CGPoint(x: 0, y: h)) - path.addLine(to: CGPoint(x: w, y: h)) - path.addLine(to: CGPoint(x: w, y: h/2)) + path.move(to: CGPoint(x: w/2, y: insetAmount)) + path.addLine(to: CGPoint(x: insetAmount, y: h/2)) + path.addLine(to: CGPoint(x: insetAmount, y: h - insetAmount)) + path.addLine(to: CGPoint(x: w - insetAmount, y: h - insetAmount)) + path.addLine(to: CGPoint(x: w - insetAmount, y: h/2)) path.closeSubpath() } } } - +extension Pentagon: InsettableShape { + public func inset(by amount: CGFloat) -> some InsettableShape { + var shape = self + shape.insetAmount += amount + return shape + } +} diff --git a/Sources/Shapes/PolarGrid.swift b/Sources/Shapes/PolarGrid.swift index 2eda6db..69d12e9 100644 --- a/Sources/Shapes/PolarGrid.swift +++ b/Sources/Shapes/PolarGrid.swift @@ -1,10 +1,8 @@ // Created by Kieran Brown on 4/8/20. // Copyright © 2020 BrownandSons. All rights reserved. - import SwiftUI -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) public struct PolarGrid: Shape { public var rCount: Int public var thetaCount: Int @@ -12,7 +10,7 @@ public struct PolarGrid: Shape { self.rCount = rCount self.thetaCount = thetaCount } - + public func path(in rect: CGRect) -> Path { let w = rect.width let h = rect.height diff --git a/Sources/Shapes/Polygon.swift b/Sources/Shapes/Polygon.swift index e61e521..df9b7d3 100644 --- a/Sources/Shapes/Polygon.swift +++ b/Sources/Shapes/Polygon.swift @@ -1,12 +1,12 @@ import SwiftUI + // https://swiftui-lab.com/swiftui-animations-part1/ -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) public struct Polygon: Shape { public var sides: Double public var scale: Double public init(sides: Double, scale: Double) { self.sides = sides - self.scale = scale + self.scale = scale } public var animatableData: AnimatablePair { get { AnimatablePair(sides, scale) } @@ -15,33 +15,34 @@ public struct Polygon: Shape { scale = newValue.second } } - + public func path(in rect: CGRect) -> Path { // hypotenuse - let h = Double(min(rect.size.width, rect.size.height)) / 2.0 * scale - + let hypotenuse = Double(min(rect.size.width, rect.size.height)) / 2.0 * scale + // center - let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) - + let center = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) + var path = Path() - + let extra: Int = sides != Double(Int(sides)) ? 1 : 0 - + for i in 0.. Path { Path { path in path.move(to: self.start) - path.addQuadCurve(to: self.end, - control: self.control) + path.addQuadCurve(to: self.end, control: self.control) } } } diff --git a/Sources/Shapes/RadialTickMarks.swift b/Sources/Shapes/RadialTickMarks.swift index 053f976..0ad5f15 100644 --- a/Sources/Shapes/RadialTickMarks.swift +++ b/Sources/Shapes/RadialTickMarks.swift @@ -3,7 +3,7 @@ import SwiftUI -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) + public struct RadialTickMarks: Shape { func determineScale(_ i: Int) -> CGFloat { switch i%6 { @@ -13,8 +13,9 @@ public struct RadialTickMarks: Shape { default: return 0 } } - + public init() {} + public func path(in rect: CGRect) -> Path { Path { path in let r = min(rect.width, rect.height)/2 @@ -29,6 +30,5 @@ public struct RadialTickMarks: Shape { } } } - -} +} diff --git a/Sources/Shapes/Shapes.swift b/Sources/Shapes/Shapes.swift index 3036d65..d8912d7 100644 --- a/Sources/Shapes/Shapes.swift +++ b/Sources/Shapes/Shapes.swift @@ -1,11 +1,9 @@ import SwiftUI /// A type erased `Shape` -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) public struct AnyShape: Shape { private let _makePath: (CGRect) -> Path - - @available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) + public init(_ shape: S) { self._makePath = shape.path } @@ -14,7 +12,7 @@ public struct AnyShape: Shape { self._makePath(rect) } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) + public extension Shape { func eraseToAnyShape() -> AnyShape { AnyShape(self) diff --git a/Sources/Shapes/Square.swift b/Sources/Shapes/Square.swift index 8e944f7..f7166bb 100644 --- a/Sources/Shapes/Square.swift +++ b/Sources/Shapes/Square.swift @@ -1,34 +1,30 @@ import SwiftUI - public struct Square: Shape { + /// Creates the largest square that will fit in the containing `CGRect` public init() {} - public func path(in rect: CGRect) -> Path { + var insetAmount: CGFloat = 0 + public func path(in rect: CGRect) -> Path { Path { path in - + let rect = rect.insetBy(dx: insetAmount, dy: insetAmount) + let isWidthSmaller = rect.width < rect.height let length = min(rect.width, rect.height) - - let x = ( - rect.width < rect.height - ? 0 - : (rect.width - length)/2 - ) + rect.minX - - let y = ( - rect.width < rect.height - ? (rect.height - length)/2 - : 0 - ) + rect.minY + let x = isWidthSmaller ? 0 : (rect.width - length)/2 + let y = isWidthSmaller ? (rect.height - length)/2 : 0 path.addRect( - CGRect( - x: x , - y: y , - width: length, - height: length - ) + CGRect(x: x, y: y, width: length, height: length) + .offsetBy(dx: rect.minX, dy: rect.minY) ) } } } + +extension Square: InsettableShape { + public func inset(by amount: CGFloat) -> some InsettableShape { + var shape = self + shape.insetAmount += amount + return shape + } +} diff --git a/Sources/Shapes/TangentArc.swift b/Sources/Shapes/TangentArc.swift index 5293c71..252a669 100644 --- a/Sources/Shapes/TangentArc.swift +++ b/Sources/Shapes/TangentArc.swift @@ -13,21 +13,28 @@ public struct TangentArc: Shape { public var tangent1End: CGPoint public var tangent2End: CGPoint public var radius: CGFloat - + public init(start: CGPoint, tangent1End: CGPoint, tangent2End: CGPoint, radius: CGFloat) { self.start = start self.tangent1End = tangent1End self.tangent2End = tangent2End - self.radius = radius + self.radius = radius } public var animatableData: AnimatablePair, AnimatablePair> { - get {AnimatablePair(AnimatablePair(self.start, self.radius), AnimatablePair(self.tangent1End, self.tangent2End))} + get { + AnimatablePair( + AnimatablePair(self.start, + self.radius), + AnimatablePair(self.tangent1End, + self.tangent2End) + ) + } set { self.start = newValue.first.first + self.radius = newValue.first.second self.tangent1End = newValue.second.first self.tangent2End = newValue.second.second - self.radius = newValue.first.second } } diff --git a/Sources/Shapes/TickMarks.swift b/Sources/Shapes/TickMarks.swift index 7ab1b07..17b4164 100644 --- a/Sources/Shapes/TickMarks.swift +++ b/Sources/Shapes/TickMarks.swift @@ -4,25 +4,84 @@ import SwiftUI -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) public struct TickMarks: Shape { public var spacing: CGFloat public var ticks: Int + + /// Creates `ticks` number of tickmarks of varying sizes each spaced apart with the given `spacing` + /// + /// ## Example Usage + /// ``` + /// struct Ruler: View { + /// var length: CGFloat = 600 + /// @State private var offset: CGSize = .zero + /// @State private var dragState: CGSize = .zero + /// @State private var angle: Angle = Angle(degrees: -45) + /// @State private var rotationState: Angle = .zero + /// + /// var body: some View { + /// VStack(alignment: .leading, spacing: 0) { + /// TickMarks(spacing: 10, ticks: Int(length/10)) + /// .stroke(Color.gray) + /// .frame(height: 50) + /// values + /// .offset(x: -10) + /// } + /// .frame(width: length) + /// .padding(.leading) + /// .background(Color.pink.opacity(0.5)) + /// .rotationEffect(angle + rotationState) + /// .gesture(dragGesture) + /// .offset(x: offset.width + dragState.width, + /// y: offset.height + dragState.height) + /// .simultaneousGesture(rotationGesture) + /// } + /// + /// // Create the text for every 100 points + /// var values: some View { + /// let size = 0.. CGFloat { - if i%100 == 0 { - return 1 - } else if i%10 == 0 { - return 0.75 - }else if i%5 == 0 { - return 0.5 - } else { - return 0.25 - } + if i%100 == 0 { return 1 } + if i%10 == 0 { return 0.75 } + if i%5 == 0 { return 0.5 } + return 0.25 } + public func path(in rect: CGRect) -> Path { Path { path in for i in 0...ticks { @@ -34,3 +93,6 @@ public struct TickMarks: Shape { } + + + diff --git a/Sources/Shapes/Triangles.swift b/Sources/Shapes/Triangles.swift index 6972d1e..f83fd5c 100644 --- a/Sources/Shapes/Triangles.swift +++ b/Sources/Shapes/Triangles.swift @@ -1,24 +1,32 @@ // Created by Kieran Brown on 3/21/20. // Copyright © 2020 BrownandSons. All rights reserved. + import SwiftUI -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) public struct Triangle: Shape { + /// Creates an upside down isosceles triangle that is the height and width of its containing rectangle. The left and right sides are congruent. public init() {} + + var insetAmount: CGFloat = 0 + public func path(in rect: CGRect) -> Path { - Path { (path) in - let w = rect.width - let h = rect.height - - path.move(to: CGPoint(x: w/2, y: 0)) - path.addLine(to: CGPoint(x: 0, y: h)) - path.addLine(to: CGPoint(x: w, y: h)) + Path { path in + path.move(to: CGPoint(x: insetAmount, y: insetAmount)) + path.addLine(to: .init(x: rect.size.width/2, y: rect.size.height-insetAmount)) + path.addLine(to: .init(x: rect.width-insetAmount, y: insetAmount)) path.closeSubpath() - } } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) + +extension Triangle: InsettableShape { + public func inset(by amount: CGFloat) -> some InsettableShape { + var shape = self + shape.insetAmount += amount + return shape + } +} + public struct OpenTriangle: Shape { public init() {} public func path(in rect: CGRect) -> Path { @@ -28,18 +36,17 @@ public struct OpenTriangle: Shape { path.move(to: CGPoint(x: 0, y: 0)) path.addLine(to: CGPoint(x: w, y: h/2)) path.addLine(to: CGPoint(x: 0, y: h)) - } } } -@available(iOS 13.0, macOS 10.15, watchOS 6.0 , tvOS 13.0, *) + public struct RightTriangle: Shape { public init() {} public func path(in rect: CGRect) -> Path { Path { (path) in let w = rect.width let h = rect.height - + path.move(to: CGPoint(x: 0, y: 0)) path.addLine(to: CGPoint(x: 0, y: h)) path.addLine(to: CGPoint(x: w, y: h)) diff --git a/Sources/Shapes/VerticalLine.swift b/Sources/Shapes/VerticalLine.swift new file mode 100644 index 0000000..3dca7a8 --- /dev/null +++ b/Sources/Shapes/VerticalLine.swift @@ -0,0 +1,25 @@ +// +// VerticalLine.swift +// +// +// Created by Kieran Brown on 10/15/20. +// + +import SwiftUI + +public struct VerticalLine: Shape { + var offset: CGFloat + + /// A Vertical line that is the height of its container + /// - parameter offset: A value between 0 and 1 defining the line's horizontal offset in its container (**Default**: 0.5) + public init(offset: CGFloat = 0.5) { + self.offset = offset + } + + public func path(in rect: CGRect) -> Path { + Path { path in + path.move(to: CGPoint(x: rect.maxX*offset, y: 0)) + path.addLine(to: CGPoint(x: rect.maxX*offset, y: rect.height)) + } + } +}