Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 841bde1377 | |||
| 37779e1b54 | |||
| ba5bc4f861 | |||
| 80d546de03 | |||
| 88db9aeafe | |||
| 37c51d9b46 | |||
| 75df39fc1f | |||
| 6b5affa46e |
@@ -153,7 +153,7 @@ BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", form: ChartForm.s
|
||||
## Pie charts
|
||||

|
||||
|
||||
You can add a line chart with the following code:
|
||||
You can add a pie chart with the following code:
|
||||
|
||||
```swift
|
||||
PieChartView(data: [8,23,54,32], title: "Title", legend: "Legendary") // legend is optional
|
||||
|
||||
@@ -9,15 +9,17 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct BarChartView : View {
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
private var data: ChartData
|
||||
public var title: String
|
||||
public var legend: String?
|
||||
public var style: ChartStyle
|
||||
public var darkModeStyle: ChartStyle
|
||||
public var formSize:CGSize
|
||||
public var dropShadow: Bool
|
||||
public var cornerImage: Image
|
||||
public var valueSpecifier:String
|
||||
|
||||
|
||||
@State private var touchLocation: CGFloat = -1.0
|
||||
@State private var showValue: Bool = false
|
||||
@State private var showLabelValue: Bool = false
|
||||
@@ -36,6 +38,7 @@ public struct BarChartView : View {
|
||||
self.title = title
|
||||
self.legend = legend
|
||||
self.style = style
|
||||
self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.barChartStyleOrangeDark
|
||||
self.formSize = form!
|
||||
self.dropShadow = dropShadow!
|
||||
self.cornerImage = cornerImage!
|
||||
@@ -45,7 +48,7 @@ public struct BarChartView : View {
|
||||
public var body: some View {
|
||||
ZStack{
|
||||
Rectangle()
|
||||
.fill(self.style.backgroundColor)
|
||||
.fill(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor)
|
||||
.cornerRadius(20)
|
||||
.shadow(color: Color.gray, radius: self.dropShadow ? 8 : 0)
|
||||
VStack(alignment: .leading){
|
||||
@@ -53,41 +56,48 @@ public struct BarChartView : View {
|
||||
if(!showValue){
|
||||
Text(self.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(self.style.textColor)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
|
||||
}else{
|
||||
Text("\(self.currentValue, specifier: self.valueSpecifier)")
|
||||
.font(.headline)
|
||||
.foregroundColor(self.style.textColor)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
|
||||
}
|
||||
if(self.formSize == ChartForm.large && self.legend != nil && !showValue) {
|
||||
Text(self.legend!)
|
||||
.font(.callout)
|
||||
.foregroundColor(self.style.accentColor)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.accentColor : self.style.accentColor)
|
||||
.transition(.opacity)
|
||||
.animation(.easeOut)
|
||||
}
|
||||
Spacer()
|
||||
self.cornerImage
|
||||
.imageScale(.large)
|
||||
.foregroundColor(self.style.legendTextColor)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
|
||||
}.padding()
|
||||
BarChartRow(data: data.points.map{$0.1}, accentColor: self.style.accentColor, secondGradientAccentColor: self.style.secondGradientColor, touchLocation: self.$touchLocation)
|
||||
BarChartRow(data: data.points.map{$0.1},
|
||||
accentColor: self.colorScheme == .dark ? self.darkModeStyle.accentColor : self.style.accentColor,
|
||||
secondGradientAccentColor: self.colorScheme == .dark ? self.darkModeStyle.secondGradientColor : self.style.secondGradientColor,
|
||||
touchLocation: self.$touchLocation)
|
||||
if self.legend != nil && self.formSize == ChartForm.medium && !self.showLabelValue{
|
||||
Text(self.legend!)
|
||||
.font(.headline)
|
||||
.foregroundColor(self.style.legendTextColor)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
|
||||
.padding()
|
||||
}else if (self.data.valuesGiven) {
|
||||
LabelView(arrowOffset: self.getArrowOffset(touchLocation: self.touchLocation), title: .constant(self.getCurrentValue().0)).offset(x: self.getLabelViewOffset(touchLocation: self.touchLocation), y: -6)
|
||||
}else if (self.data.valuesGiven && self.getCurrentValue() != nil) {
|
||||
LabelView(arrowOffset: self.getArrowOffset(touchLocation: self.touchLocation),
|
||||
title: .constant(self.getCurrentValue()!.0)).offset(x: self.getLabelViewOffset(touchLocation: self.touchLocation), y: -6)
|
||||
}
|
||||
|
||||
}
|
||||
}.frame(minWidth:self.formSize.width, maxWidth: self.isFullWidth ? .infinity : self.formSize.width, minHeight:self.formSize.height, maxHeight:self.formSize.height)
|
||||
}.frame(minWidth:self.formSize.width,
|
||||
maxWidth: self.isFullWidth ? .infinity : self.formSize.width,
|
||||
minHeight:self.formSize.height,
|
||||
maxHeight:self.formSize.height)
|
||||
.gesture(DragGesture()
|
||||
.onChanged({ value in
|
||||
self.touchLocation = value.location.x/self.formSize.width
|
||||
self.showValue = true
|
||||
self.currentValue = self.getCurrentValue().1
|
||||
self.currentValue = self.getCurrentValue()?.1 ?? 0
|
||||
if(self.data.valuesGiven && self.formSize == ChartForm.medium) {
|
||||
self.showLabelValue = true
|
||||
}
|
||||
@@ -117,7 +127,8 @@ public struct BarChartView : View {
|
||||
return min(self.formSize.width-110,max(10,(self.touchLocation * self.formSize.width) - 50))
|
||||
}
|
||||
|
||||
func getCurrentValue()-> (String,Double) {
|
||||
func getCurrentValue() -> (String,Double)? {
|
||||
guard self.data.points.count > 0 else { return nil}
|
||||
let index = max(0,min(self.data.points.count-1,Int(floor((self.touchLocation*self.formSize.width)/(self.formSize.width/CGFloat(self.data.points.count))))))
|
||||
return self.data.points[index]
|
||||
}
|
||||
@@ -126,7 +137,10 @@ public struct BarChartView : View {
|
||||
#if DEBUG
|
||||
struct ChartView_Previews : PreviewProvider {
|
||||
static var previews: some View {
|
||||
BarChartView(data: TestData.values ,title: "Model 3 sales", legend: "Quarterly", valueSpecifier: "%.0f")
|
||||
BarChartView(data: TestData.values ,
|
||||
title: "Model 3 sales",
|
||||
legend: "Quarterly",
|
||||
valueSpecifier: "%.0f")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -86,6 +86,13 @@ public struct Styles {
|
||||
secondGradientColor: Colors.OrangeEnd,
|
||||
textColor: Color.black,
|
||||
legendTextColor: Color.gray)
|
||||
|
||||
public static let lineViewDarkMode = ChartStyle(
|
||||
backgroundColor: Color.black,
|
||||
accentColor: Colors.OrangeStart,
|
||||
secondGradientColor: Colors.OrangeEnd,
|
||||
textColor: Color.white,
|
||||
legendTextColor: Color.white)
|
||||
}
|
||||
|
||||
public struct ChartForm {
|
||||
@@ -104,12 +111,13 @@ public struct ChartForm {
|
||||
|
||||
}
|
||||
|
||||
public struct ChartStyle {
|
||||
public class ChartStyle {
|
||||
public var backgroundColor: Color
|
||||
public var accentColor: Color
|
||||
public var secondGradientColor: Color
|
||||
public var textColor: Color
|
||||
public var legendTextColor: Color
|
||||
public weak var darkModeStyle: ChartStyle?
|
||||
|
||||
public init(backgroundColor: Color, accentColor: Color, secondGradientColor: Color, textColor: Color, legendTextColor: Color){
|
||||
self.backgroundColor = backgroundColor
|
||||
@@ -129,8 +137,9 @@ public struct ChartStyle {
|
||||
}
|
||||
|
||||
public class ChartData: ObservableObject {
|
||||
@Published var points: [(String,Double)] = [(String,Double)]()
|
||||
@Published var points: [(String,Double)]
|
||||
var valuesGiven: Bool = false
|
||||
|
||||
public init<N: BinaryFloatingPoint>(points:[N]) {
|
||||
self.points = points.map{("", Double($0))}
|
||||
}
|
||||
@@ -159,11 +168,11 @@ public class ChartData: ObservableObject {
|
||||
public class TestData{
|
||||
static public var data:ChartData = ChartData(points: [37,72,51,22,39,47,66,85,50])
|
||||
static public var values:ChartData = ChartData(values: [("2017 Q3",220),
|
||||
("2017 Q4",1550),
|
||||
("2018 Q1",8180),
|
||||
("2018 Q2",18440),
|
||||
("2018 Q3",55840),
|
||||
("2018 Q4",63150), ("2019 Q1",50900), ("2019 Q2",77550), ("2019 Q3",79600), ("2019 Q4",92550)])
|
||||
("2017 Q4",1550),
|
||||
("2018 Q1",8180),
|
||||
("2018 Q2",18440),
|
||||
("2018 Q3",55840),
|
||||
("2018 Q4",63150), ("2019 Q1",50900), ("2019 Q2",77550), ("2019 Q3",79600), ("2019 Q4",92550)])
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,13 @@ struct Line: View {
|
||||
.rotationEffect(.degrees(180), anchor: .center)
|
||||
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
|
||||
.animation(.easeOut(duration: 1.2))
|
||||
.onAppear(){
|
||||
self.showFull.toggle()
|
||||
}.drawingGroup()
|
||||
.onAppear {
|
||||
self.showFull = true
|
||||
}
|
||||
.onDisappear {
|
||||
self.showFull = false
|
||||
}
|
||||
.drawingGroup()
|
||||
if(self.showIndicator) {
|
||||
IndicatorPoint()
|
||||
.position(self.getClosestPointOnPath(touchLocation: self.touchLocation))
|
||||
@@ -71,8 +75,7 @@ struct Line: View {
|
||||
}
|
||||
|
||||
func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint {
|
||||
let percentage:CGFloat = min(max(touchLocation.x,0)/self.frame.width,1)
|
||||
let closest = self.path.percentPoint(percentage)
|
||||
let closest = self.path.point(to: touchLocation.x)
|
||||
return closest
|
||||
}
|
||||
|
||||
@@ -113,7 +116,7 @@ extension Path {
|
||||
return path
|
||||
}
|
||||
guard var offset = points.min() else { return path }
|
||||
offset -= 3
|
||||
// offset -= 3
|
||||
var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
|
||||
path.move(to: p1)
|
||||
for pointIndex in 1..<points.count {
|
||||
@@ -132,7 +135,7 @@ extension Path {
|
||||
return path
|
||||
}
|
||||
guard var offset = points.min() else { return path }
|
||||
offset -= 3
|
||||
// offset -= 3
|
||||
path.move(to: .zero)
|
||||
var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
|
||||
path.addLine(to: p1)
|
||||
@@ -147,22 +150,6 @@ extension Path {
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
|
||||
func percentPoint(_ percent: CGFloat) -> CGPoint {
|
||||
// percent difference between points
|
||||
let diff: CGFloat = 0.001
|
||||
let comp: CGFloat = 1 - diff
|
||||
|
||||
// handle limits
|
||||
let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
|
||||
|
||||
let f = pct > comp ? comp : pct
|
||||
let t = pct > comp ? 1 : pct + diff
|
||||
let tp = self.trimmedPath(from: f, to: t)
|
||||
|
||||
return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct Line_Previews: PreviewProvider {
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct LineChartView: View {
|
||||
// let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
@ObservedObject var data:ChartData
|
||||
public var title: String
|
||||
public var legend: String?
|
||||
public var style: ChartStyle
|
||||
public var darkModeStyle: ChartStyle
|
||||
|
||||
public var formSize:CGSize
|
||||
public var dropShadow: Bool
|
||||
public var valueSpecifier:String
|
||||
@@ -23,7 +25,6 @@ public struct LineChartView: View {
|
||||
@State private var currentValue: Double = 2 {
|
||||
didSet{
|
||||
if (oldValue != self.currentValue && showIndicatorDot) {
|
||||
// selectionFeedbackGenerator.selectionChanged()
|
||||
HapticFeedback.playSelection()
|
||||
}
|
||||
|
||||
@@ -32,11 +33,20 @@ public struct LineChartView: View {
|
||||
let frame = CGSize(width: 180, height: 120)
|
||||
private var rateValue: Int
|
||||
|
||||
public init(data: [Double], title: String, legend: String? = nil, style: ChartStyle = Styles.lineChartStyleOne, form: CGSize? = ChartForm.medium ,rateValue: Int? = 14, dropShadow: Bool? = true, valueSpecifier: String? = "%.1f"){
|
||||
public init(data: [Double],
|
||||
title: String,
|
||||
legend: String? = nil,
|
||||
style: ChartStyle = Styles.lineChartStyleOne,
|
||||
form: CGSize? = ChartForm.medium,
|
||||
rateValue: Int? = 14,
|
||||
dropShadow: Bool? = true,
|
||||
valueSpecifier: String? = "%.1f") {
|
||||
|
||||
self.data = ChartData(points: data)
|
||||
self.title = title
|
||||
self.legend = legend
|
||||
self.style = style
|
||||
self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode
|
||||
self.formSize = form!
|
||||
self.rateValue = rateValue!
|
||||
self.dropShadow = dropShadow!
|
||||
@@ -45,13 +55,21 @@ public struct LineChartView: View {
|
||||
|
||||
public var body: some View {
|
||||
ZStack(alignment: .center){
|
||||
RoundedRectangle(cornerRadius: 20).fill(self.style.backgroundColor).frame(width: frame.width, height: 240, alignment: .center).shadow(radius: self.dropShadow ? 8 : 0)
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor)
|
||||
.frame(width: frame.width, height: 240, alignment: .center)
|
||||
.shadow(radius: self.dropShadow ? 8 : 0)
|
||||
VStack(alignment: .leading){
|
||||
if(!self.showIndicatorDot){
|
||||
VStack(alignment: .leading, spacing: 8){
|
||||
Text(self.title).font(.title).bold().foregroundColor(self.style.textColor)
|
||||
Text(self.title)
|
||||
.font(.title)
|
||||
.bold()
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
|
||||
if (self.legend != nil){
|
||||
Text(self.legend!).font(.callout).foregroundColor(self.style.legendTextColor)
|
||||
Text(self.legend!)
|
||||
.font(.callout)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor :self.style.legendTextColor)
|
||||
}
|
||||
HStack {
|
||||
if (self.rateValue >= 0){
|
||||
@@ -74,12 +92,12 @@ public struct LineChartView: View {
|
||||
Spacer()
|
||||
}
|
||||
.transition(.scale)
|
||||
// .animation(.spring())
|
||||
|
||||
}
|
||||
Spacer()
|
||||
GeometryReader{ geometry in
|
||||
Line(data: self.data, frame: .constant(geometry.frame(in: .local)), touchLocation: self.$touchLocation, showIndicator: self.$showIndicatorDot)
|
||||
Line(data: self.data,
|
||||
frame: .constant(geometry.frame(in: .local)),
|
||||
touchLocation: self.$touchLocation, showIndicator: self.$showIndicatorDot)
|
||||
}
|
||||
.frame(width: frame.width, height: frame.height + 30)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
|
||||
@@ -13,6 +13,7 @@ public struct LineView: View {
|
||||
public var title: String?
|
||||
public var legend: String?
|
||||
public var style: ChartStyle
|
||||
public var darkModeStyle: ChartStyle
|
||||
public var valueSpecifier:String
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
@@ -24,12 +25,18 @@ public struct LineView: View {
|
||||
@State private var currentDataNumber: Double = 0
|
||||
@State private var hideHorizontalLines: Bool = false
|
||||
|
||||
public init(data: [Double], title: String? = nil, legend: String? = nil, style: ChartStyle? = Styles.lineChartStyleOne, valueSpecifier: String? = "%.1f"){
|
||||
public init(data: [Double],
|
||||
title: String? = nil,
|
||||
legend: String? = nil,
|
||||
style: ChartStyle = Styles.lineChartStyleOne,
|
||||
valueSpecifier: String? = "%.1f") {
|
||||
|
||||
self.data = ChartData(points: data)
|
||||
self.title = title
|
||||
self.legend = legend
|
||||
self.style = style!
|
||||
self.style = style
|
||||
self.valueSpecifier = valueSpecifier!
|
||||
self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
@@ -37,25 +44,36 @@ public struct LineView: View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Group{
|
||||
if (self.title != nil){
|
||||
Text(self.title!).font(.title).bold().foregroundColor(self.colorScheme == .dark ? Color.white : self.style.textColor)
|
||||
Text(self.title!)
|
||||
.font(.title)
|
||||
.bold().foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
|
||||
}
|
||||
if (self.legend != nil){
|
||||
Text(self.legend!).font(.callout).foregroundColor(self.colorScheme == .dark ? Color.white : self.style.legendTextColor)
|
||||
Text(self.legend!)
|
||||
.font(.callout)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
|
||||
}
|
||||
}.offset(x: 0, y: 20)
|
||||
ZStack{
|
||||
GeometryReader{ reader in
|
||||
Rectangle().foregroundColor(self.colorScheme == .dark ? Color.black : self.style.backgroundColor)
|
||||
Rectangle()
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor)
|
||||
if(self.showLegend){
|
||||
Legend(data: self.data, frame: .constant(reader.frame(in: .local)), hideHorizontalLines: self.$hideHorizontalLines)
|
||||
Legend(data: self.data,
|
||||
frame: .constant(reader.frame(in: .local)), hideHorizontalLines: self.$hideHorizontalLines)
|
||||
.transition(.opacity)
|
||||
.animation(Animation.easeOut(duration: 1).delay(1))
|
||||
}
|
||||
Line(data: self.data, frame: .constant(CGRect(x: 0, y: 0, width: reader.frame(in: .local).width - 30, height: reader.frame(in: .local).height)), touchLocation: self.$indicatorLocation,showIndicator: self.$hideHorizontalLines ,showBackground: false).offset(x: 30, y: 0)
|
||||
Line(data: self.data,
|
||||
frame: .constant(CGRect(x: 0, y: 0, width: reader.frame(in: .local).width - 30, height: reader.frame(in: .local).height)),
|
||||
touchLocation: self.$indicatorLocation,showIndicator: self.$hideHorizontalLines ,showBackground: false)
|
||||
.offset(x: 30, y: 0)
|
||||
.onAppear(){
|
||||
self.showLegend.toggle()
|
||||
}
|
||||
}.frame(width: geometry.frame(in: .local).size.width, height: 240).offset(x: 0, y: 40 )
|
||||
}
|
||||
.frame(width: geometry.frame(in: .local).size.width, height: 240)
|
||||
.offset(x: 0, y: 40 )
|
||||
MagnifierRect(currentNumber: self.$currentDataNumber, valueSpecifier: self.valueSpecifier)
|
||||
.opacity(self.opacity)
|
||||
.offset(x: self.dragLocation.x - geometry.frame(in: .local).size.width/2, y: 36)
|
||||
@@ -115,7 +133,6 @@ struct MagnifierRect: View {
|
||||
Text("\(self.currentNumber, specifier: valueSpecifier)")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.offset(x: 0, y:-110)
|
||||
// .animation(.spring())
|
||||
.foregroundColor(self.colorScheme == .dark ? Color.white : Color.black)
|
||||
if (self.colorScheme == .dark ){
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
@@ -131,6 +148,6 @@ struct MagnifierRect: View {
|
||||
}
|
||||
|
||||
|
||||
}//.animation(.linear)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by xspyhack on 2020/1/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension Path {
|
||||
func trimmedPath(for percent: CGFloat) -> Path {
|
||||
// percent difference between points
|
||||
let boundsDistance: CGFloat = 0.001
|
||||
let completion: CGFloat = 1 - boundsDistance
|
||||
|
||||
let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
|
||||
|
||||
let start = pct > completion ? completion : pct - boundsDistance
|
||||
let end = pct > completion ? 1 : pct + boundsDistance
|
||||
return trimmedPath(from: start, to: end)
|
||||
}
|
||||
|
||||
func point(for percent: CGFloat) -> CGPoint {
|
||||
let path = trimmedPath(for: percent)
|
||||
return CGPoint(x: path.boundingRect.midX, y: path.boundingRect.midY)
|
||||
}
|
||||
|
||||
func point(to maxX: CGFloat) -> CGPoint {
|
||||
let total = length
|
||||
let sub = length(to: maxX)
|
||||
let percent = sub / total
|
||||
return point(for: percent)
|
||||
}
|
||||
|
||||
var length: CGFloat {
|
||||
var ret: CGFloat = 0.0
|
||||
var start: CGPoint?
|
||||
var point = CGPoint.zero
|
||||
|
||||
forEach { ele in
|
||||
switch ele {
|
||||
case .move(let to):
|
||||
if start == nil {
|
||||
start = to
|
||||
}
|
||||
point = to
|
||||
case .line(let to):
|
||||
ret += point.line(to: to)
|
||||
point = to
|
||||
case .quadCurve(let to, let control):
|
||||
ret += point.quadCurve(to: to, control: control)
|
||||
point = to
|
||||
case .curve(let to, let control1, let control2):
|
||||
ret += point.curve(to: to, control1: control1, control2: control2)
|
||||
point = to
|
||||
case .closeSubpath:
|
||||
if let to = start {
|
||||
ret += point.line(to: to)
|
||||
point = to
|
||||
}
|
||||
start = nil
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func length(to maxX: CGFloat) -> CGFloat {
|
||||
var ret: CGFloat = 0.0
|
||||
var start: CGPoint?
|
||||
var point = CGPoint.zero
|
||||
var finished = false
|
||||
|
||||
forEach { ele in
|
||||
if finished {
|
||||
return
|
||||
}
|
||||
switch ele {
|
||||
case .move(let to):
|
||||
if to.x > maxX {
|
||||
finished = true
|
||||
return
|
||||
}
|
||||
if start == nil {
|
||||
start = to
|
||||
}
|
||||
point = to
|
||||
case .line(let to):
|
||||
if to.x > maxX {
|
||||
finished = true
|
||||
ret += point.line(to: to, x: maxX)
|
||||
return
|
||||
}
|
||||
ret += point.line(to: to)
|
||||
point = to
|
||||
case .quadCurve(let to, let control):
|
||||
if to.x > maxX {
|
||||
finished = true
|
||||
ret += point.quadCurve(to: to, control: control, x: maxX)
|
||||
return
|
||||
}
|
||||
ret += point.quadCurve(to: to, control: control)
|
||||
point = to
|
||||
case .curve(let to, let control1, let control2):
|
||||
if to.x > maxX {
|
||||
finished = true
|
||||
ret += point.curve(to: to, control1: control1, control2: control2, x: maxX)
|
||||
return
|
||||
}
|
||||
ret += point.curve(to: to, control1: control1, control2: control2)
|
||||
point = to
|
||||
case .closeSubpath:
|
||||
fatalError("Can't include closeSubpath")
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
extension CGPoint {
|
||||
func point(to: CGPoint, x: CGFloat) -> CGPoint {
|
||||
let a = (to.y - self.y) / (to.x - self.x)
|
||||
let y = self.y + (x - self.x) * a
|
||||
return CGPoint(x: x, y: y)
|
||||
}
|
||||
|
||||
func line(to: CGPoint) -> CGFloat {
|
||||
dist(to: to)
|
||||
}
|
||||
|
||||
func line(to: CGPoint, x: CGFloat) -> CGFloat {
|
||||
dist(to: point(to: to, x: x))
|
||||
}
|
||||
|
||||
func quadCurve(to: CGPoint, control: CGPoint) -> CGFloat {
|
||||
var dist: CGFloat = 0
|
||||
let steps: CGFloat = 100
|
||||
|
||||
for i in 0..<Int(steps) {
|
||||
let t0 = CGFloat(i) / steps
|
||||
let t1 = CGFloat(i+1) / steps
|
||||
let a = point(to: to, t: t0, control: control)
|
||||
let b = point(to: to, t: t1, control: control)
|
||||
|
||||
dist += a.line(to: b)
|
||||
}
|
||||
return dist
|
||||
}
|
||||
|
||||
func quadCurve(to: CGPoint, control: CGPoint, x: CGFloat) -> CGFloat {
|
||||
var dist: CGFloat = 0
|
||||
let steps: CGFloat = 100
|
||||
|
||||
for i in 0..<Int(steps) {
|
||||
let t0 = CGFloat(i) / steps
|
||||
let t1 = CGFloat(i+1) / steps
|
||||
let a = point(to: to, t: t0, control: control)
|
||||
let b = point(to: to, t: t1, control: control)
|
||||
|
||||
if a.x >= x {
|
||||
return dist
|
||||
} else if b.x > x {
|
||||
dist += a.line(to: b, x: x)
|
||||
return dist
|
||||
} else if b.x == x {
|
||||
dist += a.line(to: b)
|
||||
return dist
|
||||
}
|
||||
|
||||
dist += a.line(to: b)
|
||||
}
|
||||
return dist
|
||||
}
|
||||
|
||||
func point(to: CGPoint, t: CGFloat, control: CGPoint) -> CGPoint {
|
||||
let x = CGPoint.value(x: self.x, y: to.x, t: t, c: control.x)
|
||||
let y = CGPoint.value(x: self.y, y: to.y, t: t, c: control.y)
|
||||
|
||||
return CGPoint(x: x, y: y)
|
||||
}
|
||||
|
||||
func curve(to: CGPoint, control1: CGPoint, control2: CGPoint) -> CGFloat {
|
||||
var dist: CGFloat = 0
|
||||
let steps: CGFloat = 100
|
||||
|
||||
for i in 0..<Int(steps) {
|
||||
let t0 = CGFloat(i) / steps
|
||||
let t1 = CGFloat(i+1) / steps
|
||||
|
||||
let a = point(to: to, t: t0, control1: control1, control2: control2)
|
||||
let b = point(to: to, t: t1, control1: control1, control2: control2)
|
||||
|
||||
dist += a.line(to: b)
|
||||
}
|
||||
|
||||
return dist
|
||||
}
|
||||
|
||||
func curve(to: CGPoint, control1: CGPoint, control2: CGPoint, x: CGFloat) -> CGFloat {
|
||||
var dist: CGFloat = 0
|
||||
let steps: CGFloat = 100
|
||||
|
||||
for i in 0..<Int(steps) {
|
||||
let t0 = CGFloat(i) / steps
|
||||
let t1 = CGFloat(i+1) / steps
|
||||
|
||||
let a = point(to: to, t: t0, control1: control1, control2: control2)
|
||||
let b = point(to: to, t: t1, control1: control1, control2: control2)
|
||||
|
||||
if a.x >= x {
|
||||
return dist
|
||||
} else if b.x > x {
|
||||
dist += a.line(to: b, x: x)
|
||||
return dist
|
||||
} else if b.x == x {
|
||||
dist += a.line(to: b)
|
||||
return dist
|
||||
}
|
||||
|
||||
dist += a.line(to: b)
|
||||
}
|
||||
|
||||
return dist
|
||||
}
|
||||
|
||||
func point(to: CGPoint, t: CGFloat, control1: CGPoint, control2: CGPoint) -> CGPoint {
|
||||
let x = CGPoint.value(x: self.x, y: to.x, t: t, c1: control1.x, c2: control2.x)
|
||||
let y = CGPoint.value(x: self.y, y: to.y, t: t, c1: control1.y, c2: control2.x)
|
||||
|
||||
return CGPoint(x: x, y: y)
|
||||
}
|
||||
|
||||
static func value(x: CGFloat, y: CGFloat, t: CGFloat, c: CGFloat) -> CGFloat {
|
||||
var value: CGFloat = 0.0
|
||||
// (1-t)^2 * p0 + 2 * (1-t) * t * c1 + t^2 * p1
|
||||
value += pow(1-t, 2) * x
|
||||
value += 2 * (1-t) * t * c
|
||||
value += pow(t, 2) * y
|
||||
return value
|
||||
}
|
||||
|
||||
static func value(x: CGFloat, y: CGFloat, t: CGFloat, c1: CGFloat, c2: CGFloat) -> CGFloat {
|
||||
var value: CGFloat = 0.0
|
||||
// (1-t)^3 * p0 + 3 * (1-t)^2 * t * c1 + 3 * (1-t) * t^2 * c2 + t^3 * p1
|
||||
value += pow(1-t, 3) * x
|
||||
value += 3 * pow(1-t, 2) * t * c1
|
||||
value += 3 * (1-t) * pow(t, 2) * c2
|
||||
value += pow(t, 3) * y
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user