From 2d691e4af64326480379495e2cb0c6a175bb34a2 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Sun, 16 May 2021 19:26:01 +0800 Subject: [PATCH 01/41] Add SphericText --- Package.swift | 3 +- Sources/Rings/SphericText.md | 2 + Sources/Rings/SphericText.swift | 236 ++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 Sources/Rings/SphericText.md create mode 100644 Sources/Rings/SphericText.swift diff --git a/Package.swift b/Package.swift index 79d3d26..2865cb9 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,8 @@ let package = Package( exclude: ["RingText.md", "ClockIndex.md", "ArchimedeanSpiralText.md", - "HandAiguille.md"]), + "HandAiguille.md", + "SphericText.md"]), .testTarget( name: "RingsTests", dependencies: ["Rings", diff --git a/Sources/Rings/SphericText.md b/Sources/Rings/SphericText.md new file mode 100644 index 0000000..64e53d4 --- /dev/null +++ b/Sources/Rings/SphericText.md @@ -0,0 +1,2 @@ +# <#Title#> + diff --git a/Sources/Rings/SphericText.swift b/Sources/Rings/SphericText.swift new file mode 100644 index 0000000..00fdf42 --- /dev/null +++ b/Sources/Rings/SphericText.swift @@ -0,0 +1,236 @@ +// +// SphericText.swift +// Rings +// +// Created by Chen Hai Teng on 3/7/21. +// + +import SwiftUI +import CoreGraphicsExtension + +struct SphericText: View { + + @Binding var offsetDegree: T + + private let stringTable: [(offset: Int, element:String)] + private var wordSpacing: Double = 30.0 + private var font: Font? + private var wordColor = Color.white + private var wordBackground = Color.clear + private var hideOpposite = false + private var blurMinors = false + private var oppositeRange = 150...210 + private var frontMostRange = -10...10 + private var perspective: CGFloat = 0.0 + private var anchorZ: CGFloat = 100.0 + @State private var estHight: CGFloat = 0.0 + + init(words: [String], word_spacing: T = 30.0 , font: Font? = nil, word_color: Color = Color.white, word_background: Color = Color.clear, hide_opposite: Bool = false, degree_offset: Binding = .constant(0)) { + stringTable = words.enumerated().map { (i, e) in + return (i, e) + } + _offsetDegree = degree_offset + wordSpacing = Double(word_spacing) + self.font = font ?? .system(size: 20.0) + wordColor = word_color + wordBackground = word_background + hideOpposite = hide_opposite + } + + init(_ text: String, _ rotateDegree: Binding) { + let words = Array(text).map { c -> String in + String(c) + } + let spacing = 360.0/Double(text.count) + self.init(words: words, word_spacing: T(spacing), degree_offset: rotateDegree) + } + + init(_ text: String, word_spacing: T, _ rotateDegree: Binding) { + let words = Array(text).map { c -> String in + String(c) + } + self.init(words: words, word_spacing: word_spacing, degree_offset: rotateDegree) + } + + init(list: [String], _ rotateDegree: Binding) { + let spacing = 360.0/Double(list.count) + self.init(words: list, word_spacing: T(spacing), degree_offset: rotateDegree) + } + + var body: some View { + GeometryReader { geo in + let w = geo.size.width + let frameL = geo.frame(in: .local) + ZStack(alignment: .center) { + //hide text to caculate height + Sizing { + Text("A").font(font).opacity(0.0) + } + ForEach(stringTable, id: \.self.offset) { (i, e) in + let deg = Double(i)*wordSpacing + Double(offsetDegree) + let _ = print("degrees: \(deg)") + let normalizedD = Int(deg)%360 + let shouldBlur = blurMinors ? (frontMostRange ~= normalizedD) : false + let shouldHide = hideOpposite ? (oppositeRange ~= abs(normalizedD)) : false + Text(e).frame(width: w/2, height: estHight, alignment: .center) + .font(font) + .foregroundColor(wordColor) + .background(wordBackground) + .rotation3DEffect( + .degrees(deg), + axis: (x: 0.0, y: 1.0, z: 0.0), + anchor: .center, + anchorZ: anchorZ, + perspective: perspective + ).opacity(shouldHide ? 0.0 : 1.0).blur(radius: shouldBlur ? 0.0 : 1.0) + } + + }.frame(width: frameL.size.width, height: estHight).onPreferenceChange(ViewSizeKey.self) { sizes in + estHight = (sizes.reduce(0, {$0 + $1.height}))/CGFloat(sizes.count)*CGFloat(1.1) + } + } + } +} + +extension SphericText : Adjustable { + public func wordSpacing(_ spacing: Double) -> SphericText { + setProperty { tmp in + tmp.wordSpacing = spacing + } + } + + public func font(_ font: Font) -> SphericText { + setProperty { tmp in + tmp.font = font + } + } + + public func wordColor(_ color: Color) -> SphericText { + setProperty { tmp in + tmp.wordColor = color + } + } + + public func wordBackground(_ color: Color) -> SphericText { + setProperty { tmp in + tmp.wordBackground = color + } + } + + public func hideOpposite(_ isHidden: Bool) -> SphericText { + setProperty { tmp in + tmp.hideOpposite = isHidden + } + } + + public func rangeOfOpposite(in range: ClosedRange) -> SphericText { + setProperty { tmp in + tmp.oppositeRange = range + } + } + + public func rangeOfFrontMost(in range: ClosedRange) -> SphericText { + setProperty { tmp in + tmp.frontMostRange = range + } + } + + public func perspective(_ value: CGFloat) -> SphericText { + setProperty { tmp in + tmp.perspective = value + } + } + + public func radius(_ value: CGFloat) -> SphericText { + setProperty { tmp in + tmp.anchorZ = value + } + } +} + +struct SphericTextDemo: View { + @State var rotateDeg: Double = 0.0 + @State var showModifier: Bool = false + @State var radius: CGFloat = 40.0 + @State var perspective: CGFloat = 0.0 + @State var characters = "ABCDE" + @State var wordsInput = "Test\n100" + @State var words = ["Test", "100"] + + var body: some View { + let wordsInputBinding = Binding(get: { + self.wordsInput + }, set: { + self.wordsInput = $0 + self.words = self.wordsInput.split(separator: "\n").map({ word -> String in + String(word) + }) + }) + GeometryReader { geo in + VStack { + Group { + ZStack { + SphericText(characters, $rotateDeg).hideOpposite(true).rangeOfOpposite(in: 145...210) + .radius(radius).perspective(perspective).frame(width: geo.size.width, height: 50) + } + HStack { + Spacer() + TextField("Input Spheric Characters", text: $characters).textFieldStyle(RoundedBorderTextFieldStyle()) + Spacer() + } + } + Divider().background(Color.white) + SphericText(words: words, degree_offset: $rotateDeg).wordSpacing(100.0).font( .system(size: 32.0)).wordColor(.red).hideOpposite(false).perspective(perspective).radius(radius).frame(width: geo.size.width) + Spacer(minLength: 50.0) + if #available(macOS 11.0, *) { + TextEditor(text: wordsInputBinding).textFieldStyle(RoundedBorderTextFieldStyle()).frame(width: 100, height: 100, alignment: .center) + } else { + // Fallback on earlier versions + } + Divider().background(Color.white) + Group { + HStack { + Spacer(minLength: 20) + Button(action: { + rotateDeg = 0.0 + }, label: { + Text("Rotate").padding(5).overlay(RoundedRectangle(cornerRadius: 10.0).stroke()) + }) + Slider(value: $rotateDeg, in: -360.0...360.0) + Text("\(rotateDeg, specifier: "%.2f")").foregroundColor(.white) + Spacer(minLength: 20) + } + HStack { + Spacer(minLength: 20) + Button(action: { + radius = 40.0 + }, label: { + Text("Radius").padding(5).overlay(RoundedRectangle(cornerRadius: 10.0).stroke()) + }) + Slider(value: $radius, in: 0...500) + Text("\(radius, specifier: "%.2f")").foregroundColor(.white) + Spacer(minLength: 20) + } + HStack {Spacer(minLength: 20) + Button(action: { + perspective = 0.0 + }, label: { + Text("Perspective").padding(5).overlay(RoundedRectangle(cornerRadius: 10.0).stroke()) + }) + Slider(value: $perspective, in: -1...1) + Text("\(perspective, specifier: "%.2f")").foregroundColor(.white) + Spacer(minLength: 0) + } + } + Spacer(minLength: 40) + }.background(Color.black) + } + } +} + +struct SphericText_Previews: + PreviewProvider { + static var previews: some View { + SphericTextDemo() + } +} From 42dd50d66822f160fab8125d61545001aeeed010 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Mon, 17 May 2021 20:31:06 +0800 Subject: [PATCH 02/41] 1. Fix blur range bug 2. Update each text frame depends on font size. 3. Re organize preview to show better result. --- Sources/Rings/SphericText.swift | 115 ++++++++++++++++++++++++-------- 1 file changed, 87 insertions(+), 28 deletions(-) diff --git a/Sources/Rings/SphericText.swift b/Sources/Rings/SphericText.swift index 00fdf42..08920fe 100644 --- a/Sources/Rings/SphericText.swift +++ b/Sources/Rings/SphericText.swift @@ -23,7 +23,8 @@ struct SphericText: View { private var frontMostRange = -10...10 private var perspective: CGFloat = 0.0 private var anchorZ: CGFloat = 100.0 - @State private var estHight: CGFloat = 0.0 + @State private var estHeight: CGFloat = 30.0 + @State private var estWidth: CGFloat = 30.0 init(words: [String], word_spacing: T = 30.0 , font: Font? = nil, word_color: Color = Color.white, word_background: Color = Color.clear, hide_opposite: Bool = false, degree_offset: Binding = .constant(0)) { stringTable = words.enumerated().map { (i, e) in @@ -70,9 +71,9 @@ struct SphericText: View { let deg = Double(i)*wordSpacing + Double(offsetDegree) let _ = print("degrees: \(deg)") let normalizedD = Int(deg)%360 - let shouldBlur = blurMinors ? (frontMostRange ~= normalizedD) : false + let shouldBlur = blurMinors ? !(frontMostRange ~= abs(normalizedD)) : false let shouldHide = hideOpposite ? (oppositeRange ~= abs(normalizedD)) : false - Text(e).frame(width: w/2, height: estHight, alignment: .center) + Text(e).frame(width: estWidth*CGFloat(e.count), height: 50, alignment: Alignment(horizontal: .center, vertical: .center)) .font(font) .foregroundColor(wordColor) .background(wordBackground) @@ -82,12 +83,15 @@ struct SphericText: View { anchor: .center, anchorZ: anchorZ, perspective: perspective - ).opacity(shouldHide ? 0.0 : 1.0).blur(radius: shouldBlur ? 0.0 : 1.0) - } + ).opacity(shouldHide ? 0.0 : (shouldBlur ? 0.5 : 1.0)) + .blur(radius: (shouldBlur ? 1.0 : 0.0)) + }.frame(width: w, height: estHeight, alignment: .center) - }.frame(width: frameL.size.width, height: estHight).onPreferenceChange(ViewSizeKey.self) { sizes in - estHight = (sizes.reduce(0, {$0 + $1.height}))/CGFloat(sizes.count)*CGFloat(1.1) + }.frame(width: frameL.size.width, height: frameL.size.height).onPreferenceChange(ViewSizeKey.self) { sizes in + estHeight = (sizes.reduce(0, {$0 + $1.height}))/CGFloat(sizes.count)*CGFloat(1.1) + estWidth = (sizes.reduce(0, {$0 + $1.width}))/CGFloat(sizes.count) } + } } } @@ -146,6 +150,12 @@ extension SphericText : Adjustable { tmp.anchorZ = value } } + + public func blurMinors(_ isBlur: Bool) -> Self { + setProperty { tmp in + tmp.blurMinors = isBlur + } + } } struct SphericTextDemo: View { @@ -156,6 +166,11 @@ struct SphericTextDemo: View { @State var characters = "ABCDE" @State var wordsInput = "Test\n100" @State var words = ["Test", "100"] + @State var blurMinors: Bool = false + @State var hideOpposite: Bool = false + @State var textColor: Color = .white + @State var backgroundColor: Color = .clear + @State var wordSpacing: Double = 100.0 var body: some View { let wordsInputBinding = Binding(get: { @@ -168,33 +183,77 @@ struct SphericTextDemo: View { }) GeometryReader { geo in VStack { - Group { - ZStack { - SphericText(characters, $rotateDeg).hideOpposite(true).rangeOfOpposite(in: 145...210) - .radius(radius).perspective(perspective).frame(width: geo.size.width, height: 50) + HStack { + let width = geo.size.width/2 + VStack { + ZStack { + SphericText(characters, $rotateDeg).rangeOfOpposite(in: 145...210) + .radius(radius) + .perspective(perspective) + .blurMinors(blurMinors) + .hideOpposite(hideOpposite) + .frame(width: width) + } + HStack { + Spacer() + TextField("Input Spheric Characters", text: $characters).textFieldStyle(RoundedBorderTextFieldStyle()) + Spacer() + } + Toggle("Blur Minor", isOn: $blurMinors) + Toggle("Hide Opposite", isOn: $hideOpposite) } - HStack { - Spacer() - TextField("Input Spheric Characters", text: $characters).textFieldStyle(RoundedBorderTextFieldStyle()) - Spacer() + Divider().background(Color.white) + VStack { + SphericText(words: words, degree_offset: $rotateDeg).wordSpacing(wordSpacing).font( .system(size: 32.0)).wordColor(textColor).wordBackground(backgroundColor).hideOpposite(false).perspective(perspective).radius(radius).frame(width: width) + if #available(macOS 11.0, iOS 14.0, macCatalyst 14.0, *) { + TextEditor(text: wordsInputBinding).textFieldStyle(RoundedBorderTextFieldStyle()).frame(width: 100, height: 80, alignment: .center) + } else { + // Fallback on earlier versions + Text(words.reduce("", { result, word in + result + word + "," + })) + } + HStack { + Spacer() + Text("text:") + Picker("", selection: $textColor) { + Text("White").tag(Color.white) + Text("Red").tag(Color.red) + Text("Blue").tag(Color.blue) + Text("Green").tag(Color.green) + }.colorPicker($textColor) + Spacer() + Text("background:") + Picker("", selection: $backgroundColor) { + Text("Clear").tag(Color.clear) + Text("White").tag(Color.white) + Text("Red").tag(Color.red) + Text("Blue").tag(Color.blue) + Text("Green").tag(Color.green) + }.colorPicker($backgroundColor) + Spacer() + } + HStack { + Spacer(minLength: 20) + Button(action: { + wordSpacing = 100.0 + }, label: { + Text("word spacing") + }) + Slider(value: $wordSpacing, in: 50...200, step: 5.0) + Text("\(wordSpacing, specifier: "%.2f")").foregroundColor(.white) + Spacer(minLength: 20) + } } } Divider().background(Color.white) - SphericText(words: words, degree_offset: $rotateDeg).wordSpacing(100.0).font( .system(size: 32.0)).wordColor(.red).hideOpposite(false).perspective(perspective).radius(radius).frame(width: geo.size.width) - Spacer(minLength: 50.0) - if #available(macOS 11.0, *) { - TextEditor(text: wordsInputBinding).textFieldStyle(RoundedBorderTextFieldStyle()).frame(width: 100, height: 100, alignment: .center) - } else { - // Fallback on earlier versions - } - Divider().background(Color.white) Group { HStack { Spacer(minLength: 20) Button(action: { rotateDeg = 0.0 }, label: { - Text("Rotate").padding(5).overlay(RoundedRectangle(cornerRadius: 10.0).stroke()) + Text("Rotate") }) Slider(value: $rotateDeg, in: -360.0...360.0) Text("\(rotateDeg, specifier: "%.2f")").foregroundColor(.white) @@ -205,7 +264,7 @@ struct SphericTextDemo: View { Button(action: { radius = 40.0 }, label: { - Text("Radius").padding(5).overlay(RoundedRectangle(cornerRadius: 10.0).stroke()) + Text("Radius") }) Slider(value: $radius, in: 0...500) Text("\(radius, specifier: "%.2f")").foregroundColor(.white) @@ -215,14 +274,14 @@ struct SphericTextDemo: View { Button(action: { perspective = 0.0 }, label: { - Text("Perspective").padding(5).overlay(RoundedRectangle(cornerRadius: 10.0).stroke()) + Text("Perspective") }) Slider(value: $perspective, in: -1...1) Text("\(perspective, specifier: "%.2f")").foregroundColor(.white) - Spacer(minLength: 0) + Spacer(minLength: 20) } } - Spacer(minLength: 40) + Spacer(minLength: 10) }.background(Color.black) } } From 5d7c4ac02c8bfd73a528cce78ad8a479d54bdbde Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Tue, 18 May 2021 15:00:33 +0800 Subject: [PATCH 03/41] 1. Publishing SphericText 2. Make extension functions generic. --- Sources/Rings/SphericText.swift | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Sources/Rings/SphericText.swift b/Sources/Rings/SphericText.swift index 08920fe..7340112 100644 --- a/Sources/Rings/SphericText.swift +++ b/Sources/Rings/SphericText.swift @@ -8,7 +8,7 @@ import SwiftUI import CoreGraphicsExtension -struct SphericText: View { +public struct SphericText: View { @Binding var offsetDegree: T @@ -26,7 +26,7 @@ struct SphericText: View { @State private var estHeight: CGFloat = 30.0 @State private var estWidth: CGFloat = 30.0 - init(words: [String], word_spacing: T = 30.0 , font: Font? = nil, word_color: Color = Color.white, word_background: Color = Color.clear, hide_opposite: Bool = false, degree_offset: Binding = .constant(0)) { + public init(words: [String], word_spacing: T = 30.0 , font: Font? = nil, word_color: Color = Color.white, word_background: Color = Color.clear, hide_opposite: Bool = false, degree_offset: Binding = .constant(0)) { stringTable = words.enumerated().map { (i, e) in return (i, e) } @@ -38,7 +38,7 @@ struct SphericText: View { hideOpposite = hide_opposite } - init(_ text: String, _ rotateDegree: Binding) { + public init(_ text: String, _ rotateDegree: Binding) { let words = Array(text).map { c -> String in String(c) } @@ -46,19 +46,19 @@ struct SphericText: View { self.init(words: words, word_spacing: T(spacing), degree_offset: rotateDegree) } - init(_ text: String, word_spacing: T, _ rotateDegree: Binding) { + public init(_ text: String, word_spacing: T, _ rotateDegree: Binding) { let words = Array(text).map { c -> String in String(c) } self.init(words: words, word_spacing: word_spacing, degree_offset: rotateDegree) } - init(list: [String], _ rotateDegree: Binding) { + public init(list: [String], _ rotateDegree: Binding) { let spacing = 360.0/Double(list.count) self.init(words: list, word_spacing: T(spacing), degree_offset: rotateDegree) } - var body: some View { + public var body: some View { GeometryReader { geo in let w = geo.size.width let frameL = geo.frame(in: .local) @@ -97,9 +97,9 @@ struct SphericText: View { } extension SphericText : Adjustable { - public func wordSpacing(_ spacing: Double) -> SphericText { + public func wordSpacing(_ spacing: T) -> SphericText { setProperty { tmp in - tmp.wordSpacing = spacing + tmp.wordSpacing = Double(spacing) } } @@ -139,15 +139,15 @@ extension SphericText : Adjustable { } } - public func perspective(_ value: CGFloat) -> SphericText { + public func perspective(_ value: T) -> SphericText { setProperty { tmp in - tmp.perspective = value + tmp.perspective = CGFloat(value) } } - public func radius(_ value: CGFloat) -> SphericText { + public func radius(_ value: T) -> SphericText { setProperty { tmp in - tmp.anchorZ = value + tmp.anchorZ = CGFloat(value) } } @@ -159,7 +159,7 @@ extension SphericText : Adjustable { } struct SphericTextDemo: View { - @State var rotateDeg: Double = 0.0 + @State var rotateDeg: CGFloat = 0.0 @State var showModifier: Bool = false @State var radius: CGFloat = 40.0 @State var perspective: CGFloat = 0.0 @@ -170,7 +170,7 @@ struct SphericTextDemo: View { @State var hideOpposite: Bool = false @State var textColor: Color = .white @State var backgroundColor: Color = .clear - @State var wordSpacing: Double = 100.0 + @State var wordSpacing: CGFloat = 100.0 var body: some View { let wordsInputBinding = Binding(get: { From 536f4c717cd6cf3a8d235b4df34e90e5ef763798 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Tue, 18 May 2021 22:41:13 +0800 Subject: [PATCH 04/41] Update SphericText Document --- Sources/Rings/SphericText.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Rings/SphericText.md b/Sources/Rings/SphericText.md index 64e53d4..8664106 100644 --- a/Sources/Rings/SphericText.md +++ b/Sources/Rings/SphericText.md @@ -1,2 +1,6 @@ -# <#Title#> +## SphericText + +![Screen Recording 2021-05-18 at 22 32 45](https://user-images.githubusercontent.com/1284944/118671214-db754e00-b829-11eb-80b1-d5b3cf090035.gif) + +### Usage From efbaf3b08cad8c8da914340d3baa7105671cf148 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Tue, 18 May 2021 22:58:04 +0800 Subject: [PATCH 05/41] Update SphericText.md Update demo git, and Add usages --- Sources/Rings/SphericText.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/Rings/SphericText.md b/Sources/Rings/SphericText.md index 8664106..5a3a774 100644 --- a/Sources/Rings/SphericText.md +++ b/Sources/Rings/SphericText.md @@ -1,6 +1,14 @@ ## SphericText -![Screen Recording 2021-05-18 at 22 32 45](https://user-images.githubusercontent.com/1284944/118671214-db754e00-b829-11eb-80b1-d5b3cf090035.gif) +![Spheric Text Demo](https://user-images.githubusercontent.com/1284944/118671827-60f8fe00-b82a-11eb-9f0f-821841867cba.gif) ### Usage +```swift +// Create SphericText with String +SphericText("123456") + +// Create SphericText with word list +SphericText(words: ["123", "456", "789"]) + +``` From 4c7368f1cec4a2e89333550b799ae5d52bf51141 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Wed, 19 May 2021 16:54:38 +0800 Subject: [PATCH 06/41] Update initializer to make it access BinaryFloatPointer --- Sources/Rings/SphericText.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Rings/SphericText.swift b/Sources/Rings/SphericText.swift index 7340112..94ccaba 100644 --- a/Sources/Rings/SphericText.swift +++ b/Sources/Rings/SphericText.swift @@ -38,7 +38,7 @@ public struct SphericText: View { hideOpposite = hide_opposite } - public init(_ text: String, _ rotateDegree: Binding) { + public init(_ text: String, _ rotateDegree: Binding = .constant(0)) { let words = Array(text).map { c -> String in String(c) } @@ -46,16 +46,16 @@ public struct SphericText: View { self.init(words: words, word_spacing: T(spacing), degree_offset: rotateDegree) } - public init(_ text: String, word_spacing: T, _ rotateDegree: Binding) { + public init(_ text: String, word_spacing: T, _ rotateDegree: Binding = .constant(0)) { let words = Array(text).map { c -> String in String(c) } self.init(words: words, word_spacing: word_spacing, degree_offset: rotateDegree) } - public init(list: [String], _ rotateDegree: Binding) { - let spacing = 360.0/Double(list.count) - self.init(words: list, word_spacing: T(spacing), degree_offset: rotateDegree) + public init(words: [String], _ rotateDegree: Binding) { + let spacing = 360.0/Double(words.count) + self.init(words: words, word_spacing: T(spacing), degree_offset: rotateDegree) } public var body: some View { From ad9fbe66c110ea4c04a876433d4e94a34d3e63de Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Wed, 19 May 2021 16:54:59 +0800 Subject: [PATCH 07/41] Update document --- Sources/Rings/SphericText.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Sources/Rings/SphericText.md b/Sources/Rings/SphericText.md index 5a3a774..d7756d0 100644 --- a/Sources/Rings/SphericText.md +++ b/Sources/Rings/SphericText.md @@ -11,4 +11,19 @@ SphericText("123456") // Create SphericText with word list SphericText(words: ["123", "456", "789"]) +// Create SphericText and bind it with variable +@State var degrees: CGFloat +SphericText("123456", $degrees) + +// Adjust SphericText appearance +SphericText("123456") + .wordSpacing(wordSpacing) // modify space between words + .font(.system(size: 32.0)) // adjust font family and size + .wordColor(textColor) // change text color + .wordBackground(backgroundColor) // change text background color + .blurMinors(blurMinors) // bluring words which are not front most + .rangeOfOpposite(in: 145...210) // specify the opposite range of the most front word + .hideOpposite(true) // hide words located in opposite range + .perspective(perspective) // adjust viewing point + .radius(radius) // adjust the radius of spheric ``` From 336745348cd69f7ef0e6adf4b5b02516e7874b9c Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 19 May 2021 16:59:39 +0800 Subject: [PATCH 08/41] Update README.md --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1de68af..b85aba2 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,10 @@ It includes following controls, click to see what it looks like: * **[ClockIndex](#clockindex)** * **[HandAiguille](#handaiguille)** * **[ArchimedeanSpiralText](#archimedeanspiraltext)** +* **[SphericText](#spherictext)** and following functions are in progress: -* SphericText + * Knob --- @@ -78,6 +79,13 @@ targets: [ ### ![How to use it](Sources/Rings/ArchimedeanSpiralText.md) +## SphericText + +### What it looks like: +![Spheric Text Demo](https://user-images.githubusercontent.com/1284944/118671827-60f8fe00-b82a-11eb-9f0f-821841867cba.gif) + +### ![How to use it](Sources/Rings/SphericText.md) + --- # License Rings is released under the [MIT License](LICENSE). From ecf02cfc1923fddee29adc0246bc4091538af1e3 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Mon, 24 May 2021 15:07:10 +0800 Subject: [PATCH 09/41] Implement Knob Basic 1. Knob -- a control that can change its value with single-finger circular gesture. Developer can change its appearance by composing different KnobLayer, and customize the mapping between value and degree. 2. KnobLayer -- a protocol to provide view for different layer 3. RingKnobLayer -- drawing simple ring as Knob 4. ArcKnobLayer -- a layer that drawing arc when knob value changed. 5. ImageKnobLayer -- provide rotatable image for Knob. --- Sources/Rings/Knob.swift | 213 ++++++++++++++++++ Sources/Rings/KnobLayers/ArcKnobLayer.swift | 59 +++++ Sources/Rings/KnobLayers/ImageKnobLayer.swift | 23 ++ Sources/Rings/KnobLayers/KnobLayer.swift | 88 ++++++++ Sources/Rings/KnobLayers/RingKnobLayer.swift | 36 +++ 5 files changed, 419 insertions(+) create mode 100644 Sources/Rings/Knob.swift create mode 100644 Sources/Rings/KnobLayers/ArcKnobLayer.swift create mode 100644 Sources/Rings/KnobLayers/ImageKnobLayer.swift create mode 100644 Sources/Rings/KnobLayers/KnobLayer.swift create mode 100644 Sources/Rings/KnobLayers/RingKnobLayer.swift diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift new file mode 100644 index 0000000..0a2e5c0 --- /dev/null +++ b/Sources/Rings/Knob.swift @@ -0,0 +1,213 @@ +// +// Knob.swift +// +// +// Created by Chen-Hai Teng on 2021/5/21. +// + +import SwiftUI + +public protocol KnobMapping { + func degree(from value: Double) -> Double + func degree(delta value: Double) -> Double + func value(from degree: Double) -> Double + func value(delta degree: Double) -> Double + func configure(with knob: Knob) -> Self +} + +extension KnobMapping { + func configure(with knob: Knob) -> Self { + return self + } +} + +public struct LinearMapping : KnobMapping, Adjustable { + var minDegree: Double = -225 + var maxDegree: Double = 45 + var minValue: Double = 0.0 + var maxValue: Double = 1.0 + + public func degree(from value: Double) -> Double { + if(value < minValue) { + return minDegree + } + if(value > maxValue) { + return maxDegree + } + let ratio = (value - minValue)/(maxValue - minValue) + return ratio * (maxDegree - minDegree) + minDegree + } + + public func degree(delta value: Double) -> Double { + return value * (maxDegree - minDegree) / (maxValue - minValue) + } + + public func value(from degree: Double) -> Double { + if(degree < minDegree) { + return minValue + } + if(degree > maxDegree) { + return maxValue + } + let ratio = (degree - minDegree)/(maxDegree - minDegree) + return ratio * (maxValue - minValue) + minValue + } + + public func value(delta degree: Double) -> Double { + return degree * (maxValue - minValue) / (maxDegree - minDegree) + } + + public func configure(with knob: Knob) -> Self { + setProperty { tmp in + tmp.minDegree = knob.minDegree + tmp.maxDegree = knob.maxDegree + tmp.minValue = knob.minValue + tmp.maxValue = knob.maxValue + } + } +} + +extension CGPoint { + static func -(left: CGPoint, right: CGPoint) -> CGVector { + // The origin of View's coordinate is on left-top, adjust it to left-bottom to fit mathmatic behaviour. + return CGVector(dx: left.x - right.x, dy: right.y - left.y) + } +} + +extension CGVector { + + static func adjustedAtan2(y: T ,x: T) -> T where T: BinaryFloatingPoint { + let result = atan2(CGFloat(y), CGFloat(x)) + return T(result + (y < 0 ? 2*CGFloat.pi : 0)) + } + + static func angularDistance(v1: CGVector, v2: CGVector) -> Angle { + let angle2 = adjustedAtan2(y: v2.dy, x: v2.dx) + let angle1 = adjustedAtan2(y: v1.dy, x: v1.dx) + return Angle(radians: Double(angle2 - angle1)) + } +} + +public struct Knob: View { + private var layers: [AnyKnobLayer] = [] + var minDegree: Double = -225 + var maxDegree: Double = 45 + var minValue: Double = 0.0 + var maxValue: Double = 1.0 + + private var mappingObj: KnobMapping = LinearMapping() + + @Binding var value: Double + @GestureState var previousVector: CGVector = .zero + + //Debug State + @State var currentVector: CGVector = .zero + @State var deltaAngle: Angle = .zero + @State var currentAngle: Angle = .zero + @State var prevsAngle: Angle = .zero + + + init(_ value: Binding) { + _value = Binding(get: { + Double(value.wrappedValue) + }, set: { v in + value.wrappedValue = F(v) + }) + } + + public var body: some View { + GeometryReader { geo in + let center = CGPoint(x: geo.size.width/2, y: geo.size.height/2) + let radius = min(geo.size.width, geo.size.height)/2.0 + let mapping = mappingObj.configure(with: self) + + let pt = CGPoint(x: center.x + previousVector.dx, y: center.y - previousVector.dy) + + ZStack { + Path { p in + p.move(to: CGPoint(x: center.x - radius, y: center.y)) + p.addLine(to: CGPoint(x: center.x + radius, y: center.y)) + p.move(to: CGPoint(x: center.x, y: center.y - radius)) + p.addLine(to: CGPoint(x: center.x, y: center.y + radius)) + }.stroke() + Path { p in + p.move(to: center) + p.addLine(to: pt) + }.stroke() + HStack { + VStack { + Text("current") + Text(String(format: "x: %.2f, y:%.2f", currentVector.dx, currentVector.dy)).frame(height: 20) + Text(String(format: "angle: %.2f", currentAngle.degrees)) + Divider() + Text("previous") + Text(String(format: "x: %.2f, y:%.2f", previousVector.dx, previousVector.dy)).frame(height: 20) + Text(String(format: "angle: %.2f", prevsAngle.degrees)) + Divider() + Text(String(format: "delta Angle: %.2f", deltaAngle.degrees)) + }.frame(width: 120) + Spacer() + } + ForEach(layers.indices) { index in + layers[index].degreeRange(minDegree...maxDegree).degree(mapping.degree(from: value)).view.frame(width: geo.size.width, height: geo.size.height, alignment: .center) + } + }.contentShape(Circle()).gesture(DragGesture().onChanged({ value in + if(previousVector != CGVector.zero) { + currentVector = value.location - center + currentAngle = Angle.radians(Double(CGVector.adjustedAtan2(y: currentVector.dy, x: currentVector.dx))) + prevsAngle = Angle.radians(Double(CGVector.adjustedAtan2(y: previousVector.dy, x: previousVector.dx))) + deltaAngle = CGVector.angularDistance(v1: currentVector, v2: previousVector) + let deltaValue = mapping.value(delta: deltaAngle.degrees) + self.value += deltaValue + } + }).onEnded({ value in + currentVector = .zero + deltaAngle = .zero + prevsAngle = .zero + currentAngle = .zero + }).updating($previousVector, body: { value, state, transaction in + state = value.location - center + })) + } + } +} + +extension Knob : Adjustable { + func addLayer(_ layer: L) -> Self where L : KnobLayer { + setProperty { tmp in + tmp.layers.append(AnyKnobLayer(layer)) + } + } + + func mapping(with mapping: T) -> Self { + setProperty { tmp in + tmp.mappingObj = mapping.configure(with: tmp) + } + } +} + +struct KnobDemo: View { + @State var testValue: CGFloat = 0 + @State var ringWidth: CGFloat = 5 + @State var arcWidth: CGFloat = 5 + var body: some View { + VStack { + Knob($testValue).addLayer(RingKnobLayer().ringColor(Color.blue).ringWidth(ringWidth)).addLayer(ArcKnobLayer()) + Slider(value: $ringWidth, in: 5.0...25.0, step: 1.0) { + Text(String(format: "Ring Width: %.2f", ringWidth)) + } + Slider(value: $arcWidth, in: 5.0...25.0, step: 1.0) { + Text(String(format: "Arc Width: %.2f", arcWidth)) + } + Slider(value: $testValue, in: 0.0...1.0) { + Text("test value") + } + } + } +} + +struct Knob_Previews: PreviewProvider { + static var previews: some View { + KnobDemo() + } +} diff --git a/Sources/Rings/KnobLayers/ArcKnobLayer.swift b/Sources/Rings/KnobLayers/ArcKnobLayer.swift new file mode 100644 index 0000000..0a6f9aa --- /dev/null +++ b/Sources/Rings/KnobLayers/ArcKnobLayer.swift @@ -0,0 +1,59 @@ +// +// ArcKnobLayer.swift +// +// +// Created by Chen-Hai Teng on 2021/5/22. +// + +import SwiftUI + +public struct ArcKnobLayer : KnobLayer { + public var isFixed: Bool = false + public var minDegree: Double = 0.0 + public var maxDegree: Double = 0.0 + private var _degree: CGFloat = 120.0 + public var degree: CGFloat { + get { + return _degree + } + set { + if newValue > CGFloat(maxDegree) { + _degree = CGFloat(maxDegree) + } else if newValue < CGFloat(minDegree) { + _degree = CGFloat(minDegree) + } else { + _degree = newValue + } + } + } + + public var view: AnyView { + get { + AnyView(ZStack { + GeometryReader { geo in + Path { p in + let radius = min(geo.size.height, geo.size.width)/2 + p.addArc(center: CGPoint(x: geo.size.width/2, y: geo.size.height/2), radius: radius, startAngle: Angle.degrees(minDegree), endAngle: Angle.degrees(Double(degree)), clockwise: false) + }.stroke(arcColor, lineWidth: arcWidth).opacity(0.5) + } + }) + } + } + + private var arcWidth: CGFloat = 5.0 + private var arcColor: Color = .white +} + +extension ArcKnobLayer : Adjustable { + public func arcWidth(_ width:F) -> Self where F: BinaryFloatingPoint { + setProperty { tmp in + tmp.arcWidth = CGFloat(width) + } + } + + public func arcColor(_ color:Color) -> Self { + setProperty { tmp in + tmp.arcColor = color + } + } +} diff --git a/Sources/Rings/KnobLayers/ImageKnobLayer.swift b/Sources/Rings/KnobLayers/ImageKnobLayer.swift new file mode 100644 index 0000000..c87caaf --- /dev/null +++ b/Sources/Rings/KnobLayers/ImageKnobLayer.swift @@ -0,0 +1,23 @@ +// +// ImageKnobLayer.swift +// +// +// Created by Chen-Hai Teng on 2021/5/22. +// + +import SwiftUI + +struct ImageKnobLayer : KnobLayer { + var image: Image + var isFixed: Bool + + var minDegree: Double = 0.0 + var maxDegree: Double = 0.0 + var degree: CGFloat + + var view: AnyView { + get { + AnyView(image.rotationEffect(Angle.degrees(Double(degree)))) + } + } +} diff --git a/Sources/Rings/KnobLayers/KnobLayer.swift b/Sources/Rings/KnobLayers/KnobLayer.swift new file mode 100644 index 0000000..74c28f6 --- /dev/null +++ b/Sources/Rings/KnobLayers/KnobLayer.swift @@ -0,0 +1,88 @@ +// +// KnobLayer.swift +// +// +// Created by Chen-Hai Teng on 2021/5/22. +// + +import SwiftUI + +public protocol KnobLayer { + var isFixed: Bool { get set } + var minDegree: Double { get set } + var maxDegree: Double { get set } + var degree: CGFloat { get set } + var view: AnyView { get } +} + +public struct AnyKnobLayer: KnobLayer { + var rawLayer: KnobLayer + public var isFixed: Bool { + get { + return rawLayer.isFixed + } + set { + rawLayer.isFixed = newValue + } + } + + public var minDegree: Double { + get { + rawLayer.minDegree + } + set { + rawLayer.minDegree = newValue + } + } + public var maxDegree: Double { + get { + rawLayer.maxDegree + } + set { + rawLayer.maxDegree = newValue + } + } + + public var degree: CGFloat { + get { + return rawLayer.degree + } + set { + rawLayer.degree = newValue + } + } + + public var view: AnyView { + get { + rawLayer.view + } + } + + public init(_ knobLayer: T) where T: KnobLayer { + self.rawLayer = knobLayer + } +} + +extension KnobLayer { + func setBaseProperty(_ setBlock: (_ text: inout Self) -> Void) -> Self { + let result = _setProperty(content: self) { (tmp :inout Self) in + setBlock(&tmp) + return tmp + } + return result + } + + public func degree(_ degree: F) -> Self where F: BinaryFloatingPoint { + setBaseProperty { tmp in + tmp.degree = CGFloat(degree) + } + } + + public func degreeRange(_ range: ClosedRange) -> Self where F: BinaryFloatingPoint { + setBaseProperty { tmp in + tmp.minDegree = Double(range.lowerBound) + tmp.maxDegree = Double(range.upperBound) + } + } +} + diff --git a/Sources/Rings/KnobLayers/RingKnobLayer.swift b/Sources/Rings/KnobLayers/RingKnobLayer.swift new file mode 100644 index 0000000..048e070 --- /dev/null +++ b/Sources/Rings/KnobLayers/RingKnobLayer.swift @@ -0,0 +1,36 @@ +// +// SwiftUIView.swift +// +// +// Created by Chen-Hai Teng on 2021/5/22. +// + +import SwiftUI + +struct RingKnobLayer : KnobLayer { + var isFixed: Bool = true + var minDegree: Double = 0.0 + var maxDegree: Double = 0.0 + var degree: CGFloat = 0.0 + + var view: AnyView { + get { + AnyView(Circle().stroke(ringColor, lineWidth: ringWidth).padding(EdgeInsets(top: ringWidth/2.0, leading: ringWidth/2.0, bottom: ringWidth/2.0, trailing: ringWidth/2.0))) + } + } + private var ringColor: Color = .white + private var ringWidth: CGFloat = 2.0 +} + +extension RingKnobLayer : Adjustable { + func ringColor(_ color: Color) -> Self { + setProperty { tmp in + tmp.ringColor = color + } + } + func ringWidth(_ width: T) -> Self where T:BinaryFloatingPoint { + setProperty { tmp in + tmp.ringWidth = CGFloat(width) + } + } +} From 84595aa3350016da372337094ffc3975896b4333 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Mon, 24 May 2021 15:09:15 +0800 Subject: [PATCH 10/41] Update preview provider name to avoid default name conflict. --- Sources/Rings/ArchimedeanSpiralPath.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Rings/ArchimedeanSpiralPath.swift b/Sources/Rings/ArchimedeanSpiralPath.swift index ecf8086..700e4fe 100644 --- a/Sources/Rings/ArchimedeanSpiralPath.swift +++ b/Sources/Rings/ArchimedeanSpiralPath.swift @@ -113,7 +113,7 @@ struct ArchimedeanSpiralPathDemo : View { } } -struct SwiftUIView_Previews: PreviewProvider { +struct ArchimedeanSpiralPath_Previews: PreviewProvider { static var previews: some View { ArchimedeanSpiralPathDemo() } From 874c6189a41bc216222d8f44abd9f0a6e67bd900 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Tue, 25 May 2021 17:28:44 +0800 Subject: [PATCH 11/41] 1. Modify angular distance calculation. a. process the result of atan2 when x < 0 b. process the situation when move cross quadrant 3 and 4 2. Add blue print property to show/hide coordinate line, also reorganize the layout to show it. 3. Refactor initializer to setup default mapping. 4. Update ArcKnobLayer to reflect effect of line width. 5. Make CircleKnobLayer can change line color with gradient --- Sources/Rings/Knob.swift | 110 +++++++++++-------- Sources/Rings/KnobLayers/ArcKnobLayer.swift | 2 +- Sources/Rings/KnobLayers/RingKnobLayer.swift | 14 ++- 3 files changed, 77 insertions(+), 49 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index 0a2e5c0..82fb7c6 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -16,7 +16,7 @@ public protocol KnobMapping { } extension KnobMapping { - func configure(with knob: Knob) -> Self { + public func configure(with knob: Knob) -> Self { return self } } @@ -75,16 +75,25 @@ extension CGPoint { } extension CGVector { - + // atan2 only holds when x > 0. + // When x < 0, the angle apparent from the expression above is + // pointing in the opposite direction of the correct angle, + // and a value of π (or 180°) must be either added or subtracted + // from θ to put the Cartesian point (x, y) into the correct quadrant + // of the Euclidean plane. static func adjustedAtan2(y: T ,x: T) -> T where T: BinaryFloatingPoint { let result = atan2(CGFloat(y), CGFloat(x)) - return T(result + (y < 0 ? 2*CGFloat.pi : 0)) + return T(result + ((x < 0 && y < 0) ? 2*CGFloat.pi : 0)) } static func angularDistance(v1: CGVector, v2: CGVector) -> Angle { let angle2 = adjustedAtan2(y: v2.dy, x: v2.dx) let angle1 = adjustedAtan2(y: v1.dy, x: v1.dx) - return Angle(radians: Double(angle2 - angle1)) + if(v1.dy*v2.dy > 0 && v2.dx*v1.dx < 0) { // v1, v2 cross quadrant 3 and 4 + return Angle.radians(Double(atan2(v2.dy, v2.dx) - atan2(v1.dy, v1.dx))) + } else { + return Angle(radians: Double(angle2 - angle1)) + } } } @@ -95,24 +104,24 @@ public struct Knob: View { var minValue: Double = 0.0 var maxValue: Double = 1.0 - private var mappingObj: KnobMapping = LinearMapping() + private var mappingObj: KnobMapping @Binding var value: Double @GestureState var previousVector: CGVector = .zero + private var blueprint: Bool = false + //Debug State @State var currentVector: CGVector = .zero @State var deltaAngle: Angle = .zero - @State var currentAngle: Angle = .zero - @State var prevsAngle: Angle = .zero - - init(_ value: Binding) { + init(_ value: Binding, _ mapping: KnobMapping = LinearMapping(minDegree: -225, maxDegree: 45, minValue: 0.0, maxValue: 1.0)) { _value = Binding(get: { Double(value.wrappedValue) }, set: { v in value.wrappedValue = F(v) }) + mappingObj = mapping } public var body: some View { @@ -124,47 +133,41 @@ public struct Knob: View { let pt = CGPoint(x: center.x + previousVector.dx, y: center.y - previousVector.dy) ZStack { - Path { p in - p.move(to: CGPoint(x: center.x - radius, y: center.y)) - p.addLine(to: CGPoint(x: center.x + radius, y: center.y)) - p.move(to: CGPoint(x: center.x, y: center.y - radius)) - p.addLine(to: CGPoint(x: center.x, y: center.y + radius)) - }.stroke() - Path { p in - p.move(to: center) - p.addLine(to: pt) - }.stroke() - HStack { - VStack { - Text("current") - Text(String(format: "x: %.2f, y:%.2f", currentVector.dx, currentVector.dy)).frame(height: 20) - Text(String(format: "angle: %.2f", currentAngle.degrees)) - Divider() - Text("previous") - Text(String(format: "x: %.2f, y:%.2f", previousVector.dx, previousVector.dy)).frame(height: 20) - Text(String(format: "angle: %.2f", prevsAngle.degrees)) - Divider() - Text(String(format: "delta Angle: %.2f", deltaAngle.degrees)) - }.frame(width: 120) - Spacer() - } ForEach(layers.indices) { index in layers[index].degreeRange(minDegree...maxDegree).degree(mapping.degree(from: value)).view.frame(width: geo.size.width, height: geo.size.height, alignment: .center) } + Group { + Path { p in + p.move(to: CGPoint(x: center.x - radius, y: center.y)) + p.addLine(to: CGPoint(x: center.x + radius, y: center.y)) + p.move(to: CGPoint(x: center.x, y: center.y - radius)) + p.addLine(to: CGPoint(x: center.x, y: center.y + radius)) + p.addEllipse(in: CGRect(x: center.x - radius, y: center.y - radius, width: 2*radius, height: 2*radius)) + }.stroke(Color.blue.opacity(0.5)) + Path { p in + p.move(to: center) + p.addLine(to: pt) + }.stroke(Color.blue.opacity(0.5)) + }.if(!blueprint) { content in + content.hidden() + } }.contentShape(Circle()).gesture(DragGesture().onChanged({ value in if(previousVector != CGVector.zero) { currentVector = value.location - center - currentAngle = Angle.radians(Double(CGVector.adjustedAtan2(y: currentVector.dy, x: currentVector.dx))) - prevsAngle = Angle.radians(Double(CGVector.adjustedAtan2(y: previousVector.dy, x: previousVector.dx))) deltaAngle = CGVector.angularDistance(v1: currentVector, v2: previousVector) let deltaValue = mapping.value(delta: deltaAngle.degrees) - self.value += deltaValue + var newValue = self.value + deltaValue + if(newValue > maxValue) { + newValue = maxValue + } + if(newValue < minValue) { + newValue = minValue + } + self.value = newValue } }).onEnded({ value in currentVector = .zero deltaAngle = .zero - prevsAngle = .zero - currentAngle = .zero }).updating($previousVector, body: { value, state, transaction in state = value.location - center })) @@ -184,23 +187,36 @@ extension Knob : Adjustable { tmp.mappingObj = mapping.configure(with: tmp) } } + + func blueprint(_ show: Bool) -> Self { + setProperty { tmp in + tmp.blueprint = show + } + } } struct KnobDemo: View { @State var testValue: CGFloat = 0 @State var ringWidth: CGFloat = 5 @State var arcWidth: CGFloat = 5 + @State var showBlueprint: Bool = false + let gradient = AngularGradient(gradient: Gradient(colors: [Color.red, Color.blue]), center: .center) var body: some View { VStack { - Knob($testValue).addLayer(RingKnobLayer().ringColor(Color.blue).ringWidth(ringWidth)).addLayer(ArcKnobLayer()) - Slider(value: $ringWidth, in: 5.0...25.0, step: 1.0) { - Text(String(format: "Ring Width: %.2f", ringWidth)) - } - Slider(value: $arcWidth, in: 5.0...25.0, step: 1.0) { - Text(String(format: "Arc Width: %.2f", arcWidth)) - } - Slider(value: $testValue, in: 0.0...1.0) { - Text("test value") + Spacer(minLength: 40) + Knob($testValue).addLayer(RingKnobLayer().ringColor(Gradient(colors: [.blue, .red, .red, .red, .red, .red, .blue])).ringWidth(ringWidth)).addLayer(ArcKnobLayer().arcWidth(arcWidth)).blueprint(showBlueprint) + Spacer(minLength: 40) + Group { + Slider(value: $ringWidth, in: 5.0...25.0, step: 1.0) { + Text(String(format: "Ring Width: %.2f", ringWidth)) + } + Slider(value: $arcWidth, in: 5.0...25.0, step: 1.0) { + Text(String(format: "Arc Width: %.2f", arcWidth)) + } + Slider(value: $testValue, in: 0.0...1.0) { + Text("test value") + } + Toggle("blue print", isOn: $showBlueprint) } } } diff --git a/Sources/Rings/KnobLayers/ArcKnobLayer.swift b/Sources/Rings/KnobLayers/ArcKnobLayer.swift index 0a6f9aa..a0575a0 100644 --- a/Sources/Rings/KnobLayers/ArcKnobLayer.swift +++ b/Sources/Rings/KnobLayers/ArcKnobLayer.swift @@ -32,7 +32,7 @@ public struct ArcKnobLayer : KnobLayer { AnyView(ZStack { GeometryReader { geo in Path { p in - let radius = min(geo.size.height, geo.size.width)/2 + let radius = min(geo.size.height, geo.size.width)/2.0 - arcWidth/2.0 p.addArc(center: CGPoint(x: geo.size.width/2, y: geo.size.height/2), radius: radius, startAngle: Angle.degrees(minDegree), endAngle: Angle.degrees(Double(degree)), clockwise: false) }.stroke(arcColor, lineWidth: arcWidth).opacity(0.5) } diff --git a/Sources/Rings/KnobLayers/RingKnobLayer.swift b/Sources/Rings/KnobLayers/RingKnobLayer.swift index 048e070..ead173b 100644 --- a/Sources/Rings/KnobLayers/RingKnobLayer.swift +++ b/Sources/Rings/KnobLayers/RingKnobLayer.swift @@ -15,10 +15,15 @@ struct RingKnobLayer : KnobLayer { var view: AnyView { get { - AnyView(Circle().stroke(ringColor, lineWidth: ringWidth).padding(EdgeInsets(top: ringWidth/2.0, leading: ringWidth/2.0, bottom: ringWidth/2.0, trailing: ringWidth/2.0))) + AnyView(Circle().stroke(ringAngularGradient(), lineWidth: ringWidth).padding(EdgeInsets(top: ringWidth/2.0, leading: ringWidth/2.0, bottom: ringWidth/2.0, trailing: ringWidth/2.0))) } } + private func ringAngularGradient() -> AngularGradient { + let gradient = ringGradient ?? Gradient(colors: [ringColor]) + return AngularGradient(gradient: gradient, center: .center, startAngle: Angle.degrees(minDegree), endAngle: Angle.degrees(maxDegree)) + } private var ringColor: Color = .white + private var ringGradient: Gradient? = nil private var ringWidth: CGFloat = 2.0 } @@ -26,6 +31,13 @@ extension RingKnobLayer : Adjustable { func ringColor(_ color: Color) -> Self { setProperty { tmp in tmp.ringColor = color + tmp.ringGradient = nil + } + } + func ringColor(_ gradient: Gradient) -> Self { + setProperty { tmp in + tmp.ringGradient = gradient + tmp.ringColor = .clear } } func ringWidth(_ width: T) -> Self where T:BinaryFloatingPoint { From 0b897dc479da41daa97c904e1f96ae78599199a7 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Tue, 25 May 2021 18:46:47 +0800 Subject: [PATCH 12/41] [Refactor] Extract default constants to enum --- Sources/Rings/Knob.swift | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index 82fb7c6..e3e75bc 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -7,6 +7,17 @@ import SwiftUI +enum Default { + enum Degree: Double { + case Min = -225.0 + case Max = 45.0 + } + enum Value: Double { + case Min = 0.0 + case Max = 1.0 + } +} + public protocol KnobMapping { func degree(from value: Double) -> Double func degree(delta value: Double) -> Double @@ -22,10 +33,10 @@ extension KnobMapping { } public struct LinearMapping : KnobMapping, Adjustable { - var minDegree: Double = -225 - var maxDegree: Double = 45 - var minValue: Double = 0.0 - var maxValue: Double = 1.0 + var minDegree: Double = Default.Degree.Min.rawValue + var maxDegree: Double = Default.Degree.Max.rawValue + var minValue: Double = Default.Value.Min.rawValue + var maxValue: Double = Default.Value.Max.rawValue public func degree(from value: Double) -> Double { if(value < minValue) { @@ -99,10 +110,10 @@ extension CGVector { public struct Knob: View { private var layers: [AnyKnobLayer] = [] - var minDegree: Double = -225 - var maxDegree: Double = 45 - var minValue: Double = 0.0 - var maxValue: Double = 1.0 + var minDegree: Double = Default.Degree.Min.rawValue + var maxDegree: Double = Default.Degree.Max.rawValue + var minValue: Double = Default.Value.Min.rawValue + var maxValue: Double = Default.Value.Max.rawValue private var mappingObj: KnobMapping @@ -111,11 +122,10 @@ public struct Knob: View { private var blueprint: Bool = false - //Debug State @State var currentVector: CGVector = .zero @State var deltaAngle: Angle = .zero - init(_ value: Binding, _ mapping: KnobMapping = LinearMapping(minDegree: -225, maxDegree: 45, minValue: 0.0, maxValue: 1.0)) { + init(_ value: Binding, _ mapping: KnobMapping = LinearMapping()) { _value = Binding(get: { Double(value.wrappedValue) }, set: { v in From ed978681146a9455c7bd6e380d6076f3ff7f6895 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Tue, 25 May 2021 20:50:52 +0800 Subject: [PATCH 13/41] [Refactor] 1. Remove useless configure(with knob:) from KnobMapping protocol 2. Add newValue(_:new:old:) to KnobMapping protocol to calculate new value based on old value, old angle, and new angle. 3. Extract crossQuadrant34 to make statements much clear and more reusable. 4. Remove useless value(from:), degree(delta:), value(delta:) from KnobMapping protocol, also make value(delta:) in LinearMapping to be private. --- Sources/Rings/Knob.swift | 86 +++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 49 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index e3e75bc..f2463c1 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -20,16 +20,7 @@ enum Default { public protocol KnobMapping { func degree(from value: Double) -> Double - func degree(delta value: Double) -> Double - func value(from degree: Double) -> Double - func value(delta degree: Double) -> Double - func configure(with knob: Knob) -> Self -} - -extension KnobMapping { - public func configure(with knob: Knob) -> Self { - return self - } + func newValue(_ v: Double, new: Angle, old: Angle) -> Double } public struct LinearMapping : KnobMapping, Adjustable { @@ -38,6 +29,19 @@ public struct LinearMapping : KnobMapping, Adjustable { var minValue: Double = Default.Value.Min.rawValue var maxValue: Double = Default.Value.Max.rawValue + public func newValue(_ v: Double, new: Angle, old: Angle) -> Double { + let deltaDegree = (old - new).degrees + let deltaValue = value(delta: deltaDegree) + var newValue = v + deltaValue + if newValue > maxValue { + newValue = maxValue + } + if newValue < minValue { + newValue = minValue + } + return newValue + } + public func degree(from value: Double) -> Double { if(value < minValue) { return minDegree @@ -49,33 +53,9 @@ public struct LinearMapping : KnobMapping, Adjustable { return ratio * (maxDegree - minDegree) + minDegree } - public func degree(delta value: Double) -> Double { - return value * (maxDegree - minDegree) / (maxValue - minValue) - } - - public func value(from degree: Double) -> Double { - if(degree < minDegree) { - return minValue - } - if(degree > maxDegree) { - return maxValue - } - let ratio = (degree - minDegree)/(maxDegree - minDegree) - return ratio * (maxValue - minValue) + minValue - } - - public func value(delta degree: Double) -> Double { + private func value(delta degree: Double) -> Double { return degree * (maxValue - minValue) / (maxDegree - minDegree) } - - public func configure(with knob: Knob) -> Self { - setProperty { tmp in - tmp.minDegree = knob.minDegree - tmp.maxDegree = knob.maxDegree - tmp.minValue = knob.minValue - tmp.maxValue = knob.maxValue - } - } } extension CGPoint { @@ -97,15 +77,32 @@ extension CGVector { return T(result + ((x < 0 && y < 0) ? 2*CGFloat.pi : 0)) } + static func crossQuadrant34(v1: CGVector, v2: CGVector) -> Bool { + return (v1.dy*v2.dy > 0 && v2.dx*v1.dx < 0) + } + static func angularDistance(v1: CGVector, v2: CGVector) -> Angle { let angle2 = adjustedAtan2(y: v2.dy, x: v2.dx) let angle1 = adjustedAtan2(y: v1.dy, x: v1.dx) - if(v1.dy*v2.dy > 0 && v2.dx*v1.dx < 0) { // v1, v2 cross quadrant 3 and 4 + if(crossQuadrant34(v1: v1, v2: v2)) { // v1, v2 cross quadrant 3 and 4 return Angle.radians(Double(atan2(v2.dy, v2.dx) - atan2(v1.dy, v1.dx))) } else { return Angle(radians: Double(angle2 - angle1)) } } + + func angle(_ shouldAdjust: Bool = false) -> Angle { + return Angle.radians(Double(radians(shouldAdjust))) + } + + func radians(_ shouldAdjust: Bool = false) -> CGFloat { + return shouldAdjust ? CGVector.adjustedAtan2(y: dy, x: dx) : atan2(dy, dx) + } + + func degrees(_ shouldAdjust: Bool = false) -> CGFloat { + return CGFloat(angle(shouldAdjust).degrees) + } +} } public struct Knob: View { @@ -138,13 +135,12 @@ public struct Knob: View { GeometryReader { geo in let center = CGPoint(x: geo.size.width/2, y: geo.size.height/2) let radius = min(geo.size.width, geo.size.height)/2.0 - let mapping = mappingObj.configure(with: self) let pt = CGPoint(x: center.x + previousVector.dx, y: center.y - previousVector.dy) ZStack { ForEach(layers.indices) { index in - layers[index].degreeRange(minDegree...maxDegree).degree(mapping.degree(from: value)).view.frame(width: geo.size.width, height: geo.size.height, alignment: .center) + layers[index].degreeRange(minDegree...maxDegree).degree(mappingObj.degree(from: value)).view.frame(width: geo.size.width, height: geo.size.height, alignment: .center) } Group { Path { p in @@ -164,16 +160,8 @@ public struct Knob: View { }.contentShape(Circle()).gesture(DragGesture().onChanged({ value in if(previousVector != CGVector.zero) { currentVector = value.location - center - deltaAngle = CGVector.angularDistance(v1: currentVector, v2: previousVector) - let deltaValue = mapping.value(delta: deltaAngle.degrees) - var newValue = self.value + deltaValue - if(newValue > maxValue) { - newValue = maxValue - } - if(newValue < minValue) { - newValue = minValue - } - self.value = newValue + let shouldAdjust = !CGVector.crossQuadrant34(v1: currentVector, v2: previousVector) + self.value = mappingObj.newValue(self.value, new: currentVector.angle(shouldAdjust), old: previousVector.angle(shouldAdjust)) } }).onEnded({ value in currentVector = .zero @@ -194,7 +182,7 @@ extension Knob : Adjustable { func mapping(with mapping: T) -> Self { setProperty { tmp in - tmp.mappingObj = mapping.configure(with: tmp) + tmp.mappingObj = mapping } } From f60879481a1c2febde70bf19b372cd2864a86b20 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Thu, 27 May 2021 21:35:15 +0800 Subject: [PATCH 14/41] [Refactor] 1. Create struct KnobGestureRecord to simplify the KnobMapping protocol 2. Move minDegree, maxDegree, minValue and maxValue from Knob to instance which conforming KnobMapping; also, provide property degreeRange in KnobMapping. 3. To make meaning clear, rename currentVector to nextVector and previousVector to currentVector. 4. Add startVector and startValue for future mapping object. --- Sources/Rings/InternalUtilities.swift | 2 +- Sources/Rings/Knob.swift | 142 ++++++++++++++++++++------ 2 files changed, 109 insertions(+), 35 deletions(-) diff --git a/Sources/Rings/InternalUtilities.swift b/Sources/Rings/InternalUtilities.swift index c967800..51dc3cd 100644 --- a/Sources/Rings/InternalUtilities.swift +++ b/Sources/Rings/InternalUtilities.swift @@ -13,7 +13,7 @@ internal func _setProperty(content: T, _ setBlock:(_ newContent: inout T) -> } -protocol Adjustable {} +public protocol Adjustable {} extension Adjustable { func setProperty(_ setBlock: (_ text: inout Self) -> Void) -> Self { diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index f2463c1..d480a20 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -18,28 +18,47 @@ enum Default { } } -public protocol KnobMapping { +public protocol KnobMapping : Adjustable { + var degreeRange: ClosedRange { get set } func degree(from value: Double) -> Double - func newValue(_ v: Double, new: Angle, old: Angle) -> Double + func newValue(_ record: KnobGestureRecord) -> Double +} + +extension KnobMapping { + func degreeRange(_ range: ClosedRange) -> Self { + setProperty { tmp in + tmp.degreeRange = range + } + } +} } public struct LinearMapping : KnobMapping, Adjustable { - var minDegree: Double = Default.Degree.Min.rawValue - var maxDegree: Double = Default.Degree.Max.rawValue + private var minDegree: Double { + get { + degreeRange.lowerBound + } + } + private var maxDegree: Double { + get { + degreeRange.upperBound + } + } + public var degreeRange = Default.Degree.Min.rawValue...Default.Degree.Max.rawValue var minValue: Double = Default.Value.Min.rawValue var maxValue: Double = Default.Value.Max.rawValue - public func newValue(_ v: Double, new: Angle, old: Angle) -> Double { - let deltaDegree = (old - new).degrees - let deltaValue = value(delta: deltaDegree) - var newValue = v + deltaValue - if newValue > maxValue { - newValue = maxValue + public func newValue(_ record: KnobGestureRecord) -> Double { + let deltaDegre = (record.current.angle - record.next.angle).degrees + let deltaValue = value(delta: deltaDegre) + var nextValue = record.current.value + deltaValue + if nextValue > maxValue { + nextValue = maxValue } - if newValue < minValue { - newValue = minValue + if nextValue < minValue { + nextValue = minValue } - return newValue + return nextValue } public func degree(from value: Double) -> Double { @@ -103,24 +122,31 @@ extension CGVector { return CGFloat(angle(shouldAdjust).degrees) } } + +public struct KnobGestureRecord { + struct Value { + var value: Double = .nan + var angle: Angle + } + var start: Value + var current: Value + var next: Value } public struct Knob: View { private var layers: [AnyKnobLayer] = [] - var minDegree: Double = Default.Degree.Min.rawValue - var maxDegree: Double = Default.Degree.Max.rawValue - var minValue: Double = Default.Value.Min.rawValue - var maxValue: Double = Default.Value.Max.rawValue private var mappingObj: KnobMapping @Binding var value: Double - @GestureState var previousVector: CGVector = .zero + @GestureState var currentVector: CGVector = .zero private var blueprint: Bool = false - @State var currentVector: CGVector = .zero + @State var nextVector: CGVector = .zero @State var deltaAngle: Angle = .zero + @State var startVector: CGVector = .zero + @State private var startValue: Double = .nan init(_ value: Binding, _ mapping: KnobMapping = LinearMapping()) { _value = Binding(get: { @@ -130,17 +156,16 @@ public struct Knob: View { }) mappingObj = mapping } - public var body: some View { GeometryReader { geo in let center = CGPoint(x: geo.size.width/2, y: geo.size.height/2) let radius = min(geo.size.width, geo.size.height)/2.0 - let pt = CGPoint(x: center.x + previousVector.dx, y: center.y - previousVector.dy) - + let pt = CGPoint(x: center.x + currentVector.dx, y: center.y - currentVector.dy) + let startPt = CGPoint(x: center.x + startVector.dx, y: center.y - startVector.dy) ZStack { ForEach(layers.indices) { index in - layers[index].degreeRange(minDegree...maxDegree).degree(mappingObj.degree(from: value)).view.frame(width: geo.size.width, height: geo.size.height, alignment: .center) + layers[index].degreeRange(mappingObj.degreeRange).degree(mappingObj.degree(from: value)).view.frame(width: geo.size.width, height: geo.size.height, alignment: .center) } Group { Path { p in @@ -154,21 +179,59 @@ public struct Knob: View { p.move(to: center) p.addLine(to: pt) }.stroke(Color.blue.opacity(0.5)) + Path { p in + p.move(to: center) + p.addLine(to: startPt) + }.stroke(Color.red.opacity(0.5)) }.if(!blueprint) { content in content.hidden() } }.contentShape(Circle()).gesture(DragGesture().onChanged({ value in - if(previousVector != CGVector.zero) { - currentVector = value.location - center - let shouldAdjust = !CGVector.crossQuadrant34(v1: currentVector, v2: previousVector) - self.value = mappingObj.newValue(self.value, new: currentVector.angle(shouldAdjust), old: previousVector.angle(shouldAdjust)) + if(currentVector != CGVector.zero) { + if(startValue.isNaN) { + startValue = self.value + } + startVector = value.startLocation - center + nextVector = value.location - center + + let shouldAdjust = !CGVector.crossQuadrant34(v1: nextVector, v2: currentVector) + + let valueStart = KnobGestureRecord.Value(value: startValue, angle: startVector.angle(shouldAdjust)) + + let valueCurrent = KnobGestureRecord.Value(value: self.value, angle: currentVector.angle(shouldAdjust)) + + let valueNext = KnobGestureRecord.Value(angle: nextVector.angle(shouldAdjust)) + + let _degree = mappingObj.degree(from: self.value) + if _degree >= mappingObj.degreeRange.upperBound { + if valueCurrent.angle.degrees > valueNext.angle.degrees { + return + } + } + + if _degree <= mappingObj.degreeRange.lowerBound { + if valueCurrent.angle.degrees < valueNext.angle.degrees { + return + } + } + + let record = KnobGestureRecord(start: valueStart, current:valueCurrent, next:valueNext) + let newValue = mappingObj.newValue(record) + if !newValue.isNaN { + if(self.value != newValue) { + self.value = newValue + } + } } }).onEnded({ value in - currentVector = .zero + nextVector = .zero deltaAngle = .zero - }).updating($previousVector, body: { value, state, transaction in + startValue = .nan + startVector = .zero + }).updating($currentVector, body: { value, state, transaction in state = value.location - center })) + } } } @@ -194,7 +257,8 @@ extension Knob : Adjustable { } struct KnobDemo: View { - @State var testValue: CGFloat = 0 + @State var valueSegmented: CGFloat = 0 + @State var valueContiune: CGFloat = 0 @State var ringWidth: CGFloat = 5 @State var arcWidth: CGFloat = 5 @State var showBlueprint: Bool = false @@ -202,7 +266,20 @@ struct KnobDemo: View { var body: some View { VStack { Spacer(minLength: 40) - Knob($testValue).addLayer(RingKnobLayer().ringColor(Gradient(colors: [.blue, .red, .red, .red, .red, .red, .blue])).ringWidth(ringWidth)).addLayer(ArcKnobLayer().arcWidth(arcWidth)).blueprint(showBlueprint) + HStack { + VStack { + Knob($valueContiune).addLayer(RingKnobLayer().ringColor(Gradient(colors: [.red, .blue, .blue, .blue, .blue, .blue, .red])).ringWidth(ringWidth)).addLayer(ArcKnobLayer().arcWidth(arcWidth)).blueprint(showBlueprint) + Slider(value: $valueContiune, in: 0.0...1.0) { + Text(String(format: "value: %.2f", valueContiune)) + } + } + VStack { + Knob($valueSegmented).addLayer(RingKnobLayer().ringColor(Gradient(colors: [.blue, .red, .red, .red, .red, .red, .blue])).ringWidth(ringWidth)).addLayer(ArcKnobLayer().arcWidth(arcWidth)).blueprint(showBlueprint).mapping(with: SegmentMapping().stops([KnobStop(0.0, -215.0), KnobStop(1.0, 45.0), KnobStop(0.5, -90.0), KnobStop(0.2, 0.0), KnobStop(0.8, -180.0), KnobStop(0.3, -135)])) + Slider(value: $valueSegmented, in: 0.0...1.0, step: 0.1) { + Text(String(format: "value: %.2f", valueSegmented)) + } + } + } Spacer(minLength: 40) Group { Slider(value: $ringWidth, in: 5.0...25.0, step: 1.0) { @@ -211,9 +288,6 @@ struct KnobDemo: View { Slider(value: $arcWidth, in: 5.0...25.0, step: 1.0) { Text(String(format: "Arc Width: %.2f", arcWidth)) } - Slider(value: $testValue, in: 0.0...1.0) { - Text("test value") - } Toggle("blue print", isOn: $showBlueprint) } } From be84d83c8c31e589fdcb28be70a18f6cf3d3f7fe Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Thu, 27 May 2021 23:15:06 +0800 Subject: [PATCH 15/41] [Refactor] 1. Rename the folder KnobLayout to KnobComponents 2. Add subfolder Layers 3. Move all layer related file to KnobComponents/Layers --- .../{KnobLayers => KnobComponents/Layers}/ArcKnobLayer.swift | 0 .../{KnobLayers => KnobComponents/Layers}/ImageKnobLayer.swift | 0 .../Rings/{KnobLayers => KnobComponents/Layers}/KnobLayer.swift | 0 .../{KnobLayers => KnobComponents/Layers}/RingKnobLayer.swift | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename Sources/Rings/{KnobLayers => KnobComponents/Layers}/ArcKnobLayer.swift (100%) rename Sources/Rings/{KnobLayers => KnobComponents/Layers}/ImageKnobLayer.swift (100%) rename Sources/Rings/{KnobLayers => KnobComponents/Layers}/KnobLayer.swift (100%) rename Sources/Rings/{KnobLayers => KnobComponents/Layers}/RingKnobLayer.swift (100%) diff --git a/Sources/Rings/KnobLayers/ArcKnobLayer.swift b/Sources/Rings/KnobComponents/Layers/ArcKnobLayer.swift similarity index 100% rename from Sources/Rings/KnobLayers/ArcKnobLayer.swift rename to Sources/Rings/KnobComponents/Layers/ArcKnobLayer.swift diff --git a/Sources/Rings/KnobLayers/ImageKnobLayer.swift b/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift similarity index 100% rename from Sources/Rings/KnobLayers/ImageKnobLayer.swift rename to Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift diff --git a/Sources/Rings/KnobLayers/KnobLayer.swift b/Sources/Rings/KnobComponents/Layers/KnobLayer.swift similarity index 100% rename from Sources/Rings/KnobLayers/KnobLayer.swift rename to Sources/Rings/KnobComponents/Layers/KnobLayer.swift diff --git a/Sources/Rings/KnobLayers/RingKnobLayer.swift b/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift similarity index 100% rename from Sources/Rings/KnobLayers/RingKnobLayer.swift rename to Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift From 4f757f65cb7eb7964d32a21486046293a90781a1 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Thu, 27 May 2021 23:21:47 +0800 Subject: [PATCH 16/41] [Refactor] Extract protocol KnobMapping to KnobMapping.swift --- Sources/Rings/Knob.swift | 18 ---------- .../KnobComponents/Mappings/KnobMapping.swift | 33 +++++++++++++++++++ 2 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 Sources/Rings/KnobComponents/Mappings/KnobMapping.swift diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index d480a20..19f423f 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -7,31 +7,13 @@ import SwiftUI -enum Default { - enum Degree: Double { - case Min = -225.0 - case Max = 45.0 } - enum Value: Double { - case Min = 0.0 - case Max = 1.0 - } -} - -public protocol KnobMapping : Adjustable { - var degreeRange: ClosedRange { get set } - func degree(from value: Double) -> Double - func newValue(_ record: KnobGestureRecord) -> Double } -extension KnobMapping { - func degreeRange(_ range: ClosedRange) -> Self { setProperty { tmp in - tmp.degreeRange = range } } } -} public struct LinearMapping : KnobMapping, Adjustable { private var minDegree: Double { diff --git a/Sources/Rings/KnobComponents/Mappings/KnobMapping.swift b/Sources/Rings/KnobComponents/Mappings/KnobMapping.swift new file mode 100644 index 0000000..f23fb58 --- /dev/null +++ b/Sources/Rings/KnobComponents/Mappings/KnobMapping.swift @@ -0,0 +1,33 @@ +// +// KnobMapping.swift +// +// +// Created by Chen-Hai Teng on 2021/5/27. +// + +import Foundation + +enum Default { + enum Degree: Double { + case Min = -225.0 + case Max = 45.0 + } + enum Value: Double { + case Min = 0.0 + case Max = 1.0 + } +} + +public protocol KnobMapping : Adjustable { + var degreeRange: ClosedRange { get set } + func degree(from value: Double) -> Double + func newValue(_ record: KnobGestureRecord) -> Double +} + +extension KnobMapping { + func degreeRange(_ range: ClosedRange) -> Self { + setProperty { tmp in + tmp.degreeRange = range + } + } +} From 8c0ff918ab08a1a8194ef99ba0bd200384e5a74e Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Thu, 27 May 2021 23:25:02 +0800 Subject: [PATCH 17/41] [Refactor] Move KnobGestureRecord to KnobMapping.swift --- Sources/Rings/Knob.swift | 10 ---------- .../Rings/KnobComponents/Mappings/KnobMapping.swift | 12 +++++++++++- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index 19f423f..7234f1e 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -105,16 +105,6 @@ extension CGVector { } } -public struct KnobGestureRecord { - struct Value { - var value: Double = .nan - var angle: Angle - } - var start: Value - var current: Value - var next: Value -} - public struct Knob: View { private var layers: [AnyKnobLayer] = [] diff --git a/Sources/Rings/KnobComponents/Mappings/KnobMapping.swift b/Sources/Rings/KnobComponents/Mappings/KnobMapping.swift index f23fb58..c7e19c4 100644 --- a/Sources/Rings/KnobComponents/Mappings/KnobMapping.swift +++ b/Sources/Rings/KnobComponents/Mappings/KnobMapping.swift @@ -5,7 +5,7 @@ // Created by Chen-Hai Teng on 2021/5/27. // -import Foundation +import SwiftUI enum Default { enum Degree: Double { @@ -18,6 +18,16 @@ enum Default { } } +public struct KnobGestureRecord { + struct Value { + var value: Double = .nan + var angle: Angle + } + var start: Value + var current: Value + var next: Value +} + public protocol KnobMapping : Adjustable { var degreeRange: ClosedRange { get set } func degree(from value: Double) -> Double From 29c80a4df737ff012806753316a1888260e579a3 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Thu, 27 May 2021 23:43:15 +0800 Subject: [PATCH 18/41] [Refactor] Move LinearMapping to LinearMapping.swift --- Sources/Rings/Knob.swift | 33 ------------ .../Mappings/LinearMapping.swift | 52 +++++++++++++++++++ 2 files changed, 52 insertions(+), 33 deletions(-) create mode 100644 Sources/Rings/KnobComponents/Mappings/LinearMapping.swift diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index 7234f1e..fab7014 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -10,52 +10,19 @@ import SwiftUI } } - setProperty { tmp in - } - } -} - -public struct LinearMapping : KnobMapping, Adjustable { - private var minDegree: Double { get { - degreeRange.lowerBound } - } - private var maxDegree: Double { - get { - degreeRange.upperBound } } - public var degreeRange = Default.Degree.Min.rawValue...Default.Degree.Max.rawValue - var minValue: Double = Default.Value.Min.rawValue - var maxValue: Double = Default.Value.Max.rawValue - public func newValue(_ record: KnobGestureRecord) -> Double { - let deltaDegre = (record.current.angle - record.next.angle).degrees - let deltaValue = value(delta: deltaDegre) - var nextValue = record.current.value + deltaValue - if nextValue > maxValue { - nextValue = maxValue } - if nextValue < minValue { - nextValue = minValue } - return nextValue } - public func degree(from value: Double) -> Double { - if(value < minValue) { - return minDegree } - if(value > maxValue) { - return maxDegree } - let ratio = (value - minValue)/(maxValue - minValue) - return ratio * (maxDegree - minDegree) + minDegree } - private func value(delta degree: Double) -> Double { - return degree * (maxValue - minValue) / (maxDegree - minDegree) } } diff --git a/Sources/Rings/KnobComponents/Mappings/LinearMapping.swift b/Sources/Rings/KnobComponents/Mappings/LinearMapping.swift new file mode 100644 index 0000000..87f8709 --- /dev/null +++ b/Sources/Rings/KnobComponents/Mappings/LinearMapping.swift @@ -0,0 +1,52 @@ +// +// LinearMapping.swift +// +// +// Created by Chen-Hai Teng on 2021/5/27. +// + +import SwiftUI + +public struct LinearMapping : KnobMapping, Adjustable { + private var minDegree: Double { + get { + degreeRange.lowerBound + } + } + private var maxDegree: Double { + get { + degreeRange.upperBound + } + } + public var degreeRange = Default.Degree.Min.rawValue...Default.Degree.Max.rawValue + var minValue: Double = Default.Value.Min.rawValue + var maxValue: Double = Default.Value.Max.rawValue + + public func newValue(_ record: KnobGestureRecord) -> Double { + let deltaDegre = (record.current.angle - record.next.angle).degrees + let deltaValue = value(delta: deltaDegre) + var nextValue = record.current.value + deltaValue + if nextValue > maxValue { + nextValue = maxValue + } + if nextValue < minValue { + nextValue = minValue + } + return nextValue + } + + public func degree(from value: Double) -> Double { + if(value < minValue) { + return minDegree + } + if(value > maxValue) { + return maxDegree + } + let ratio = (value - minValue)/(maxValue - minValue) + return ratio * (maxDegree - minDegree) + minDegree + } + + private func value(delta degree: Double) -> Double { + return degree * (maxValue - minValue) / (maxDegree - minDegree) + } +} From 221516fc8d36bce951987a1f3903d212a172b02d Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Thu, 27 May 2021 23:48:57 +0800 Subject: [PATCH 19/41] Implement SegmentMapping to support segmented Knob. --- Sources/Rings/Knob.swift | 19 ---- .../Mappings/SegmentMapping.swift | 89 +++++++++++++++++++ 2 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 Sources/Rings/KnobComponents/Mappings/SegmentMapping.swift diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index fab7014..ceb94ff 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -7,25 +7,6 @@ import SwiftUI - } -} - - get { - } - } - } - - } - } - } - - } - } - } - - } -} - extension CGPoint { static func -(left: CGPoint, right: CGPoint) -> CGVector { // The origin of View's coordinate is on left-top, adjust it to left-bottom to fit mathmatic behaviour. diff --git a/Sources/Rings/KnobComponents/Mappings/SegmentMapping.swift b/Sources/Rings/KnobComponents/Mappings/SegmentMapping.swift new file mode 100644 index 0000000..daccd3b --- /dev/null +++ b/Sources/Rings/KnobComponents/Mappings/SegmentMapping.swift @@ -0,0 +1,89 @@ +// +// SegmentMapping.swift +// +// +// Created by Chen-Hai Teng on 2021/5/27. +// + +import SwiftUI + +struct KnobStop { + var value: Double + var degree: Double + init(_ v:Double, _ d: Double) { + value = v + degree = d + } +} + +public struct SegmentMapping: KnobMapping, Adjustable { + public var degreeRange = Default.Degree.Min.rawValue...Default.Degree.Max.rawValue + private var sortedStops: [KnobStop] = [] + var stops: [KnobStop] { + get { + return sortedStops + } + set { + sortedStops = newValue.sorted { $0.degree < $1.degree } + if let lower = sortedStops.first, let upper = sortedStops.last { + degreeRange = lower.degree...upper.degree + } + } + } + + public func degree(from value: Double) -> Double { + let s = stops.first { stop in + stop.value == value + } + return s?.degree ?? Double.nan + } + + func findValue(by degree: Double) -> Double { + if let estIndex = sortedStops.firstIndex(where: {$0.degree > degree }) { + if(estIndex == 0) { return sortedStops[estIndex].value } + let estMax = sortedStops[estIndex] + let estMin = sortedStops[estIndex - 1] + let mid = estMax.degree + estMin.degree + if degree*2 > mid { + let v = estMax.value + return v + } else { + return estMin.value + } + } else { + return sortedStops.last?.value ?? .nan + } + } + + public func newValue(_ record: KnobGestureRecord) -> Double { + let delta = (record.start.angle - record.next.angle).degrees + if let oldStop = sortedStops.first(where: { $0.value == record.start.value }) { + let newDegrees = oldStop.degree + delta + if(record.current.angle.degrees == newDegrees) { return record.current.value } + // Process new.degrees + return findValue(by: newDegrees) + } + // Cannot find matched degree for v. Find the nearest instead. + var minDiff = Double.nan + var index = sortedStops.count + sortedStops.enumerated().forEach { i, s in + let d = abs(s.value - record.current.value) + if(minDiff == .nan || d < minDiff) { + minDiff = d + index = i + } + } + if index < sortedStops.count { + let oldStop = sortedStops[index] + let newDegrees = oldStop.degree + delta + return findValue(by: newDegrees) + } + return .nan + } + + func stops(_ at:[KnobStop]) -> Self { + setProperty { tmp in + tmp.stops = at + } + } +} From 7140902558e1177493e772ba4971b19d6284cf39 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 15:44:26 +0800 Subject: [PATCH 20/41] [Refactor] publish initializers --- Sources/Rings/Knob.swift | 2 +- Sources/Rings/KnobComponents/Layers/ArcKnobLayer.swift | 2 ++ Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift | 8 ++++++-- Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift | 2 ++ Sources/Rings/KnobComponents/Mappings/LinearMapping.swift | 3 +++ .../Rings/KnobComponents/Mappings/SegmentMapping.swift | 2 ++ 6 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index ceb94ff..5e41134 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -68,7 +68,7 @@ public struct Knob: View { @State var startVector: CGVector = .zero @State private var startValue: Double = .nan - init(_ value: Binding, _ mapping: KnobMapping = LinearMapping()) { + public init(_ value: Binding, _ mapping: KnobMapping = LinearMapping()) { _value = Binding(get: { Double(value.wrappedValue) }, set: { v in diff --git a/Sources/Rings/KnobComponents/Layers/ArcKnobLayer.swift b/Sources/Rings/KnobComponents/Layers/ArcKnobLayer.swift index a0575a0..4440173 100644 --- a/Sources/Rings/KnobComponents/Layers/ArcKnobLayer.swift +++ b/Sources/Rings/KnobComponents/Layers/ArcKnobLayer.swift @@ -42,6 +42,8 @@ public struct ArcKnobLayer : KnobLayer { private var arcWidth: CGFloat = 5.0 private var arcColor: Color = .white + + public init() {} } extension ArcKnobLayer : Adjustable { diff --git a/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift b/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift index c87caaf..542f3dd 100644 --- a/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift +++ b/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift @@ -9,15 +9,19 @@ import SwiftUI struct ImageKnobLayer : KnobLayer { var image: Image - var isFixed: Bool + var isFixed: Bool = false var minDegree: Double = 0.0 var maxDegree: Double = 0.0 - var degree: CGFloat + var degree: CGFloat = 0.0 var view: AnyView { get { AnyView(image.rotationEffect(Angle.degrees(Double(degree)))) } } + + public init(_ image: Image) { + self.image = image + } } diff --git a/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift b/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift index ead173b..67b67e0 100644 --- a/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift +++ b/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift @@ -25,6 +25,8 @@ struct RingKnobLayer : KnobLayer { private var ringColor: Color = .white private var ringGradient: Gradient? = nil private var ringWidth: CGFloat = 2.0 + + public init() {} } extension RingKnobLayer : Adjustable { diff --git a/Sources/Rings/KnobComponents/Mappings/LinearMapping.swift b/Sources/Rings/KnobComponents/Mappings/LinearMapping.swift index 87f8709..027acaa 100644 --- a/Sources/Rings/KnobComponents/Mappings/LinearMapping.swift +++ b/Sources/Rings/KnobComponents/Mappings/LinearMapping.swift @@ -49,4 +49,7 @@ public struct LinearMapping : KnobMapping, Adjustable { private func value(delta degree: Double) -> Double { return degree * (maxValue - minValue) / (maxDegree - minDegree) } + public init() { + + } } diff --git a/Sources/Rings/KnobComponents/Mappings/SegmentMapping.swift b/Sources/Rings/KnobComponents/Mappings/SegmentMapping.swift index daccd3b..610df06 100644 --- a/Sources/Rings/KnobComponents/Mappings/SegmentMapping.swift +++ b/Sources/Rings/KnobComponents/Mappings/SegmentMapping.swift @@ -86,4 +86,6 @@ public struct SegmentMapping: KnobMapping, Adjustable { tmp.stops = at } } + + public init() {} } From 733e09f164114feec5ca9156453e36f2d1026faa Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 15:50:49 +0800 Subject: [PATCH 21/41] [Refactor] publishing adjustable methods. --- Sources/Rings/Knob.swift | 6 +++--- Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift | 6 +++--- Sources/Rings/KnobComponents/Mappings/SegmentMapping.swift | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index 5e41134..0ac630f 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -157,19 +157,19 @@ public struct Knob: View { } extension Knob : Adjustable { - func addLayer(_ layer: L) -> Self where L : KnobLayer { + public func addLayer(_ layer: L) -> Self where L : KnobLayer { setProperty { tmp in tmp.layers.append(AnyKnobLayer(layer)) } } - func mapping(with mapping: T) -> Self { + public func mapping(with mapping: T) -> Self { setProperty { tmp in tmp.mappingObj = mapping } } - func blueprint(_ show: Bool) -> Self { + public func blueprint(_ show: Bool) -> Self { setProperty { tmp in tmp.blueprint = show } diff --git a/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift b/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift index 67b67e0..e179126 100644 --- a/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift +++ b/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift @@ -30,19 +30,19 @@ struct RingKnobLayer : KnobLayer { } extension RingKnobLayer : Adjustable { - func ringColor(_ color: Color) -> Self { + public func ringColor(_ color: Color) -> Self { setProperty { tmp in tmp.ringColor = color tmp.ringGradient = nil } } - func ringColor(_ gradient: Gradient) -> Self { + public func ringColor(_ gradient: Gradient) -> Self { setProperty { tmp in tmp.ringGradient = gradient tmp.ringColor = .clear } } - func ringWidth(_ width: T) -> Self where T:BinaryFloatingPoint { + public func ringWidth(_ width: T) -> Self where T:BinaryFloatingPoint { setProperty { tmp in tmp.ringWidth = CGFloat(width) } diff --git a/Sources/Rings/KnobComponents/Mappings/SegmentMapping.swift b/Sources/Rings/KnobComponents/Mappings/SegmentMapping.swift index 610df06..b362cc7 100644 --- a/Sources/Rings/KnobComponents/Mappings/SegmentMapping.swift +++ b/Sources/Rings/KnobComponents/Mappings/SegmentMapping.swift @@ -7,7 +7,7 @@ import SwiftUI -struct KnobStop { +public struct KnobStop { var value: Double var degree: Double init(_ v:Double, _ d: Double) { @@ -81,7 +81,7 @@ public struct SegmentMapping: KnobMapping, Adjustable { return .nan } - func stops(_ at:[KnobStop]) -> Self { + public func stops(_ at:[KnobStop]) -> Self { setProperty { tmp in tmp.stops = at } From bfe48c3dd8d4b31169f3413ffa0be0f0b9b501cd Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 15:58:56 +0800 Subject: [PATCH 22/41] [Refactor] Publishing ImageKnobLayer and RingKnobLayer --- Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift | 2 +- Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift b/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift index 542f3dd..08cd58b 100644 --- a/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift +++ b/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift @@ -7,7 +7,7 @@ import SwiftUI -struct ImageKnobLayer : KnobLayer { +public struct ImageKnobLayer : KnobLayer { var image: Image var isFixed: Bool = false diff --git a/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift b/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift index e179126..683ae35 100644 --- a/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift +++ b/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift @@ -7,7 +7,7 @@ import SwiftUI -struct RingKnobLayer : KnobLayer { +public struct RingKnobLayer : KnobLayer { var isFixed: Bool = true var minDegree: Double = 0.0 var maxDegree: Double = 0.0 From f173dc37cec012ffc3d93bf3707ec8a7c813de3a Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 16:04:31 +0800 Subject: [PATCH 23/41] Fix visibility issues --- .../Rings/KnobComponents/Layers/ImageKnobLayer.swift | 10 +++++----- .../Rings/KnobComponents/Layers/RingKnobLayer.swift | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift b/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift index 08cd58b..e6cbe18 100644 --- a/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift +++ b/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift @@ -9,13 +9,13 @@ import SwiftUI public struct ImageKnobLayer : KnobLayer { var image: Image - var isFixed: Bool = false + public var isFixed: Bool = false - var minDegree: Double = 0.0 - var maxDegree: Double = 0.0 - var degree: CGFloat = 0.0 + public var minDegree: Double = 0.0 + public var maxDegree: Double = 0.0 + public var degree: CGFloat = 0.0 - var view: AnyView { + public var view: AnyView { get { AnyView(image.rotationEffect(Angle.degrees(Double(degree)))) } diff --git a/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift b/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift index 683ae35..ddb4a81 100644 --- a/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift +++ b/Sources/Rings/KnobComponents/Layers/RingKnobLayer.swift @@ -8,16 +8,17 @@ import SwiftUI public struct RingKnobLayer : KnobLayer { - var isFixed: Bool = true - var minDegree: Double = 0.0 - var maxDegree: Double = 0.0 - var degree: CGFloat = 0.0 + public var isFixed: Bool = true + public var minDegree: Double = 0.0 + public var maxDegree: Double = 0.0 + public var degree: CGFloat = 0.0 - var view: AnyView { + public var view: AnyView { get { AnyView(Circle().stroke(ringAngularGradient(), lineWidth: ringWidth).padding(EdgeInsets(top: ringWidth/2.0, leading: ringWidth/2.0, bottom: ringWidth/2.0, trailing: ringWidth/2.0))) } } + private func ringAngularGradient() -> AngularGradient { let gradient = ringGradient ?? Gradient(colors: [ringColor]) return AngularGradient(gradient: gradient, center: .center, startAngle: Angle.degrees(minDegree), endAngle: Angle.degrees(maxDegree)) From 99f1467aa27846f2d662d971a9e659fddccfce0b Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 16:21:41 +0800 Subject: [PATCH 24/41] Make image resizable --- Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift b/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift index e6cbe18..10eaf0a 100644 --- a/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift +++ b/Sources/Rings/KnobComponents/Layers/ImageKnobLayer.swift @@ -17,7 +17,7 @@ public struct ImageKnobLayer : KnobLayer { public var view: AnyView { get { - AnyView(image.rotationEffect(Angle.degrees(Double(degree)))) + AnyView(image.resizable().rotationEffect(Angle.degrees(Double(degree)))) } } From ba720daeb1857c1c20d00c128cb0523b696345d2 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 19:11:52 +0800 Subject: [PATCH 25/41] Add debug print --- Sources/Rings/Knob.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index 0ac630f..b8488ff 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -124,15 +124,19 @@ public struct Knob: View { let _degree = mappingObj.degree(from: self.value) if _degree >= mappingObj.degreeRange.upperBound { + debugPrint("over upper bound") if valueCurrent.angle.degrees > valueNext.angle.degrees { return } + debugPrint("current: \(valueCurrent.angle), next:\(valueNext.angle)") } if _degree <= mappingObj.degreeRange.lowerBound { + debugPrint("over lower bound") if valueCurrent.angle.degrees < valueNext.angle.degrees { return } + debugPrint("current: \(valueCurrent.angle), next:\(valueNext.angle)") } let record = KnobGestureRecord(start: valueStart, current:valueCurrent, next:valueNext) From acd5cadd25396600a1cf2a302f19b246d8fe3d5c Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 19:20:48 +0800 Subject: [PATCH 26/41] Add checking statement for debug. --- Sources/Rings/Knob.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index b8488ff..2aca02b 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -128,7 +128,8 @@ public struct Knob: View { if valueCurrent.angle.degrees > valueNext.angle.degrees { return } - debugPrint("current: \(valueCurrent.angle), next:\(valueNext.angle)") + let d = CGVector.angularDistance(v1: currentVector, v2: nextVector) + debugPrint("current: \(valueCurrent.angle), next:\(valueNext.angle), d: \(d)") } if _degree <= mappingObj.degreeRange.lowerBound { @@ -136,7 +137,8 @@ public struct Knob: View { if valueCurrent.angle.degrees < valueNext.angle.degrees { return } - debugPrint("current: \(valueCurrent.angle), next:\(valueNext.angle)") + let d = CGVector.angularDistance(v1: currentVector, v2: nextVector) + debugPrint("current: \(valueCurrent.angle), next:\(valueNext.angle), d: \(d)") } let record = KnobGestureRecord(start: valueStart, current:valueCurrent, next:valueNext) From 3074a16d20a47d4fb48a5a2608ef52c41156d11e Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 19:25:31 +0800 Subject: [PATCH 27/41] Change over range conditional statement to solve cross quadrant issue. --- Sources/Rings/Knob.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index 2aca02b..b5d64ce 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -125,19 +125,25 @@ public struct Knob: View { let _degree = mappingObj.degree(from: self.value) if _degree >= mappingObj.degreeRange.upperBound { debugPrint("over upper bound") - if valueCurrent.angle.degrees > valueNext.angle.degrees { +// if valueCurrent.angle.degrees > valueNext.angle.degrees { +// return +// } + let d = CGVector.angularDistance(v1: currentVector, v2: nextVector) + if d.degrees > 0.0 { return } - let d = CGVector.angularDistance(v1: currentVector, v2: nextVector) debugPrint("current: \(valueCurrent.angle), next:\(valueNext.angle), d: \(d)") } if _degree <= mappingObj.degreeRange.lowerBound { debugPrint("over lower bound") - if valueCurrent.angle.degrees < valueNext.angle.degrees { +// if valueCurrent.angle.degrees < valueNext.angle.degrees { +// return +// } + let d = CGVector.angularDistance(v1: currentVector, v2: nextVector) + if d.degrees < 0.0 { return } - let d = CGVector.angularDistance(v1: currentVector, v2: nextVector) debugPrint("current: \(valueCurrent.angle), next:\(valueNext.angle), d: \(d)") } From ebb9b1cd985c4ce258c9ccb7b65cc6af2b95f553 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 19:31:27 +0800 Subject: [PATCH 28/41] modify the method to check boundry --- Sources/Rings/Knob.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index b5d64ce..1b104c6 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -125,11 +125,11 @@ public struct Knob: View { let _degree = mappingObj.degree(from: self.value) if _degree >= mappingObj.degreeRange.upperBound { debugPrint("over upper bound") -// if valueCurrent.angle.degrees > valueNext.angle.degrees { -// return -// } + if valueCurrent.angle.degrees > valueNext.angle.degrees { + return + } let d = CGVector.angularDistance(v1: currentVector, v2: nextVector) - if d.degrees > 0.0 { + if d.radians > 0 { return } debugPrint("current: \(valueCurrent.angle), next:\(valueNext.angle), d: \(d)") @@ -137,11 +137,11 @@ public struct Knob: View { if _degree <= mappingObj.degreeRange.lowerBound { debugPrint("over lower bound") -// if valueCurrent.angle.degrees < valueNext.angle.degrees { -// return -// } + if valueCurrent.angle.degrees < valueNext.angle.degrees { + return + } let d = CGVector.angularDistance(v1: currentVector, v2: nextVector) - if d.degrees < 0.0 { + if d.radians < 0 { return } debugPrint("current: \(valueCurrent.angle), next:\(valueNext.angle), d: \(d)") From 61bede85e357bf5b79901badb82bb8754214b8d7 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 19:37:07 +0800 Subject: [PATCH 29/41] Re-organize debug infro --- Sources/Rings/Knob.swift | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index 1b104c6..04757c0 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -123,28 +123,24 @@ public struct Knob: View { let valueNext = KnobGestureRecord.Value(angle: nextVector.angle(shouldAdjust)) let _degree = mappingObj.degree(from: self.value) + + let delta = CGVector.angularDistance(v1: currentVector, v2: nextVector) + if _degree >= mappingObj.degreeRange.upperBound { - debugPrint("over upper bound") + debugPrint("over upper bound :\(_degree)") + debugPrint("delta distance: \(delta)") + if valueCurrent.angle.degrees > valueNext.angle.degrees { return } - let d = CGVector.angularDistance(v1: currentVector, v2: nextVector) - if d.radians > 0 { - return - } - debugPrint("current: \(valueCurrent.angle), next:\(valueNext.angle), d: \(d)") } if _degree <= mappingObj.degreeRange.lowerBound { - debugPrint("over lower bound") + debugPrint("over lower bound : \(_degree)") + debugPrint("delta distance: \(delta)") if valueCurrent.angle.degrees < valueNext.angle.degrees { return } - let d = CGVector.angularDistance(v1: currentVector, v2: nextVector) - if d.radians < 0 { - return - } - debugPrint("current: \(valueCurrent.angle), next:\(valueNext.angle), d: \(d)") } let record = KnobGestureRecord(start: valueStart, current:valueCurrent, next:valueNext) From 660e739530b70f3ed144c1f027411f4ec83abd31 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 19:43:41 +0800 Subject: [PATCH 30/41] quick determine if cross quadrant 3, 4 --- Sources/Rings/Knob.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index 04757c0..665dda5 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -133,6 +133,9 @@ public struct Knob: View { if valueCurrent.angle.degrees > valueNext.angle.degrees { return } + if currentVector.dx*nextVector.dx < 0 { + return + } } if _degree <= mappingObj.degreeRange.lowerBound { @@ -141,6 +144,9 @@ public struct Knob: View { if valueCurrent.angle.degrees < valueNext.angle.degrees { return } + if currentVector.dx*nextVector.dx < 0 { + return + } } let record = KnobGestureRecord(start: valueStart, current:valueCurrent, next:valueNext) From 93ab32a050ca7b1024972c12528e284b4d8311fe Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 19:50:22 +0800 Subject: [PATCH 31/41] add more information --- Sources/Rings/Knob.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index 665dda5..d146dd1 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -133,6 +133,7 @@ public struct Knob: View { if valueCurrent.angle.degrees > valueNext.angle.degrees { return } + debugPrint("current:\(currentVector), next:\(nextVector)") if currentVector.dx*nextVector.dx < 0 { return } @@ -144,6 +145,7 @@ public struct Knob: View { if valueCurrent.angle.degrees < valueNext.angle.degrees { return } + debugPrint("current:\(currentVector), next:\(nextVector)") if currentVector.dx*nextVector.dx < 0 { return } From d30f66fcec4dcd5387285a7617de54d107057b4f Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 20:05:16 +0800 Subject: [PATCH 32/41] test simplify checking. --- Sources/Rings/Knob.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index d146dd1..fae16c3 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -129,14 +129,16 @@ public struct Knob: View { if _degree >= mappingObj.degreeRange.upperBound { debugPrint("over upper bound :\(_degree)") debugPrint("delta distance: \(delta)") - - if valueCurrent.angle.degrees > valueNext.angle.degrees { - return - } - debugPrint("current:\(currentVector), next:\(nextVector)") - if currentVector.dx*nextVector.dx < 0 { + if nextVector.dx < currentVector.dx { return } +// if valueCurrent.angle.degrees > valueNext.angle.degrees { +// return +// } +// debugPrint("current:\(currentVector), next:\(nextVector)") +// if currentVector.dx*nextVector.dx < 0 { +// return +// } } if _degree <= mappingObj.degreeRange.lowerBound { From b52f84dd1dd74b69abea50c6f71ce99e9c4f0fdf Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 20:10:34 +0800 Subject: [PATCH 33/41] Add log to help trace --- Sources/Rings/Knob.swift | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index fae16c3..255439c 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -127,30 +127,17 @@ public struct Knob: View { let delta = CGVector.angularDistance(v1: currentVector, v2: nextVector) if _degree >= mappingObj.degreeRange.upperBound { - debugPrint("over upper bound :\(_degree)") - debugPrint("delta distance: \(delta)") if nextVector.dx < currentVector.dx { return } -// if valueCurrent.angle.degrees > valueNext.angle.degrees { -// return -// } -// debugPrint("current:\(currentVector), next:\(nextVector)") -// if currentVector.dx*nextVector.dx < 0 { -// return -// } + debugPrint("cannot stop over q <-") } if _degree <= mappingObj.degreeRange.lowerBound { - debugPrint("over lower bound : \(_degree)") - debugPrint("delta distance: \(delta)") - if valueCurrent.angle.degrees < valueNext.angle.degrees { - return - } - debugPrint("current:\(currentVector), next:\(nextVector)") - if currentVector.dx*nextVector.dx < 0 { + if nextVector.dx > currentVector.dx { return } + debugPrint("cannot stop over q ->") } let record = KnobGestureRecord(start: valueStart, current:valueCurrent, next:valueNext) From 7c6805796edf3a0f7ac736bae9f850c28471c040 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 20:59:28 +0800 Subject: [PATCH 34/41] Simplify the behaviour when rotate out of range. --- Sources/Rings/Knob.swift | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index 255439c..6961836 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -124,20 +124,12 @@ public struct Knob: View { let _degree = mappingObj.degree(from: self.value) - let delta = CGVector.angularDistance(v1: currentVector, v2: nextVector) - - if _degree >= mappingObj.degreeRange.upperBound { - if nextVector.dx < currentVector.dx { - return - } - debugPrint("cannot stop over q <-") + if _degree > mappingObj.degreeRange.upperBound { + return } - if _degree <= mappingObj.degreeRange.lowerBound { - if nextVector.dx > currentVector.dx { - return - } - debugPrint("cannot stop over q ->") + if _degree < mappingObj.degreeRange.lowerBound { + return } let record = KnobGestureRecord(start: valueStart, current:valueCurrent, next:valueNext) From 00f036b59f0d500d835cf52bc379c3dda4821028 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Fri, 28 May 2021 22:09:21 +0800 Subject: [PATCH 35/41] Use nextVector instead of _degree --- Sources/Rings/Knob.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/Rings/Knob.swift b/Sources/Rings/Knob.swift index 6961836..68132f5 100644 --- a/Sources/Rings/Knob.swift +++ b/Sources/Rings/Knob.swift @@ -121,16 +121,14 @@ public struct Knob: View { let valueCurrent = KnobGestureRecord.Value(value: self.value, angle: currentVector.angle(shouldAdjust)) let valueNext = KnobGestureRecord.Value(angle: nextVector.angle(shouldAdjust)) - - let _degree = mappingObj.degree(from: self.value) - - if _degree > mappingObj.degreeRange.upperBound { + + if -nextVector.angle(shouldAdjust).degrees > mappingObj.degreeRange.upperBound { return } - - if _degree < mappingObj.degreeRange.lowerBound { + if -nextVector.angle(shouldAdjust).degrees < mappingObj.degreeRange.lowerBound { return } + let record = KnobGestureRecord(start: valueStart, current:valueCurrent, next:valueNext) let newValue = mappingObj.newValue(record) From f2ffa6a70755f97997884eda91adb09a35a8b94c Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Sat, 29 May 2021 17:37:49 +0800 Subject: [PATCH 36/41] Add document --- Sources/Rings/Knob.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Sources/Rings/Knob.md diff --git a/Sources/Rings/Knob.md b/Sources/Rings/Knob.md new file mode 100644 index 0000000..9f66386 --- /dev/null +++ b/Sources/Rings/Knob.md @@ -0,0 +1,35 @@ +## Knob + +### Preview + +### Usage +```swift + // Baisc Knob drawing value along the circumference. + @State knobValue : Double // default range: 0.0...1.0, the range of knob value depends on mapping object. + Knob($knobValue) // Create a Knob with default mapping(LinearMapping) + .addLayer(ArcKnobLayer() // Add ArcKnobLayer to draw circumference. + .arcWidth(10.0) + .arcColor(.blue.opacity(0.7))) + .frame(width: 100.0, height: 100.0) +``` + +```swift + // A Knob drawing value along circular track. + @State knobValue : Double // default range: 0.0...1.0, the range of knob value depends on mapping object. + Knob($knobValue) // Create a Knob with default mapping(LinearMapping) + .addLayer(RingKnobLayer() // Add RingKnobLayer as the track. + .ringWidth(10.0) + .ringColor(.red.opacity(0.5))) + .addLayer(ArcKnobLayer() // Add ArcKnobLayer to draw circumference. + .arcWidth(10.0) + .arcColor(.blue.opacity(0.7))) + .frame(width: 100.0, height: 100.0) +``` + +```swift + // A Knob with rotate image + @State knobValue : Double // default range: 0.0...1.0, the range of knob value depends on mapping object. + Knob($knobValue) + .addLayer(ImageKnobLayer(Image("SimpleKnob"))) + .frame(width: 150, height: 150) +``` From def333efc10dc531ebb9ccf4db401e26dab4cf85 Mon Sep 17 00:00:00 2001 From: Chen Hai Teng Date: Sat, 29 May 2021 17:39:27 +0800 Subject: [PATCH 37/41] update README to add Knob document --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index b85aba2..33b2dc3 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,12 @@ targets: [ ### ![How to use it](Sources/Rings/SphericText.md) +## Knob + +### What it looks like: + +### ![How to use it](Sources/Rings/Knob.md) + --- # License Rings is released under the [MIT License](LICENSE). From 59cb59bd5ee7c807d26d8135f24335bb55a99975 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Sat, 29 May 2021 17:41:25 +0800 Subject: [PATCH 38/41] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 33b2dc3..d620944 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,11 @@ It includes following controls, click to see what it looks like: * **[HandAiguille](#handaiguille)** * **[ArchimedeanSpiralText](#archimedeanspiraltext)** * **[SphericText](#spherictext)** +* **[Knob](#knob)** and following functions are in progress: -* Knob +* Swing --- ## Installation: From 77b639e1126cff13d68801f938347cc4310c5866 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Sat, 29 May 2021 17:47:45 +0800 Subject: [PATCH 39/41] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d620944..ef7a5b1 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ targets: [ ## Knob ### What it looks like: +![Knob Demo](https://user-images.githubusercontent.com/1284944/120065810-e2138900-c0a5-11eb-8324-2fe340bb578f.gif) + ### ![How to use it](Sources/Rings/Knob.md) From da57afe84829beca3f28fdfa745dd3d7e5163315 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Sat, 29 May 2021 17:53:35 +0800 Subject: [PATCH 40/41] Update Knob.md --- Sources/Rings/Knob.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/Rings/Knob.md b/Sources/Rings/Knob.md index 9f66386..621a537 100644 --- a/Sources/Rings/Knob.md +++ b/Sources/Rings/Knob.md @@ -3,6 +3,10 @@ ### Preview ### Usage + +![Knob Arc Demo](https://user-images.githubusercontent.com/1284944/120065862-1d15bc80-c0a6-11eb-876f-687db7b35d00.gif=50x50) + +drawing ```swift // Baisc Knob drawing value along the circumference. @State knobValue : Double // default range: 0.0...1.0, the range of knob value depends on mapping object. From 55f5b2c80beb7ea56efb60ad3724b7280f99a514 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Sat, 29 May 2021 18:01:35 +0800 Subject: [PATCH 41/41] Update Knob.md --- Sources/Rings/Knob.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Sources/Rings/Knob.md b/Sources/Rings/Knob.md index 621a537..89bbd53 100644 --- a/Sources/Rings/Knob.md +++ b/Sources/Rings/Knob.md @@ -2,24 +2,26 @@ ### Preview -### Usage +![Knob Demo](https://user-images.githubusercontent.com/1284944/120065810-e2138900-c0a5-11eb-8324-2fe340bb578f.gif) -![Knob Arc Demo](https://user-images.githubusercontent.com/1284944/120065862-1d15bc80-c0a6-11eb-876f-687db7b35d00.gif=50x50) +### Usage -drawing ```swift // Baisc Knob drawing value along the circumference. - @State knobValue : Double // default range: 0.0...1.0, the range of knob value depends on mapping object. + @State knobValue : Double // default range: 0.0...1.0 Knob($knobValue) // Create a Knob with default mapping(LinearMapping) .addLayer(ArcKnobLayer() // Add ArcKnobLayer to draw circumference. .arcWidth(10.0) .arcColor(.blue.opacity(0.7))) .frame(width: 100.0, height: 100.0) ``` +drawing + +--- ```swift // A Knob drawing value along circular track. - @State knobValue : Double // default range: 0.0...1.0, the range of knob value depends on mapping object. + @State knobValue : Double // default range: 0.0...1.0. Knob($knobValue) // Create a Knob with default mapping(LinearMapping) .addLayer(RingKnobLayer() // Add RingKnobLayer as the track. .ringWidth(10.0) @@ -29,6 +31,9 @@ .arcColor(.blue.opacity(0.7))) .frame(width: 100.0, height: 100.0) ``` +drawing + +--- ```swift // A Knob with rotate image @@ -37,3 +42,10 @@ .addLayer(ImageKnobLayer(Image("SimpleKnob"))) .frame(width: 150, height: 150) ``` +drawing + +And the image sample is following: + +drawing + +