├── Tests ├── ColorKitTests │ ├── ColorKitTests.swift │ └── XCTestManifests.swift └── LinuxMain.swift ├── .gitignore ├── ColorKitMedia ├── ColorKitLogo.pdf └── ExampleCollage.png ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Sources └── ColorKit │ ├── Color Pickers │ ├── PlatformColor.swift │ ├── ColorManager.swift │ ├── GrayScaleSlider.swift │ ├── AlphaSlider.swift │ ├── RGBColorPicker.swift │ ├── HSBColorPicker.swift │ ├── CYMKColorPicker.swift │ ├── ColorPicker.swift │ └── ColorToken.swift │ └── Gradient Picker │ ├── GradientPicker.swift │ ├── GradientData.swift │ ├── RadialGradientPicker.swift │ ├── LinearGradientPicker.swift │ ├── AngularGradientPicker.swift │ └── GradientStyles.swift ├── Package.swift └── README.md /Tests/ColorKitTests/ColorKitTests.swift: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Tests/ColorKitTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /ColorKitMedia/ColorKitLogo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieranb662/SwiftUI-Color-Kit/HEAD/ColorKitMedia/ColorKitLogo.pdf -------------------------------------------------------------------------------- /ColorKitMedia/ExampleCollage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieranb662/SwiftUI-Color-Kit/HEAD/ColorKitMedia/ExampleCollage.png -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import ColorKitTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += ColorKitTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/ColorKit/Color Pickers/PlatformColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlatformColor.swift 3 | // 4 | // 5 | // Created by Kieran Brown on 4/13/20. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | import UIKit 10 | public typealias PlatformColor = UIColor 11 | #else 12 | import AppKit 13 | public typealias PlatformColor = NSColor 14 | #endif 15 | 16 | 17 | extension PlatformColor { 18 | 19 | convenience init(cmyk: (c: CGFloat, m: CGFloat, y: CGFloat, k: CGFloat)) { 20 | let cmyTransform = { x in 21 | return x * (1 - cmyk.k) + cmyk.k 22 | } 23 | let C = cmyTransform(cmyk.c) 24 | let M = cmyTransform(cmyk.m) 25 | let Y = cmyTransform(cmyk.y) 26 | self.init(red: 1-C, green: 1-M, blue: 1-Y, alpha: 1) 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ColorKit", 8 | platforms: [.iOS(.v13), .macOS(.v10_15)], 9 | products: [ 10 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 11 | .library( 12 | name: "ColorKit", 13 | targets: ["ColorKit"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | .package(url: "https://github.com/kieranb662/Sliders.git" , from: "1.0.3"), 19 | .package(url: "https://github.com/kieranb662/Shapes.git", from: "1.0.4"), 20 | .package(url: "https://github.com/kieranb662/CGExtender.git", from: "1.0.3") 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 25 | .target( 26 | name: "ColorKit", 27 | dependencies: ["CGExtender","Shapes","Sliders"]), 28 | .testTarget( 29 | name: "ColorKitTests", 30 | dependencies: ["ColorKit"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /Sources/ColorKit/Color Pickers/ColorManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Kieran Brown on 4/12/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 11 | public class ColorManager: ObservableObject { 12 | @Published public var colors: [UUID: ColorToken] 13 | @Published public var selected: UUID? 14 | @Published public var defaultColor: ColorToken = .init(r: 0.5, g: 0.5, b: 0.5) 15 | 16 | public func add() { 17 | let new: ColorToken = { 18 | switch defaultColor.colorFormulation { 19 | case .rgb: return ColorToken(name: defaultColor.name, colorSpace: defaultColor.rgbColorSpace, r: defaultColor.red, g: defaultColor.green, b: defaultColor.blue, a: defaultColor.alpha) 20 | case .hsb: return ColorToken(name: defaultColor.name, hue: defaultColor.hue, saturation: defaultColor.saturation, brightness: defaultColor.brightness, opacity: defaultColor.alpha) 21 | case .cmyk: return ColorToken(name: defaultColor.name, cyan: defaultColor.cyan, magenta: defaultColor.magenta, yellow: defaultColor.yellow, keyBlack: defaultColor.keyBlack) 22 | case .gray: return ColorToken(name: defaultColor.name, white: defaultColor.white, opacity: defaultColor.alpha) 23 | } 24 | }() 25 | colors[new.id] = new 26 | } 27 | 28 | 29 | public func delete() { 30 | if selected != nil { 31 | let temp = selected 32 | selected = nil 33 | self.colors.removeValue(forKey: temp!) 34 | } 35 | } 36 | 37 | public init(colors: [ColorToken]) { 38 | 39 | self.colors = [:] 40 | colors.forEach { 41 | self.colors[$0.id] = $0 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/ColorKit/Color Pickers/GrayScaleSlider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GrayScaleSlider.swift 3 | // MyExamples 4 | // 5 | // Created by Kieran Brown on 4/8/20. 6 | // Copyright © 2020 BrownandSons. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Shapes 11 | import Sliders 12 | 13 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 14 | public struct GrayScaleSliderStyle: LSliderStyle { 15 | public let color: ColorToken 16 | public let sliderHeight: CGFloat 17 | private var gradient: Gradient { Gradient(colors: [Color(white: 0), Color(white: 1)]) } 18 | 19 | public func makeThumb(configuration: LSliderConfiguration) -> some View { 20 | let strokeColor = Color(white: color.white < 0.6 ? 1 : 1-color.white) 21 | return ZStack { 22 | Pentagon() 23 | .fill(color.color) 24 | Pentagon() 25 | .stroke(strokeColor, style: .init(lineWidth: 3, lineJoin: .round)) 26 | } 27 | .frame(width: sliderHeight/2, height: 0.66*sliderHeight) 28 | .offset(x: 0, y: 0.16*sliderHeight-1.5) 29 | 30 | } 31 | 32 | public func makeTrack(configuration: LSliderConfiguration) -> some View { 33 | let fill = LinearGradient(gradient: self.gradient, startPoint: .leading, endPoint: .trailing) 34 | return ZStack { 35 | RoundedRectangle(cornerRadius: 5) 36 | .fill(fill) 37 | RoundedRectangle(cornerRadius: 5) 38 | .stroke(Color.gray) 39 | } 40 | } 41 | } 42 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 43 | public struct GrayScaleSlider: View { 44 | @Binding public var color: ColorToken 45 | public var sliderHeight: CGFloat = 40 46 | public init(_ color: Binding) { 47 | self._color = color 48 | } 49 | 50 | public init(_ color: Binding, sliderHeight: CGFloat) { 51 | self._color = color 52 | self.sliderHeight = sliderHeight 53 | } 54 | 55 | public var body: some View { 56 | LSlider(Binding(get: { self.color.white}, 57 | set: { self.color = self.color.update(white: $0) })) 58 | .linearSliderStyle(GrayScaleSliderStyle(color: color, sliderHeight: sliderHeight)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/ColorKit/Color Pickers/AlphaSlider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlphaSlider.swift 3 | // MyExamples 4 | // 5 | // Created by Kieran Brown on 4/7/20. 6 | // Copyright © 2020 BrownandSons. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Sliders 11 | 12 | 13 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 14 | public struct AlphaSliderStyle: LSliderStyle { 15 | public var color: ColorToken 16 | public var sliderHeight: CGFloat = 40 17 | private var gradient: Gradient { Gradient(colors: [Color.white.opacity(0), Color.white]) } 18 | 19 | public func makeThumb(configuration: LSliderConfiguration) -> some View { 20 | ZStack { 21 | Circle() 22 | .fill(Color.white) 23 | Circle() 24 | .inset(by: 3) 25 | .fill(color.color) 26 | } 27 | .frame(width: sliderHeight, height: sliderHeight) 28 | } 29 | public var blockHeight: CGFloat = 10 30 | 31 | public func makeTrack(configuration: LSliderConfiguration) -> some View { 32 | GeometryReader { proxy in 33 | ZStack { 34 | VStack(spacing: 0) { 35 | ForEach(0..) { 63 | self._color = color 64 | } 65 | 66 | public init(_ color: Binding, sliderHeight: CGFloat) { 67 | self._color = color 68 | self.sliderHeight = sliderHeight 69 | } 70 | 71 | public var body: some View { 72 | LSlider(Binding(get: { self.color.alpha }, set: { self.color = self.color.update(alpha: $0) })) 73 | .linearSliderStyle(AlphaSliderStyle(color: color, sliderHeight: sliderHeight)) 74 | 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/ColorKit/Gradient Picker/GradientPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientPicker.swift 3 | // MyExamples 4 | // 5 | // Created by Kieran Brown on 4/6/20. 6 | // Copyright © 2020 BrownandSons. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | 12 | 13 | 14 | // MARK: Gradient Manager 15 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 16 | public class GradientManager: ObservableObject { 17 | @Published public var gradient: GradientData 18 | @Published public var hideTools: Bool = false 19 | /// The currently selected stop if one is selected 20 | @Published public var selected: UUID? = nil 21 | 22 | public init(_ gradient: GradientData) { 23 | self.gradient = gradient 24 | } 25 | } 26 | 27 | /// Example of using all three of the gradient pickers to make a single unified picker 28 | /// Does not have a color picker associated with so one must implement this as part of a larger view with a colorpicker 29 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 30 | public struct GradientPicker: View { 31 | @ObservedObject public var manager: GradientManager 32 | public init(_ manager: ObservedObject) { 33 | self._manager = manager 34 | } 35 | 36 | private var toolToggle: some View { 37 | Toggle(isOn: $manager.hideTools, 38 | label: {Text(!self.manager.hideTools ? "Hide Tools" : "Show Tools")}) 39 | } 40 | private var typePicker: some View { 41 | HStack { 42 | Text("Gradient").frame(width: 80) 43 | Picker("Gradient", selection: $manager.gradient.type) { 44 | ForEach(GradientData.GradientType.allCases, id: \.self) { (type) in 45 | Text(type.rawValue).tag(type) 46 | } 47 | }.pickerStyle(SegmentedPickerStyle()) 48 | } 49 | } 50 | private var renderModePicker: some View { 51 | HStack { 52 | Text("Render Mode").frame(width: 80) 53 | Picker("Render Mode", selection: $manager.gradient.renderMode) { 54 | ForEach(GradientData.ColorRenderMode.allCases, id: \.self) { (type) in 55 | Text(type.rawValue).tag(type) 56 | } 57 | }.pickerStyle(SegmentedPickerStyle()) 58 | } 59 | } 60 | private var currentPicker: some View { 61 | Group { 62 | if manager.gradient.type == .linear { 63 | LinearGradientPicker() 64 | } else if manager.gradient.type == .radial { 65 | RadialGradientPicker() 66 | } else { 67 | AngularGradientPicker() 68 | } 69 | }.environmentObject(manager) 70 | } 71 | 72 | public var body: some View { 73 | VStack { 74 | typePicker.padding(.horizontal, 40) 75 | renderModePicker.padding(.horizontal, 40) 76 | toolToggle.padding(.horizontal, 40) 77 | currentPicker 78 | .frame(idealHeight: 400, maxHeight: 500) 79 | .padding(35) 80 | } 81 | } 82 | } 83 | 84 | 85 | -------------------------------------------------------------------------------- /Sources/ColorKit/Color Pickers/RGBColorPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RGBColorPicker.swift 3 | // MyExamples 4 | // 5 | // Created by Kieran Brown on 4/7/20. 6 | // Copyright © 2020 BrownandSons. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Shapes 11 | import Sliders 12 | 13 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 14 | public struct RGBSliderStyle: LSliderStyle { 15 | public enum ColorType: String, CaseIterable { 16 | case red 17 | case green 18 | case blue 19 | } 20 | public var sliderHeight: CGFloat 21 | public var type: ColorType 22 | public var color: ColorToken 23 | // Creates two colors based upon what the color would look like if the value of the slider was dragged all the way left or all the way right 24 | private var colors: [Color] { 25 | switch type { 26 | case .red: 27 | return [Color(self.color.rgbColorSpace.space, red: 0, green: color.green, blue: color.blue), 28 | Color(self.color.rgbColorSpace.space, red: 1, green: color.green, blue: color.blue)] 29 | case .green: 30 | return [Color(self.color.rgbColorSpace.space, red: color.red, green: 0, blue: color.blue), 31 | Color(self.color.rgbColorSpace.space, red: color.red, green: 1, blue: color.blue)] 32 | case .blue: 33 | return [Color(self.color.rgbColorSpace.space, red: color.red, green: color.green, blue: 0), 34 | Color(self.color.rgbColorSpace.space, red: color.red, green: color.green, blue: 1)] 35 | } 36 | } 37 | 38 | public func makeThumb(configuration: LSliderConfiguration) -> some View { 39 | let currentColor: Color = { 40 | switch type { 41 | case .red: 42 | return Color(self.color.rgbColorSpace.space, red: Double(configuration.pctFill), green: 0, blue: 0) 43 | case .green: 44 | return Color(self.color.rgbColorSpace.space, red: 0, green: Double(configuration.pctFill), blue: 0) 45 | case .blue: 46 | return Color(self.color.rgbColorSpace.space, red: 0, green: 0, blue: Double(configuration.pctFill)) 47 | } 48 | }() 49 | 50 | 51 | return ZStack { 52 | Circle() 53 | .fill(Color.white) 54 | .shadow(radius: 2) 55 | Circle() 56 | .fill(currentColor) 57 | .scaleEffect(0.8) 58 | }.frame(width: sliderHeight, height: sliderHeight) 59 | } 60 | 61 | public func makeTrack(configuration: LSliderConfiguration) -> some View { 62 | let style: StrokeStyle = .init(lineWidth: sliderHeight, lineCap: .round) 63 | let gradient = LinearGradient(gradient: Gradient(colors: colors), startPoint: .leading, endPoint: .trailing) 64 | return AdaptiveLine(angle: configuration.angle) 65 | .stroke(gradient, style: style) 66 | .overlay(GeometryReader { proxy in 67 | Capsule() 68 | .stroke(Color.white) 69 | .frame(width: proxy.size.width + self.sliderHeight) 70 | .rotationEffect(configuration.angle) 71 | }) 72 | } 73 | } 74 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 75 | public struct RGBColorPicker: View { 76 | @Binding public var color: ColorToken 77 | public var sliderHeights: CGFloat = 40 78 | 79 | public init(_ color: Binding) { 80 | self._color = color 81 | } 82 | 83 | public init(_ color: Binding, sliderHeights: CGFloat) { 84 | self._color = color 85 | self.sliderHeights = sliderHeights 86 | } 87 | 88 | private func makeSlider( _ color: RGBSliderStyle.ColorType) -> some View { 89 | let value: Binding = { 90 | switch color { 91 | case .red: 92 | return Binding(get: {self.color.red}, 93 | set: {self.color = self.color.update(red: $0)}) 94 | case .blue: 95 | return Binding(get: {self.color.blue}, 96 | set: {self.color = self.color.update(blue: $0)}) 97 | case .green: 98 | return Binding(get: {self.color.green}, 99 | set: {self.color = self.color.update(green: $0)}) 100 | } 101 | }() 102 | 103 | return LSlider(value, range: 0...1, angle: .zero) 104 | .linearSliderStyle(RGBSliderStyle(sliderHeight: sliderHeights, type: color, color: self.color)) 105 | .frame(height: sliderHeights) 106 | } 107 | 108 | public var body: some View { 109 | VStack(spacing: 20){ 110 | makeSlider( .red) 111 | makeSlider(.green) 112 | makeSlider(.blue) 113 | } 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /Sources/ColorKit/Color Pickers/HSBColorPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HSBColorPicker.swift 3 | // MyExamples 4 | // 5 | // Created by Kieran Brown on 4/7/20. 6 | // Copyright © 2020 BrownandSons. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Shapes 11 | import Sliders 12 | 13 | 14 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 15 | public struct HueSliderStyle: LSliderStyle { 16 | public var sliderHeight: CGFloat 17 | private let hueColors = stride(from: 0, to: 1, by: 0.03).map { 18 | Color(hue: $0, saturation: 1, brightness: 1) 19 | } 20 | 21 | public func makeThumb(configuration: LSliderConfiguration) -> some View { 22 | return ZStack { 23 | Circle() 24 | .fill(Color.white) 25 | .shadow(radius: 2) 26 | Circle() 27 | .fill(Color(hue: configuration.pctFill, saturation: 1, brightness: 1)) 28 | .scaleEffect(0.8) 29 | }.frame(width: sliderHeight, height: sliderHeight) 30 | } 31 | 32 | public func makeTrack(configuration: LSliderConfiguration) -> some View { 33 | let style: StrokeStyle = .init(lineWidth: sliderHeight, lineCap: .round) 34 | let gradient = LinearGradient(gradient: Gradient(colors: hueColors), startPoint: .leading, endPoint: .trailing) 35 | return AdaptiveLine(angle: configuration.angle) 36 | .stroke(gradient, style: style) 37 | .overlay(GeometryReader { proxy in 38 | Capsule() 39 | .stroke(Color.white) 40 | .frame(width: proxy.size.width + self.sliderHeight) 41 | .rotationEffect(configuration.angle) 42 | }) 43 | } 44 | } 45 | 46 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 47 | public struct SaturationBrightnessStyle: TrackPadStyle { 48 | public var hue: Double 49 | private var saturationColors: [Color] { 50 | return stride(from: 0, to: 1, by: 0.01).map { 51 | Color(hue: hue, saturation: $0, brightness: 1) 52 | } 53 | } 54 | 55 | public func makeThumb(configuration: TrackPadConfiguration) -> some View { 56 | ZStack { 57 | Circle() 58 | .foregroundColor(configuration.isActive ? .yellow : .white) 59 | Circle() 60 | .fill(Color(hue: self.hue, saturation: Double(configuration.pctX), brightness: Double(configuration.pctY))) 61 | .scaleEffect(0.8) 62 | }.frame(width: 40, height: 40) 63 | } 64 | // FIXME: Come back and draw the 2D gradient with metal when I make a better pipeline 65 | public func makeTrack(configuration: TrackPadConfiguration) -> some View { 66 | let brightnessGradient = LinearGradient(gradient: Gradient(colors: [Color(red: 1, green: 1, blue: 1), Color(red: 0, green: 0, blue: 0)]), startPoint: .bottom, endPoint: .top) 67 | let saturationGradient = LinearGradient(gradient:Gradient(colors: saturationColors), startPoint: .leading, endPoint: .trailing) 68 | return ZStack { 69 | RoundedRectangle(cornerRadius: 5) 70 | .fill(brightnessGradient) 71 | 72 | RoundedRectangle(cornerRadius: 5) 73 | .fill(saturationGradient) 74 | .drawingGroup(opaque: false, colorMode: .extendedLinear) 75 | .blendMode(.plusDarker) 76 | }.overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.white, lineWidth: 2)) 77 | } 78 | } 79 | 80 | 81 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 82 | public struct HSBColorPicker: View { 83 | @Binding public var color: ColorToken 84 | public var sliderHeight: CGFloat = 40 85 | 86 | public init(_ color: Binding) { 87 | self._color = color 88 | } 89 | 90 | public init(_ color: Binding, sliderHeight: CGFloat) { 91 | self._color = color 92 | self.sliderHeight = sliderHeight 93 | } 94 | 95 | public var body: some View { 96 | VStack(spacing: 30) { 97 | TrackPad(value: Binding(get: {CGPoint(x: self.color.saturation, y: self.color.brightness)}, 98 | set: { (new) in 99 | self.color = self.color.update(saturation: Double(new.x)) 100 | self.color = self.color.update(brightness: Double(new.y)) 101 | 102 | }), rangeX: 0.01...1, rangeY: 0.01...1) 103 | .trackPadStyle(SaturationBrightnessStyle(hue: self.color.hue)) 104 | 105 | LSlider(Binding(get: {self.color.hue}, set: {self.color = self.color.update(hue: $0)})) 106 | .linearSliderStyle(HueSliderStyle(sliderHeight: sliderHeight)) 107 | .frame(height: sliderHeight) 108 | .padding(.horizontal, sliderHeight/2) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/ColorKit/Color Pickers/CYMKColorPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CYMKColorPicker.swift 3 | // MyExamples 4 | // 5 | // Created by Kieran Brown on 4/7/20. 6 | // Copyright © 2020 BrownandSons. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Shapes 11 | import Sliders 12 | 13 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 14 | public struct CMYKSliderStyle: LSliderStyle { 15 | public var sliderHeight: CGFloat 16 | public var type: ColorType 17 | public var color: ColorToken 18 | // Creates two colors based upon what the color would look like if the value of the slider was dragged all the way left or all the way right 19 | private var colors: [Color] { 20 | switch type { 21 | 22 | case .cyan: 23 | return [Color(PlatformColor(cmyk: (0,CGFloat(color.magenta), CGFloat(color.yellow), CGFloat(color.keyBlack) ))), Color(PlatformColor(cmyk: (1,CGFloat(color.magenta), CGFloat(color.yellow), CGFloat(color.keyBlack) )))] 24 | case .magenta: 25 | return [Color(PlatformColor(cmyk: (CGFloat(color.cyan),0, CGFloat(color.yellow), CGFloat(color.keyBlack) ))), Color(PlatformColor(cmyk: (CGFloat(color.cyan),1, CGFloat(color.yellow), CGFloat(color.keyBlack) )))] 26 | case .yellow: 27 | return [Color(PlatformColor(cmyk: (CGFloat(color.cyan),CGFloat(color.magenta), 0, CGFloat(color.keyBlack) ))), Color(PlatformColor(cmyk: (CGFloat(color.cyan),CGFloat(color.magenta), 1, CGFloat(color.keyBlack) )))] 28 | case .black: 29 | return [Color(PlatformColor(cmyk: (CGFloat(color.cyan),CGFloat(color.magenta), CGFloat(color.yellow), 0))), Color(PlatformColor(cmyk: (CGFloat(color.cyan),CGFloat(color.magenta), CGFloat(color.yellow), 1)))] 30 | 31 | } 32 | } 33 | public enum ColorType: String, CaseIterable { 34 | case cyan 35 | case magenta 36 | case yellow 37 | case black 38 | } 39 | 40 | public func makeThumb(configuration: LSliderConfiguration) -> some View { 41 | let currentColor: Color = { 42 | switch type { 43 | case .cyan: 44 | return Color(PlatformColor(cmyk: (CGFloat(configuration.pctFill), 0 , 0, 0))) 45 | case .magenta: 46 | return Color(PlatformColor(cmyk: (0,CGFloat(configuration.pctFill), 0, 0))) 47 | case .yellow: 48 | return Color(PlatformColor(cmyk: (0, 0, CGFloat(configuration.pctFill), 0))) 49 | case .black: 50 | return Color(PlatformColor(cmyk: (0, 0, 0, CGFloat(configuration.pctFill)))) 51 | } 52 | }() 53 | 54 | return ZStack { 55 | Circle() 56 | .fill(Color.white) 57 | .shadow(radius: 2) 58 | Circle() 59 | .fill(currentColor) 60 | .scaleEffect(0.8) 61 | }.frame(width: self.sliderHeight, height: self.sliderHeight) 62 | } 63 | 64 | public func makeTrack(configuration: LSliderConfiguration) -> some View { 65 | let style: StrokeStyle = .init(lineWidth: sliderHeight, lineCap: .round) 66 | return AdaptiveLine(angle: configuration.angle) 67 | .stroke(LinearGradient(gradient: Gradient(colors: colors), startPoint: .leading, endPoint: .trailing), style: style) 68 | .overlay(GeometryReader { proxy in 69 | Capsule() 70 | .stroke(Color.white) 71 | .frame(width: proxy.size.width + self.sliderHeight) 72 | .rotationEffect(configuration.angle) 73 | }) 74 | } 75 | 76 | } 77 | 78 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 79 | public struct CMYKColorPicker: View { 80 | @Binding public var color: ColorToken 81 | public var sliderHeights: CGFloat = 40 82 | 83 | public init(_ color: Binding) { 84 | self._color = color 85 | } 86 | 87 | public init(_ color: Binding, sliderHeights: CGFloat) { 88 | self._color = color 89 | self.sliderHeights = sliderHeights 90 | } 91 | 92 | private func makeSlider( _ color: CMYKSliderStyle.ColorType) -> some View { 93 | let value: Binding = { 94 | switch color { 95 | case .cyan: 96 | return Binding(get: {self.color.cyan}, 97 | set: {self.color = self.color.update(cyan: $0)}) 98 | case .magenta: 99 | return Binding(get: {self.color.magenta}, 100 | set: {self.color = self.color.update(magenta: $0)}) 101 | case .yellow: 102 | return Binding(get: {self.color.yellow}, 103 | set: {self.color = self.color.update(yellow: $0)}) 104 | case .black: 105 | return Binding(get: {self.color.keyBlack}, 106 | set: {self.color = self.color.update(keyBlack: $0)}) 107 | } 108 | }() 109 | let style = CMYKSliderStyle(sliderHeight: sliderHeights, type: color, color: self.color) 110 | return LSlider(value) 111 | .linearSliderStyle(style) 112 | .frame(height: sliderHeights) 113 | } 114 | 115 | public var body: some View { 116 | VStack(spacing: 20){ 117 | makeSlider( .cyan) 118 | makeSlider(.magenta) 119 | makeSlider(.yellow) 120 | makeSlider(.black) 121 | } 122 | } 123 | } 124 | 125 | -------------------------------------------------------------------------------- /Sources/ColorKit/Color Pickers/ColorPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorPicker.swift 3 | // MyExamples 4 | // 5 | // Created by Kieran Brown on 4/7/20. 6 | // Copyright © 2020 BrownandSons. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 12 | public struct ColorPickerButton: ButtonStyle { 13 | public func makeBody(configuration: Configuration) -> some View { 14 | configuration.label 15 | .foregroundColor(configuration.isPressed ? .white : .blue) 16 | .frame(width: 20, height: 20) 17 | .padding() 18 | .background( 19 | Group { 20 | if configuration.isPressed { 21 | RoundedRectangle(cornerRadius: 5) 22 | .fill(Color.blue) 23 | } else { 24 | RoundedRectangle(cornerRadius: 5) 25 | .stroke(Color.blue) 26 | } 27 | }) 28 | } 29 | } 30 | 31 | 32 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 33 | public struct ColorPicker: View { 34 | @ObservedObject public var manager: ColorManager 35 | public init(_ manager: ObservedObject) { 36 | self._manager = manager 37 | } 38 | 39 | private var colors: [ColorToken] { 40 | Array(self.manager.colors.values).sorted(by: {$0.dateCreated > $1.dateCreated}) 41 | } 42 | 43 | private var selectedColor: Binding { 44 | Binding(get: { 45 | if self.manager.selected == nil { 46 | return self.manager.defaultColor 47 | } else { 48 | return self.manager.colors[self.manager.selected!]! 49 | } 50 | }) { 51 | if self.manager.selected == nil { 52 | self.manager.defaultColor = $0 53 | } else { 54 | self.manager.colors[self.manager.selected!]! = $0 55 | } 56 | } 57 | } 58 | 59 | private func select(_ id: UUID) { 60 | if self.manager.selected == id { 61 | self.manager.selected = nil 62 | } else { 63 | self.manager.selected = id 64 | } 65 | } 66 | private var pallette: some View { 67 | ScrollView(.horizontal, showsIndicators: true) { 68 | HStack(spacing: 0) { 69 | ForEach(self.colors) { (color) in 70 | Rectangle() 71 | .fill(color.color) 72 | .frame(width: 50, height: 50) 73 | .onTapGesture { 74 | self.select(color.id) 75 | }.border(self.manager.selected == color.id ? Color.blue : Color.clear) 76 | } 77 | } 78 | } 79 | } 80 | private var formulationPicker: some View { 81 | Picker(selection: self.selectedColor.colorFormulation, label: Text("Color Formulation")) { 82 | ForEach(ColorToken.ColorFormulation.allCases) { (formulation) in 83 | Text(formulation.rawValue).tag(formulation) 84 | } 85 | }.pickerStyle(SegmentedPickerStyle()) 86 | } 87 | private var rgbColorSpacePicker: some View { 88 | Picker(selection: self.selectedColor.rgbColorSpace, label: Text("")) { 89 | ForEach(ColorToken.RGBColorSpace.allCases) { space in 90 | Text(space.rawValue).tag(space) 91 | } 92 | }.pickerStyle(SegmentedPickerStyle()) 93 | } 94 | private var rgbPicker: some View { 95 | VStack { 96 | rgbColorSpacePicker 97 | Spacer() 98 | RGBColorPicker(self.selectedColor) 99 | }.padding(.vertical, 10) 100 | } 101 | private var hsbPicker: some View { 102 | HSBColorPicker(self.selectedColor) 103 | } 104 | private var currentColorPicker: some View { 105 | Group { 106 | if self.selectedColor.colorFormulation.wrappedValue == .rgb { 107 | rgbPicker 108 | } else if self.selectedColor.colorFormulation.wrappedValue == .hsb { 109 | hsbPicker 110 | } else if self.selectedColor.colorFormulation.wrappedValue == .cmyk { 111 | CMYKColorPicker(self.selectedColor) 112 | } else if self.selectedColor.colorFormulation.wrappedValue == .gray { 113 | GrayScaleSlider(self.selectedColor) 114 | .frame(height: 40) 115 | } 116 | }.frame(height: 300) 117 | } 118 | private var buttons: some View { 119 | HStack { 120 | Button(action: self.manager.delete, label: { 121 | Image(systemName: "xmark") 122 | .resizable() 123 | .aspectRatio(contentMode: .fit) 124 | 125 | }) 126 | Button(action: self.manager.add, label: { 127 | Image(systemName: "plus") 128 | .resizable() 129 | .aspectRatio(contentMode: .fit) 130 | 131 | }) 132 | }.frame(height: 30) 133 | } 134 | 135 | public var body: some View { 136 | VStack { 137 | RoundedRectangle(cornerRadius: 5) 138 | .fill(self.selectedColor.wrappedValue.color) 139 | pallette 140 | formulationPicker 141 | currentColorPicker 142 | AlphaSlider(self.selectedColor) 143 | .frame(height: 40) 144 | .padding(.bottom, 10) 145 | buttons 146 | }.padding(.horizontal, 40) 147 | } 148 | } 149 | 150 | -------------------------------------------------------------------------------- /Sources/ColorKit/Gradient Picker/GradientData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientData.swift 3 | // 4 | // 5 | // Created by Kieran Brown on 4/13/20. 6 | // 7 | 8 | 9 | import SwiftUI 10 | 11 | 12 | /// A Token representing the composite data from `LinearGradient`, `RadialGradient`, and `AngularGradient` parameters 13 | /// When finished designing the gradient just access the `swiftUIFile` value and copy/paste it into your project. 14 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 15 | public struct GradientData: Identifiable { 16 | 17 | /// Token representing the different types of SwiftUI Gradients 18 | public enum GradientType: String , CaseIterable, Identifiable { 19 | case linear 20 | case radial 21 | case angular 22 | 23 | public var id: String {self.rawValue} 24 | } 25 | 26 | /// Wrapper enum for `ColorRenderingMode` 27 | public enum ColorRenderMode: String, CaseIterable, Identifiable { 28 | case linear 29 | case extendedLinear = "extended" 30 | case nonLinear 31 | 32 | public var id: String { self.rawValue } 33 | 34 | public var renderingMode: ColorRenderingMode { 35 | switch self { 36 | case .linear: return .linear 37 | case .extendedLinear: return .extendedLinear 38 | case .nonLinear: return .linear 39 | } 40 | } 41 | } 42 | public struct Stop: Identifiable { 43 | public var color: ColorToken 44 | public var location: CGFloat 45 | 46 | public var id: UUID { color.id } 47 | 48 | public init(color: ColorToken, location: CGFloat) { 49 | self.color = color 50 | self.location = location 51 | } 52 | 53 | public var gradientStop: Gradient.Stop { 54 | return .init(color: color.color, location: location) 55 | } 56 | } 57 | 58 | /// A name used to uniquely identify this particular gradient data 59 | public var name: String 60 | /// The type of gradient (linear, radial, angular) 61 | public var type: GradientType = .linear 62 | /// The render mode to be used (linear, extendedLinear, nonlinear) 63 | public var renderMode: ColorRenderMode = .linear 64 | 65 | /// Gradient stops using a custom wrapper `UIColor` or `NSColor` wrapper 66 | /// depending on the systems requirements. Allows for easier modification of 67 | /// stop colors. 68 | public var stops: [Stop] 69 | 70 | 71 | // Linear 72 | /// `LinearGradient` Start Location 73 | public var start: UnitPoint = .leading 74 | /// `LinearGradient` End Location 75 | public var end: UnitPoint = .trailing 76 | // Radial 77 | /// `RadialGradient` Start Radius 78 | var startRadius: CGFloat = 0 79 | /// `RadialGradient` End Radius 80 | public var endRadius: CGFloat = 200 81 | /// `Radial Gradient` or `Angular Gradient` center 82 | public var center: UnitPoint = .center 83 | // Angular 84 | /// `AngularGradient` Start Angle 85 | public var startAngle: Double = 0 86 | /// `AngularGradient` End Angle 87 | public var endAngle: Double = 0.5 88 | 89 | public var _stops: [Gradient.Stop] { 90 | self.stops 91 | .map({$0.gradientStop}) 92 | .sorted(by: {$0.location < $1.location}) 93 | } 94 | public var gradient: Gradient { 95 | Gradient(stops: _stops) 96 | } 97 | public var id: String {name} 98 | // MARK: Gradient To File 99 | /// Creates a `Gradient` to be copied and pasted into a SwiftUI project 100 | public var gradientFile: String { 101 | var file: String = "Gradient(stops: [" 102 | for stop in stops.sorted(by: {$0.location < $1.location}) { 103 | file.append(" .init(color: \(stop.color.fileFormat), location: \(String(format: "%.3f", stop.location))),") 104 | } 105 | 106 | return file.dropLast() + "])" 107 | } 108 | /// Creates a string representing the current gradient data to be copy and pasted into a SwiftUI project 109 | public var swiftUIFile: String { 110 | switch type { 111 | case .linear: 112 | return "let <#Name#> = LinearGradient(gradient: \(gradientFile), startPoint: UnitPoint(x: \(String(format: "%.3f", start.x)), y: \(String(format: "%.3f", start.y))), endPoint: UnitPoint(x: \(String(format: "%.3f", end.x)), y: \(String(format: "%.3f", end.y))))" 113 | case .radial: 114 | return "let <#Name#> = RadialGradient(gradient: \(gradientFile), center: UnitPoint(x: \(String(format: "%.3f", center.x)), y: \(String(format: "%.3f", center.y))), startRadius: \(String(format: "%.3f", startRadius)), endRadius: \(String(format: "%.3f", endRadius)))" 115 | case .angular: 116 | return "let <#Name#> = AngularGradient(gradient: \(gradientFile), center: UnitPoint(x: \(String(format: "%.3f", center.x)), y: \(String(format: "%.3f", center.y))), startAngle: Angle(radians: \(String(format: "%.3f", startAngle))), endAngle: Angle(radians: \(String(format: "%.3f", startAngle > endAngle ? endAngle + 2 * .pi : endAngle))))" 117 | } 118 | } 119 | /// A Convienient default value 120 | public static let defaultValue: GradientData = { 121 | GradientData(name: "name", 122 | stops: [.init(color: ColorToken(colorSpace: .sRGB, r: 252/255, g: 70/255, b: 107/255, a: 1), location: 0), 123 | .init(color: ColorToken(colorSpace: .sRGB, r: 63/255, g: 94/255, b: 251/255, a: 1), location: 1)]) 124 | }() 125 | 126 | public init(name: String, stops: [Stop]) { 127 | self.name = name 128 | self.stops = stops 129 | } 130 | 131 | 132 | public init(name: String, stops: [Stop], startPoint: UnitPoint, endPoint: UnitPoint) { 133 | self.name = name 134 | self.stops = stops 135 | self.start = startPoint 136 | self.end = endPoint 137 | self.type = .linear 138 | } 139 | public init(name: String, stops: [Stop], center: UnitPoint, startRadius: CGFloat, endRadius: CGFloat) { 140 | self.name = name 141 | self.stops = stops 142 | self.center = center 143 | self.startRadius = startRadius 144 | self.endRadius = endRadius 145 | self.type = .radial 146 | } 147 | public init(name: String, stops: [Stop], center: UnitPoint, startAngle: Double, endAngle: Double) { 148 | self.name = name 149 | self.stops = stops 150 | self.center = center 151 | self.startAngle = startAngle 152 | self.endAngle = endAngle 153 | self.type = .angular 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Sources/ColorKit/Gradient Picker/RadialGradientPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RadialGradientPicker.swift 3 | // MyExamples 4 | // 5 | // Created by Kieran Brown on 4/5/20. 6 | // Copyright © 2020 BrownandSons. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | 12 | // MARK: Stop 13 | 14 | /// # Radial Gradient Stop 15 | /// 16 | /// Draggable view used to represent a gradient stop within a radial gradient. 17 | /// 18 | /// ## How it works 19 | /// In the body of the view I created an exact copy of the stop but with an opacity of 0 making it invisible. 20 | /// Then I used the `RadialKey` with the `anchorPreference` method to capture the bounds of the stop 21 | /// Finally I used `overlayPreferenceValue` to overlay a visible copy of the stop which used the invisible copie's bounds 22 | /// to restrict the translation of the view from being partaill dragged over either edge of the `RadialGradientPicker`'s gradient bar. 23 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 24 | public struct RadialStop: View { 25 | // Preference key used to grab the size of the stop and then adjust the maximum translation 26 | // so that the stop doesnts get partially dragged over the ends of the gradient bar 27 | private struct RadialKey: PreferenceKey { 28 | static var defaultValue: CGRect { .zero } 29 | static func reduce(value: inout CGRect, nextValue: () -> CGRect) { 30 | value = nextValue() 31 | } 32 | } 33 | @Environment(\.radialGradientPickerStyle) private var style: AnyRadialGradientPickerStyle 34 | @Binding var stop: GradientData.Stop 35 | @Binding var selected: UUID? 36 | var isHidden: Bool 37 | public let id: UUID // Used to identify the stop 38 | 39 | @State private var isActive: Bool = false // Used to keep track of the Stops drag state 40 | private let space: String = "Radial Gradient" // Radial Gradients coordinate space identifier 41 | 42 | private var configuration: GradientStopConfiguration { 43 | return .init(isActive, selected == id, isHidden, stop.color.color, .zero) 44 | } 45 | 46 | func select() { 47 | if selected == id { 48 | self.selected = nil 49 | } else { 50 | self.selected = id 51 | } 52 | } 53 | 54 | public var body: some View { 55 | GeometryReader { proxy in 56 | ZStack { 57 | self.style 58 | .makeStop(configuration: self.configuration).opacity(0) 59 | .anchorPreference(key: RadialKey.self, value: .bounds, transform: { proxy[$0] }) 60 | .overlayPreferenceValue(RadialKey.self, { (rect) in 61 | self.style.makeStop(configuration: self.configuration) 62 | .offset(x: (self.stop.location - 0.5)*(proxy.size.width-rect.width-4), y: 0) 63 | .onTapGesture { self.select() } 64 | .simultaneousGesture(DragGesture(minimumDistance: 10, coordinateSpace: .named(self.space)) 65 | .onChanged({ 66 | self.stop.location = max(min($0.location.x/proxy.size.width, 1),0) 67 | self.isActive = true 68 | }).onEnded({ 69 | self.stop.location = max(min($0.location.x/proxy.size.width, 1),0) 70 | self.isActive = false 71 | })) 72 | }) 73 | 74 | 75 | } 76 | } 77 | } 78 | } 79 | 80 | 81 | 82 | 83 | 84 | 85 | // MARK: Picker 86 | 87 | /// # Radial Gradient Picker 88 | /// 89 | /// 90 | /// A Component view used to create and style a `RadialGradient` to the users liking 91 | /// The sub components that make up the gradient picker are 92 | /// 1. **Gradient**: The Radial Gradient containing view 93 | /// 2. **Center Thumb**: A draggable view representing the center of the gradient 94 | /// 3. **StartHandle**: A draggable circle representing the start radius that grows larger/small as you drag away/closer from the center thumb 95 | /// 4. **EndHandle**: A draggable circle representing the end radius that grows larger/small as you drag away/closer from the center thumb 96 | /// 5. **RadialStop**: A draggable view contained to the gradient bar, represents the unit location of the stop 97 | /// 6. **Gradient Bar**: A slider like container filled with a linear gradient created with the gradient stops. 98 | /// 99 | /// - important: You must create a `GradientManager` `ObservedObject` and then apply it to the `RadialGradientPicker` 100 | /// or the view containing it using the `environmentObject` method 101 | /// 102 | /// 103 | /// ## Styling The Picker 104 | /// In order to style the picker you must create a struct that conforms to the `RadialGradientPickerStyle` protocol. Conformance requires the implementation of 105 | /// 5 separate methods. To make this easier just copy and paste the following style based on the `DefaultRadialGradientPickerStyle`. After creating your custom style 106 | /// apply it by calling the `radialGradientPickerStyle` method on the `RadialGradientPicker` or a view containing it. 107 | /// 108 | /// ``` 109 | /// struct <#My Picker Style#>: RadialGradientPickerStyle { 110 | /// 111 | /// func makeGradient(gradient: RadialGradient) -> some View { 112 | /// RoundedRectangle(cornerRadius: 5) 113 | /// .fill(gradient) 114 | /// .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.white)) 115 | /// } 116 | /// func makeCenter(configuration: GradientCenterConfiguration) -> some View { 117 | /// Circle().fill(configuration.isActive ? Color.yellow : Color.white) 118 | /// .frame(width: 35, height: 35) 119 | /// .opacity(configuration.isHidden ? 0 : 1) 120 | /// .animation(.easeIn) 121 | /// } 122 | /// func makeStartHandle(configuration: GradientHandleConfiguration) -> some View { 123 | /// Circle() 124 | /// .stroke(Color.white.opacity(0.001), style: StrokeStyle(lineWidth: 10)) 125 | /// .overlay(Circle().stroke(Color.black, style: StrokeStyle(lineWidth: 1, dash: [10, 5]))) 126 | /// .opacity(configuration.isHidden ? 0 : 1) 127 | /// .animation(.easeIn) 128 | /// } 129 | /// func makeEndHandle(configuration: GradientHandleConfiguration) -> some View { 130 | /// Circle() 131 | /// .stroke(Color.white.opacity(0.001), style: StrokeStyle(lineWidth: 10)) 132 | /// .overlay(Circle().stroke(Color.white, style: StrokeStyle(lineWidth: 1, dash: [10, 5]))) 133 | /// .opacity(configuration.isHidden ? 0 : 1) 134 | /// .animation(.easeIn) 135 | /// } 136 | /// func makeStop(configuration: GradientStopConfiguration) -> some View { 137 | /// Group { 138 | /// if !configuration.isHidden { 139 | /// RoundedRectangle(cornerRadius: 5) 140 | /// .foregroundColor(configuration.color) 141 | /// .frame(width: 25, height: 45) 142 | /// .overlay(RoundedRectangle(cornerRadius: 5).stroke( configuration.isSelected ? Color.yellow : Color.white )) 143 | /// .shadow(color: configuration.isSelected ? Color.white : Color.black, radius: 3) 144 | /// .transition(AnyTransition.opacity) 145 | /// .animation(Animation.easeOut) 146 | /// } 147 | /// } 148 | /// } 149 | /// func makeBar(configuration: RadialGradientBarConfiguration) -> some View { 150 | /// Group { 151 | /// if !configuration.isHidden { 152 | /// RoundedRectangle(cornerRadius: 5) 153 | /// .fill(LinearGradient(gradient: configuration.gradient, startPoint: .leading, endPoint: .trailing)) 154 | /// .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.white)) 155 | /// .transition(AnyTransition.move(edge: .leading)) 156 | /// .animation(Animation.easeOut) 157 | /// } 158 | /// } 159 | /// } 160 | /// } 161 | /// ``` 162 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 163 | public struct RadialGradientPicker: View { 164 | // MARK: State and Utilities 165 | @Environment(\.radialGradientPickerStyle) private var style: AnyRadialGradientPickerStyle 166 | @EnvironmentObject private var manager: GradientManager 167 | @State private var centerState: CGSize = .zero // Value representing the actual drag transaltion of the center thumb 168 | @State private var startState: Double = 0 // Value on [0,1] representing the current dragging of the start handle 169 | @State private var endState: Double = 0 // Value on [0,1] representing the current dragging of the end handle 170 | private let space: String = "Radial Gradient" // Indentifier used to denote the pickers coordinate space 171 | public init() {} 172 | // The current Unit location of the center thumb 173 | private func currentCenter(_ proxy: GeometryProxy) -> UnitPoint { 174 | let x = manager.gradient.center.x + centerState.width/proxy.size.width 175 | let y = manager.gradient.center.y + centerState.height/proxy.size.height 176 | return UnitPoint(x: x, y: y) 177 | } 178 | // the current position of the center thumv 179 | private func currentCenter(_ proxy: GeometryProxy) -> CGPoint { 180 | let x = manager.gradient.center.x*proxy.size.width 181 | let y = manager.gradient.center.y*proxy.size.height 182 | return CGPoint(x: x+centerState.width, y: y+centerState.height) 183 | } 184 | 185 | // MARK: Views 186 | // Creates the radial gradient 187 | private func makeGradient(_ proxy: GeometryProxy) -> some View { 188 | style.makeGradient(gradient: RadialGradient(gradient: self.manager.gradient.gradient, 189 | center: self.currentCenter(proxy), 190 | startRadius: self.manager.gradient.startRadius + CGFloat(self.startState), 191 | endRadius: self.manager.gradient.endRadius + CGFloat(self.endState))) 192 | .drawingGroup(opaque: false, colorMode: self.manager.gradient.renderMode.renderingMode) 193 | .animation(.linear) 194 | } 195 | // Creates the center thumb representing the center of the gradient 196 | private func makeCenter(_ proxy: GeometryProxy) -> some View { 197 | self.style.makeCenter(configuration: .init(isActive: self.centerState != .zero, isHidden: self.manager.hideTools)) 198 | .position(self.currentCenter(proxy)) 199 | .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .named(self.space)) 200 | .onChanged({self.centerState = $0.translation}) 201 | .onEnded({ 202 | let x = $0.location.x/proxy.size.width 203 | let y = $0.location.y/proxy.size.height 204 | self.manager.gradient.center = UnitPoint(x: x, y: y) 205 | self.centerState = .zero 206 | })) 207 | } 208 | // Creates the draggable Circle representing the endRadius of the gradient 209 | private func makeEndHandle(_ proxy: GeometryProxy) -> some View { 210 | self.style.makeEndHandle(configuration: .init(endState != 0, endState != 0, manager.hideTools, .zero)) 211 | .frame(width: 2*(manager.gradient.endRadius + CGFloat(endState))) 212 | .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .named(space)) 213 | .onChanged({ 214 | self.endState = sqrt(($0.location - self.currentCenter(proxy)).magnitudeSquared) - Double(self.manager.gradient.endRadius) 215 | }).onEnded({ 216 | self.manager.gradient.endRadius = CGFloat(sqrt(($0.location - self.currentCenter(proxy)).magnitudeSquared)) 217 | self.endState = 0 218 | })).animation(.linear) 219 | .position(self.currentCenter(proxy)) 220 | } 221 | // The Gradient filled bar that acts as a slider for the gradient stops 222 | private var gradientBar: some View { 223 | ZStack { 224 | self.style.makeBar(configuration: .init(gradient: self.manager.gradient.gradient, isHidden: self.manager.hideTools)) 225 | ForEach(self.manager.gradient.stops.indices, id: \.self) { (i) in 226 | RadialStop(stop: self.$manager.gradient.stops[i], 227 | selected: self.$manager.selected, 228 | isHidden: self.manager.hideTools, 229 | id: self.manager.gradient.stops[i].id) 230 | } 231 | } 232 | .frame(height: 50) 233 | .coordinateSpace(name: self.space) 234 | } 235 | 236 | public var body: some View { 237 | VStack(spacing: 0) { 238 | ZStack { 239 | GeometryReader { proxy in 240 | ZStack { 241 | self.makeGradient(proxy) 242 | self.makeCenter(proxy) 243 | self.makeEndHandle(proxy) 244 | } 245 | } 246 | } 247 | .aspectRatio(1, contentMode: .fit) 248 | .coordinateSpace(name: self.space) 249 | self.gradientBar 250 | } 251 | } 252 | } 253 | 254 | -------------------------------------------------------------------------------- /Sources/ColorKit/Gradient Picker/LinearGradientPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinearGradientPicker.swift 3 | // MyExamples 4 | // 5 | // Created by Kieran Brown on 4/3/20. 6 | // Copyright © 2020 BrownandSons. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import CGExtender 11 | 12 | 13 | 14 | // MARK: Stop 15 | 16 | /// # Linear Gradient Stop 17 | /// 18 | /// Draggable view used to represent a gradient stop along a `LinearGradient` 19 | /// 20 | /// ## How It Works 21 | /// By calculating the projection of the drag gestures location onto the line segment defined between the start and end values 22 | /// The projected point which lies on the infinited line defined by the angle between the start and end values is then constrained to the line segment using 23 | /// the parametric form of the line. 24 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 25 | public struct LinearStop: View { 26 | @Environment(\.linearGradientPickerStyle) private var style: AnyLinearGradientPickerStyle 27 | @Binding var stop: GradientData.Stop 28 | @Binding var selected: UUID? 29 | var isHidden: Bool 30 | @State private var isActive: Bool = false // Used to keep track of the Stops drag state 31 | private let space: String = "Linear Gradient" // Linear Gradients coordinate space identifier 32 | public let id: UUID // Used to identify the stop 33 | public let start: CGPoint // Current location of the start handle 34 | public let end: CGPoint // Current location of the end handle 35 | // calculates the angle of the line segment defined by the start and end locations and the adds 90 degrees 36 | // so the the handles and stops are parrallel with the gradient 37 | private var angle: Angle { 38 | let diff = end - start 39 | return Angle(radians: diff.x == 0 ? Double.pi/2 : atan(Double(diff.y/diff.x)) + .pi/2) 40 | } 41 | // Uses the parametric form of the line defined between the start and end handles to calculate the location of the stop 42 | private var lerp: CGPoint { 43 | 44 | let x = (1-stop.location)*start.x + stop.location*end.x 45 | let y = (1-stop.location)*start.y + stop.location*end.y 46 | return CGPoint(x: x, y: y) 47 | } 48 | private var configuration: GradientStopConfiguration { 49 | return .init(isActive, 50 | selected == id, 51 | isHidden, 52 | stop.color.color, 53 | angle) 54 | } 55 | func select() { 56 | if selected == id { 57 | self.selected = nil 58 | } else { 59 | self.selected = id 60 | } 61 | } 62 | 63 | public var body: some View { 64 | GeometryReader { proxy in 65 | ZStack { 66 | self.style.makeStop(configuration: self.configuration) 67 | .offset(x: self.lerp.x - proxy.size.width/2, y: self.lerp.y - proxy.size.height/2) 68 | .onTapGesture { self.select() } 69 | .simultaneousGesture(DragGesture(minimumDistance: 10, coordinateSpace: .named(self.space)) 70 | .onChanged({ 71 | self.stop.location = calculateParameter(self.start, self.end, $0.location) 72 | self.isActive = true 73 | }) 74 | .onEnded({ 75 | self.stop.location = calculateParameter(self.start, self.end, $0.location) 76 | self.isActive = false 77 | })) 78 | } 79 | 80 | } 81 | } 82 | } 83 | 84 | 85 | // MARK: Picker 86 | /// # Linear Gradient Picker 87 | /// 88 | /// A Component view used to create and style a `Linear Gradient` to the users liking 89 | /// The sub components that make up the gradient picker are 90 | /// 1. Gradient: The Linear gradient containing view 91 | /// 2 Start Handle: A draggable view representing the start location of the gradient 92 | /// 3. End Handle: A draggable view representing the end location of the gradient 93 | /// 4. LinearStop: A draggable view representing a gradient stop that is constrained to be located within the start and and handles locations 94 | /// 95 | /// - important: You must create a `GradientManager` `ObservedObject` and then apply it to the `LinearGradientPicker` 96 | /// or the view containing it using the `environmentObject` method 97 | /// 98 | /// ## Styling The Picker 99 | /// In order to style the picker you must create a struct that conforms to the `LinearGradientPickerStyle` protocol. Conformance requires the implementation of 100 | /// 3 separate methods. To make this easier just copy and paste the following style based on the `DefaultLinearGradientPickerStyle`. After creating your custom style 101 | /// apply it by calling the `linearGradientPickerStyle` method on the `LinearGradientPicker` or a view containing it. 102 | /// 103 | /// ``` 104 | /// struct <#My Picker Style#>: LinearGradientPickerStyle { 105 | /// 106 | /// func makeGradient(gradient: LinearGradient) -> some View { 107 | /// RoundedRectangle(cornerRadius: 5) 108 | /// .fill(gradient) 109 | /// .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.white)) 110 | /// } 111 | /// func makeStartHandle(configuration: GradientHandleConfiguration) -> some View { 112 | /// Capsule() 113 | /// .foregroundColor(Color.white) 114 | /// .frame(width: 25, height: 75) 115 | /// .rotationEffect(configuration.angle + Angle(degrees: 90)) 116 | /// .animation(.none) 117 | /// .shadow(radius: 3) 118 | /// .opacity(configuration.isHidden ? 0 : 1) 119 | /// } 120 | /// func makeEndHandle(configuration: GradientHandleConfiguration) -> some View { 121 | /// Capsule() 122 | /// .foregroundColor(Color.white) 123 | /// .frame(width: 25, height: 75) 124 | /// .rotationEffect(configuration.angle + Angle(degrees: 90)) 125 | /// .animation(.none) 126 | /// .shadow(radius: 3) 127 | /// .opacity(configuration.isHidden ? 0 : 1) 128 | /// } 129 | /// func makeStop(configuration: GradientStopConfiguration) -> some View { 130 | /// Capsule() 131 | /// .foregroundColor(configuration.color) 132 | /// .frame(width: 20, height: 55) 133 | /// .overlay(Capsule().stroke( configuration.isSelected ? Color.yellow : Color.white )) 134 | /// .rotationEffect(configuration.angle + Angle(degrees: 90)) 135 | /// .animation(.none) 136 | /// .shadow(color: configuration.isSelected ? Color.white : Color.black, radius: 3) 137 | /// .opacity(configuration.isHidden ? 0 : 1) 138 | /// 139 | /// } 140 | /// } 141 | /// 142 | /// ``` 143 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 144 | public struct LinearGradientPicker: View { 145 | @Environment(\.linearGradientPickerStyle) private var style: AnyLinearGradientPickerStyle 146 | @EnvironmentObject private var manager: GradientManager 147 | private let space: String = "Linear Gradient" 148 | @GestureState private var startState: DragState = .inactive // Gesture state for the start point thumb 149 | @GestureState private var endState: DragState = .inactive // Gesture state for the end point thumb 150 | public init() {} 151 | enum DragState { 152 | case inactive 153 | case pressing 154 | case dragging(translation: CGSize) 155 | 156 | var translation: CGSize { 157 | switch self { 158 | case .inactive, .pressing: 159 | return .zero 160 | case .dragging(let translation): 161 | return translation 162 | } 163 | } 164 | 165 | var isActive: Bool { 166 | switch self { 167 | case .inactive: 168 | return false 169 | case .pressing, .dragging: 170 | return true 171 | } 172 | } 173 | 174 | var isDragging: Bool { 175 | switch self { 176 | case .inactive, .pressing: 177 | return false 178 | case .dragging: 179 | return true 180 | } 181 | } 182 | } 183 | 184 | // MARK: Convenience Values 185 | 186 | /// The starts current location in unit point form 187 | private func currentUnitStart(_ proxy: GeometryProxy) -> UnitPoint { 188 | if proxy.size.width == 0 || proxy.size.height == 0 { return UnitPoint.zero } 189 | return UnitPoint(x: self.manager.gradient.start.x + self.startState.translation.width/proxy.size.width, 190 | y: self.manager.gradient.start.y + self.startState.translation.height/proxy.size.height) 191 | } 192 | /// The ends current location in unit point form 193 | private func currentUnitEnd(_ proxy: GeometryProxy) -> UnitPoint { 194 | if proxy.size.width == 0 || proxy.size.height == 0 { return UnitPoint.zero} 195 | return UnitPoint(x: self.manager.gradient.end.x + self.endState.translation.width/proxy.size.width, 196 | y: self.manager.gradient.end.y + self.endState.translation.height/proxy.size.height) 197 | } 198 | /// The starts current location 199 | private func currentStartPoint(_ proxy: GeometryProxy) -> CGPoint { 200 | if proxy.size.width == 0 || proxy.size.height == 0 { return .zero } 201 | return CGPoint(x: self.manager.gradient.start.x*proxy.size.width + self.startState.translation.width, 202 | y: self.manager.gradient.start.y*proxy.size.height + self.startState.translation.height) 203 | } 204 | /// The ends current location 205 | private func currentEndPoint(_ proxy: GeometryProxy) -> CGPoint { 206 | if proxy.size.width == 0 || proxy.size.height == 0 { return .zero } 207 | return CGPoint(x: self.manager.gradient.end.x*proxy.size.width + self.endState.translation.width, 208 | y: self.manager.gradient.end.y*proxy.size.height + self.endState.translation.height) 209 | } 210 | /// Here the angle is calculated using the actual sizes of the Rectangle rather than the UnitPoint values 211 | /// This is because UnitPoints represent perfect squares with a side length of 1, therefore any angle calculated 212 | /// would be for a square region rather than a rectangular 213 | private func angle(_ proxy: GeometryProxy) -> Angle { 214 | let diff = currentEndPoint(proxy)-currentStartPoint(proxy) 215 | return Angle(radians: diff.x == 0 ? Double.pi/2 : atan(Double(diff.y/diff.x)) + .pi/2) 216 | } 217 | 218 | // MARK: Views 219 | /// Makes The Linear Gradient 220 | private func makeGradient(_ proxy: GeometryProxy) -> some View { 221 | style.makeGradient(gradient: LinearGradient(gradient: self.manager.gradient.gradient, 222 | startPoint: self.currentUnitStart(proxy), 223 | endPoint: self.currentUnitEnd(proxy))) 224 | 225 | .drawingGroup(opaque: false, colorMode: self.manager.gradient.renderMode.renderingMode) 226 | .animation(.interactiveSpring()) 227 | } 228 | /// Creates a the views to be used as either the startHandle or endHandle 229 | private func makeHandle(_ proxy: GeometryProxy, _ point: Binding, _ state: GestureState) -> some View { 230 | let offsetX = point.x.wrappedValue*proxy.size.width + state.wrappedValue.translation.width - proxy.size.width/2 231 | let offsetY = point.y.wrappedValue*proxy.size.height + state.wrappedValue.translation.height - proxy.size.height/2 232 | 233 | let longPressDrag = LongPressGesture(minimumDuration: 0.05) 234 | .sequenced(before: DragGesture()) 235 | .updating(state) { value, state, transaction in 236 | switch value { 237 | case .first(true): // Long press begins. 238 | state = .pressing 239 | case .second(true, let drag): // Long press confirmed, dragging may begin. 240 | state = .dragging(translation: drag?.translation ?? .zero) 241 | default: // Dragging ended or the long press cancelled. 242 | state = .inactive 243 | } 244 | } 245 | .onEnded { value in 246 | guard case .second(true, let drag?) = value else { return } 247 | point.wrappedValue = UnitPoint(x: drag.translation.width/proxy.size.width + point.wrappedValue.x, 248 | y: drag.translation.height/proxy.size.height + point.wrappedValue.y) 249 | } 250 | let configuration: GradientHandleConfiguration = .init(state.wrappedValue.isActive, 251 | state.wrappedValue.isDragging, 252 | manager.hideTools, 253 | angle(proxy)) 254 | 255 | return style.makeStartHandle(configuration: configuration) 256 | .offset(x: offsetX, y: offsetY) 257 | .gesture(longPressDrag) 258 | 259 | } 260 | private func makeStops(_ proxy: GeometryProxy) -> some View { 261 | ForEach(self.manager.gradient.stops.indices, id: \.self) { (i) in 262 | LinearStop(stop: self.$manager.gradient.stops[i], 263 | selected: self.$manager.selected, 264 | isHidden: self.manager.hideTools, 265 | id: self.manager.gradient.stops[i].id, 266 | start: self.currentStartPoint(proxy), 267 | end: self.currentEndPoint(proxy)) 268 | } 269 | } 270 | 271 | public var body: some View { 272 | GeometryReader { proxy in 273 | ZStack { 274 | self.makeGradient(proxy) 275 | self.makeHandle(proxy, self.$manager.gradient.start, self.$startState) // Start Handle 276 | self.makeHandle(proxy, self.$manager.gradient.end, self.$endState) // End Handle 277 | self.makeStops(proxy) 278 | 279 | }.coordinateSpace(name: self.space) 280 | } 281 | } 282 | } 283 | 284 | -------------------------------------------------------------------------------- /Sources/ColorKit/Gradient Picker/AngularGradientPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AngularGradientPicker.swift 3 | // MyExamples 4 | // 5 | // Created by Kieran Brown on 4/5/20. 6 | // Copyright © 2020 BrownandSons. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import CGExtender 11 | 12 | 13 | // MARK: Stop 14 | /// # Angular Gradient Stop 15 | /// 16 | /// Draggable view used to represent a gradient stop along a `AngularGradient` 17 | /// 18 | /// ## How It Works 19 | /// By calculating the direction between the stops drag location and the centers thumb location. The angle is then converted to value between [0,1] 20 | /// Then the angle is constrained to between the start and end handles current angles. 21 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 22 | public struct AngularStop: View { 23 | @Environment(\.angularGradientPickerStyle) private var style: AnyAngularGradientPickerStyle 24 | // MARK: Input Values 25 | @Binding var stop: GradientData.Stop 26 | @Binding var selected: UUID? 27 | var isHidden: Bool 28 | public let id: UUID 29 | public let start: Double 30 | public let end: Double 31 | public let center: CGPoint 32 | 33 | @State private var isActive: Bool = false 34 | private let space: String = "Angular Gradient" 35 | 36 | private var configuration: GradientStopConfiguration { 37 | let angle = Double(stop.location)*(start > end ? end+1-start: end-start) + start 38 | return .init(isActive, selected == id, isHidden, stop.color.color, Angle(degrees: angle*360)) 39 | } 40 | 41 | // MARK: Stop Utilities 42 | 43 | // The curent offset of the gradient stop from the center thumb 44 | private func offset(_ proxy: GeometryProxy) -> CGSize { 45 | let angle = Double(stop.location)*(start > end ? end+1-start: end-start) + start 46 | let r = Double(proxy.size.width > proxy.size.height ? proxy.size.height/2 : proxy.size.width/2) - 20 47 | let x = r*cos((angle) * 2 * .pi) 48 | let y = r*sin((angle) * 2 * .pi) 49 | 50 | return CGSize(width: x, height: y) 51 | } 52 | // All I have to say about this abomination is that "If it fits it ships" 53 | // The main issue is that my reference angle does not behave the same as the 54 | // angular gradients does, so in order to have expected behavior, this is how it is done 55 | private func calculateStopLocation(_ location: CGPoint) { 56 | var direction = calculateDirection(self.center, location) 57 | if self.start > self.end { 58 | if direction > self.start && direction < 1 { 59 | direction = (direction - self.start)/(self.end+1 - self.start) 60 | self.stop.location = CGFloat(direction) 61 | } else { 62 | direction = max(min(direction + 1 , self.end + 1), self.start) 63 | self.stop.location = abs(CGFloat((direction-self.start)/((self.end+1)-self.start) )) 64 | } 65 | } else { 66 | direction = max(min(direction, self.end), self.start) 67 | self.stop.location = CGFloat((direction-self.start)/(self.end-self.start) ) 68 | } 69 | } 70 | func select() { 71 | if selected == id { 72 | self.selected = nil 73 | } else { 74 | self.selected = id 75 | } 76 | } 77 | // MARK: Stop Body 78 | public var body: some View { 79 | GeometryReader { proxy in 80 | ZStack { 81 | self.style.makeStop(configuration: self.configuration) 82 | .offset(self.offset(proxy)) 83 | .onTapGesture { self.select()} 84 | .simultaneousGesture(DragGesture(minimumDistance: 5, coordinateSpace: .named(self.space)) 85 | .onChanged({ 86 | self.calculateStopLocation($0.location) 87 | self.isActive = true 88 | }).onEnded({ 89 | self.calculateStopLocation($0.location) 90 | self.isActive = false 91 | })) 92 | } 93 | } 94 | } 95 | } 96 | 97 | 98 | // MARK: Picker 99 | 100 | /// # Angular Gradient Picker 101 | /// 102 | /// A Component view used to create and style an `AngularGradient` to the users liking 103 | /// The sub components that make up the gradient picker are 104 | /// 1. **Gradient**: The Angular Gradient containing view 105 | /// 2. **Center Thumb**: A draggable view representing the location of the gradients center 106 | /// 3. **Start Handle**: A draggable view representing the start location of the gradient 107 | /// 4. **End Handle**: A draggable view representing the end location of the gradient 108 | /// 5. **AngularStop**: A draggable view representing a gradient stop that is constrained to be between the current stop and end angles locations 109 | /// 110 | /// - important: You must create a `GradientManager` `ObservedObject` and then apply it to the `AngularGradientPicker` 111 | /// or the view containing it using the `environmentObject` method 112 | /// 113 | /// ## Styling The Picker 114 | /// 115 | /// In order to style the picker you must create a struct that conforms to the `AngularGradientPickerStyle` protocol. Conformance requires the implementation of 116 | /// 4 separate methods. To make this easier just copy and paste the following style based on the `DefaultAngularGradientPickerStyle`. After creating your custom style 117 | /// apply it by calling the `angularGradientPickerStyle` method on the `AngularGradientPicker` or a view containing it. 118 | /// 119 | /// ``` 120 | /// struct <#My Picker Style#>: AngularGradientPickerStyle { 121 | /// 122 | /// func makeGradient(gradient: AngularGradient) -> some View { 123 | /// RoundedRectangle(cornerRadius: 5) 124 | /// .fill(gradient) 125 | /// .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.white)) 126 | /// } 127 | /// func makeCenter(configuration: GradientCenterConfiguration) -> some View { 128 | /// Circle().fill(configuration.isActive ? Color.yellow : Color.white) 129 | /// .frame(width: 35, height: 35) 130 | /// .opacity(configuration.isHidden ? 0 : 1) 131 | /// } 132 | /// func makeStartHandle(configuration: GradientHandleConfiguration) -> some View { 133 | /// Capsule() 134 | /// .foregroundColor(Color.white) 135 | /// .frame(width: 30, height: 75) 136 | /// .rotationEffect(configuration.angle) 137 | /// .animation(.none) 138 | /// .shadow(radius: 3) 139 | /// .opacity(configuration.isHidden ? 0 : 1) 140 | /// } 141 | /// func makeEndHandle(configuration: GradientHandleConfiguration) -> some View { 142 | /// Capsule() 143 | /// .foregroundColor(Color.white) 144 | /// .frame(width: 30, height: 75) 145 | /// .rotationEffect(configuration.angle) 146 | /// .animation(.none) 147 | /// .shadow(radius: 3) 148 | /// .opacity(configuration.isHidden ? 0 : 1) 149 | /// } 150 | /// func makeStop(configuration: GradientStopConfiguration) -> some View { 151 | /// Group { 152 | /// if !configuration.isHidden { 153 | /// Circle() 154 | /// .foregroundColor(configuration.color) 155 | /// .frame(width: 25, height: 45) 156 | /// .overlay(Circle().stroke( configuration.isSelected ? Color.yellow : Color.white )) 157 | /// .shadow(color: configuration.isSelected ? Color.white : Color.black, radius: 3) 158 | /// .transition(AnyTransition.opacity) 159 | /// .animation(Animation.easeOut) 160 | /// } 161 | /// } 162 | /// } 163 | /// } 164 | /// ``` 165 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 166 | public struct AngularGradientPicker: View { 167 | // MARK: State and Support Values 168 | @Environment(\.angularGradientPickerStyle) private var style: AnyAngularGradientPickerStyle 169 | @EnvironmentObject private var manager: GradientManager 170 | private let space: String = "Angular Gradient" // Used to name the coordinate space of the picker 171 | @State private var centerState: CGSize = .zero // Value representing the actual drag transaltion of the center thumb 172 | @State private var startState: Double = 0 // Value on [0,1] representing the current dragging of the start handle 173 | @State private var endState: Double = 0 // Value on [0,1] representing the current dragging of the end handle 174 | public init() {} 175 | // Convience value that calculates and adjusts the current start and end states such that the end value is always greater than the start 176 | private var currentStates: (start: Double, end: Double) { 177 | let start = startState + self.manager.gradient.startAngle 178 | let end = endState + manager.gradient.endAngle 179 | if start > end { 180 | return (start , end + 1) 181 | } else { 182 | return (start, end) 183 | } 184 | } 185 | // The current Unit location of the center thumb 186 | private func currentCenter(_ proxy: GeometryProxy) -> UnitPoint { 187 | let x = manager.gradient.center.x + centerState.width/proxy.size.width 188 | let y = manager.gradient.center.y + centerState.height/proxy.size.height 189 | return UnitPoint(x: x, y: y) 190 | } 191 | // the current position of the center thumv 192 | private func currentCenter(_ proxy: GeometryProxy) -> CGPoint { 193 | let x = manager.gradient.center.x*proxy.size.width 194 | let y = manager.gradient.center.y*proxy.size.height 195 | return CGPoint(x: x+centerState.width, y: y+centerState.height) 196 | } 197 | // The Start handles current offset from the center 198 | private func startOffset(_ proxy: GeometryProxy) -> CGSize { 199 | let angle = (manager.gradient.startAngle + startState)*2 * .pi 200 | let r = Double(proxy.size.width > proxy.size.height ? proxy.size.height/2 : proxy.size.width/2) - 20 201 | let x = r*cos(angle) 202 | let y = r*sin(angle) 203 | return CGSize(width: x, height: y) 204 | } 205 | // The end handles current offset from the center 206 | private func endOffset(_ proxy: GeometryProxy) -> CGSize { 207 | let angle = (manager.gradient.endAngle + endState)*2 * .pi 208 | let r = Double(proxy.size.width > proxy.size.height ? proxy.size.height/2 : proxy.size.width/2) - 20 209 | let x = r*cos(angle) 210 | let y = r*sin(angle) 211 | return CGSize(width: x, height: y) 212 | } 213 | 214 | // MARK: Views 215 | // Creates the Angular Gradient 216 | private func gradient(_ proxy: GeometryProxy) -> some View { 217 | style.makeGradient(gradient: AngularGradient(gradient: self.manager.gradient.gradient, 218 | center: self.currentCenter(proxy), 219 | startAngle: Angle(radians: self.currentStates.start*2 * .pi), 220 | endAngle: Angle(radians: self.currentStates.end*2 * .pi))) 221 | .drawingGroup(opaque: false, colorMode: self.manager.gradient.renderMode.renderingMode) 222 | .animation(.linear) 223 | } 224 | // Created the center thumb 225 | private func center(_ proxy: GeometryProxy) -> some View { 226 | self.style.makeCenter(configuration: .init(isActive: self.centerState != .zero, isHidden: self.manager.hideTools)) 227 | .position(self.currentCenter(proxy)) 228 | .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .named(self.space)).onChanged({self.centerState = $0.translation}) 229 | .onEnded({ 230 | let x = $0.location.x/proxy.size.width 231 | let y = $0.location.y/proxy.size.height 232 | 233 | self.manager.gradient.center = UnitPoint(x: x, y: y) 234 | self.centerState = .zero 235 | })) 236 | } 237 | private var startConfiguration: GradientHandleConfiguration { 238 | .init(startState != 0, startState != 0, manager.hideTools, Angle(radians: (currentStates.start) * 2 * .pi + .pi/2 )) 239 | } 240 | // Creates the startHandle 241 | private func startHandle(_ proxy: GeometryProxy) -> some View { 242 | self.style.makeStartHandle(configuration: startConfiguration) 243 | .offset(startOffset(proxy)) 244 | .position(currentCenter(proxy)) 245 | .gesture(DragGesture(minimumDistance: 10, coordinateSpace: .named(space)) 246 | .onChanged({ 247 | let direction = calculateDirection(self.currentCenter(proxy), $0.location) 248 | self.startState = direction - self.manager.gradient.startAngle 249 | }) 250 | .onEnded({ 251 | self.manager.gradient.startAngle = calculateDirection(self.currentCenter(proxy), $0.location) 252 | self.startState = 0 253 | })) 254 | .animation(.none) 255 | } 256 | private var endConfiguration: GradientHandleConfiguration { 257 | .init(endState != 0, endState != 0, manager.hideTools, Angle(radians: (currentStates.end) * 2 * .pi + .pi/2 )) 258 | } 259 | // Creates the end handle 260 | private func endHandle(_ proxy: GeometryProxy) -> some View { 261 | self.style.makeEndHandle(configuration: endConfiguration) 262 | .offset(self.endOffset(proxy)) 263 | .position(self.currentCenter(proxy)) 264 | .gesture(DragGesture(minimumDistance: 10, coordinateSpace: .named(space)) 265 | .onChanged({ 266 | let direction = calculateDirection(self.currentCenter(proxy), $0.location) 267 | self.endState = direction - self.manager.gradient.endAngle 268 | }) 269 | .onEnded({ 270 | self.manager.gradient.endAngle = calculateDirection(self.currentCenter(proxy), $0.location) 271 | self.endState = 0 272 | })) 273 | .animation(.none) 274 | } 275 | // Creates all stop thumbs 276 | private func stops(_ proxy: GeometryProxy) -> some View { 277 | ForEach(self.manager.gradient.stops.indices, id: \.self) { (i) in 278 | AngularStop(stop: self.$manager.gradient.stops[i], 279 | selected: self.$manager.selected, 280 | isHidden: self.manager.hideTools, 281 | id: self.manager.gradient.stops[i].id, 282 | start: self.currentStates.start, 283 | end: self.endState + self.manager.gradient.endAngle, 284 | center: self.currentCenter(proxy)) 285 | 286 | }.position(self.currentCenter(proxy)) 287 | } 288 | 289 | public var body: some View { 290 | GeometryReader { proxy in 291 | ZStack { 292 | self.gradient(proxy) 293 | self.center(proxy) 294 | self.startHandle(proxy) 295 | self.endHandle(proxy) 296 | self.stops(proxy) 297 | 298 | }.coordinateSpace(name: self.space) 299 | 300 | } 301 | } 302 | } 303 | 304 | -------------------------------------------------------------------------------- /Sources/ColorKit/Color Pickers/ColorToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorToken.swift 3 | // MyExamples 4 | // 5 | // Created by Kieran Brown on 4/8/20. 6 | // Copyright © 2020 BrownandSons. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | 12 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 13 | public struct ColorToken: Identifiable { 14 | public enum ColorFormulation: String, CaseIterable, Identifiable { 15 | case rgb 16 | case hsb 17 | case cmyk 18 | case gray 19 | 20 | public var id: String {self.rawValue} 21 | } 22 | public enum RGBColorSpace: String, CaseIterable, Identifiable { 23 | case displayP3 24 | case sRGB 25 | case sRGBLinear 26 | 27 | public var id: String {self.rawValue} 28 | 29 | public var space: Color.RGBColorSpace { 30 | switch self { 31 | case .displayP3: return .displayP3 32 | case .sRGB: return .sRGB 33 | case .sRGBLinear: return .sRGBLinear 34 | } 35 | } 36 | } 37 | 38 | public var colorFormulation: ColorFormulation 39 | public var rgbColorSpace: RGBColorSpace = .sRGB 40 | 41 | public var name: String = "New" 42 | public let id: UUID 43 | public let dateCreated: Date 44 | 45 | public var white: Double = 0.5 46 | 47 | public var red: Double = 0.5 48 | public var green: Double = 0.5 49 | public var blue: Double = 0.5 50 | 51 | public var hue: Double = 0.5 52 | public var saturation: Double = 0.5 53 | public var brightness: Double = 0.5 54 | 55 | public var cyan: Double = 0.5 56 | public var magenta: Double = 0.5 57 | public var yellow: Double = 0.5 58 | public var keyBlack: Double = 0.5 59 | 60 | public var alpha: Double = 1 61 | 62 | public var hex: String { self.color.description } 63 | 64 | public var color: Color { 65 | switch colorFormulation { 66 | case .rgb: 67 | return Color(self.rgbColorSpace.space, red: self.red, green: self.green, blue: self.blue, opacity: self.alpha) 68 | case .hsb: 69 | return Color(hue: self.hue, saturation: self.saturation, brightness: self.brightness, opacity: self.alpha) 70 | case .cmyk: 71 | return Color(PlatformColor(cmyk: (CGFloat(self.cyan), CGFloat(self.magenta), CGFloat(self.yellow), CGFloat(self.keyBlack)))).opacity(alpha) 72 | case .gray: 73 | return Color(white: self.white).opacity(alpha) 74 | } 75 | } 76 | 77 | public var fileFormat: String { 78 | switch colorFormulation { 79 | case .rgb: 80 | return "Color(.\(self.rgbColorSpace.space), red: \(self.red), green: \(self.green), blue: \(self.blue), opacity: \(self.alpha))" 81 | case .hsb: 82 | return "Color(hue: \(self.hue), saturation: \(self.saturation), brightness: \(self.brightness), opacity: \(self.alpha))" 83 | case .cmyk: 84 | return "Color(PlatformColor(cmyk: (\(self.cyan), \(self.magenta), \(self.yellow), \(self.keyBlack)))).opacity(\(alpha))" 85 | case .gray: 86 | return "Color(white: \(self.white).opacity(\(alpha))" 87 | } 88 | } 89 | 90 | internal init(id: UUID, 91 | date: Date, 92 | name: String, 93 | formulation: ColorFormulation, 94 | rgbColorSpace: RGBColorSpace, 95 | white: Double, 96 | red: Double, 97 | green: Double, 98 | blue: Double, 99 | hue: Double, 100 | saturation: Double, 101 | brightness: Double, 102 | cyan: Double, 103 | magenta: Double, 104 | yellow: Double, 105 | keyBlack: Double, 106 | alpha: Double) { 107 | self.id = id 108 | self.dateCreated = date 109 | self.name = name 110 | self.colorFormulation = formulation 111 | self.rgbColorSpace = rgbColorSpace 112 | self.white = white 113 | self.red = red 114 | self.green = green 115 | self.blue = blue 116 | self.hue = hue 117 | self.saturation = saturation 118 | self.brightness = brightness 119 | self.cyan = cyan 120 | self.magenta = magenta 121 | self.yellow = yellow 122 | self.keyBlack = keyBlack 123 | self.alpha = alpha 124 | 125 | } 126 | 127 | public func update() -> ColorToken { 128 | .init(id: self.id, 129 | date: self.dateCreated, 130 | name: self.name, 131 | formulation: self.colorFormulation, 132 | rgbColorSpace: self.rgbColorSpace, 133 | white: self.white, 134 | red: self.red, 135 | green: self.green, 136 | blue: self.blue, 137 | hue: self.hue, 138 | saturation: self.saturation, 139 | brightness: self.brightness, 140 | cyan: self.cyan, 141 | magenta: self.magenta, 142 | yellow: self.yellow, 143 | keyBlack: self.keyBlack, 144 | alpha: self.alpha) 145 | } 146 | 147 | public mutating func update(white: Double) -> ColorToken { 148 | self.white = white 149 | self.colorFormulation = .gray 150 | return self.update() 151 | } 152 | public mutating func update(red: Double) -> ColorToken { 153 | self.red = red 154 | self.colorFormulation = .rgb 155 | return self.update() 156 | } 157 | public mutating func update(green: Double) -> ColorToken { 158 | self.green = green 159 | self.colorFormulation = .rgb 160 | return self.update() 161 | } 162 | public mutating func update(blue: Double) -> ColorToken { 163 | self.blue = blue 164 | self.colorFormulation = .rgb 165 | return self.update() 166 | } 167 | public mutating func update(hue: Double) -> ColorToken { 168 | self.hue = hue 169 | self.colorFormulation = .hsb 170 | return self.update() 171 | } 172 | public mutating func update(saturation: Double) -> ColorToken { 173 | self.saturation = saturation 174 | self.colorFormulation = .hsb 175 | return self.update() 176 | } 177 | public mutating func update(brightness: Double) -> ColorToken { 178 | self.brightness = brightness 179 | self.colorFormulation = .hsb 180 | return self.update() 181 | } 182 | public mutating func update(cyan: Double) -> ColorToken { 183 | self.cyan = cyan 184 | self.colorFormulation = .cmyk 185 | return self.update() 186 | } 187 | public mutating func update(magenta: Double) -> ColorToken { 188 | self.magenta = magenta 189 | self.colorFormulation = .cmyk 190 | return self.update() 191 | } 192 | public mutating func update(yellow: Double) -> ColorToken { 193 | self.yellow = yellow 194 | self.colorFormulation = .cmyk 195 | return self.update() 196 | } 197 | public mutating func update(keyBlack: Double) -> ColorToken { 198 | self.keyBlack = keyBlack 199 | self.colorFormulation = .cmyk 200 | return self.update() 201 | } 202 | public mutating func update(alpha: Double) -> ColorToken { 203 | self.alpha = alpha 204 | return self.update() 205 | } 206 | 207 | 208 | // MARK: RGB Inits 209 | public init(r: Double, g: Double, b: Double) { 210 | self.id = .init() 211 | self.dateCreated = .init() 212 | self.red = r 213 | self.green = g 214 | self.blue = b 215 | self.colorFormulation = .rgb 216 | } 217 | public init(name: String, r: Double, g: Double, b: Double) { 218 | self.name = name 219 | self.id = .init() 220 | self.dateCreated = .init() 221 | self.red = r 222 | self.green = g 223 | self.blue = b 224 | self.colorFormulation = .rgb 225 | } 226 | public init(colorSpace: RGBColorSpace, r: Double, g: Double, b: Double) { 227 | self.id = .init() 228 | self.dateCreated = .init() 229 | self.red = r 230 | self.green = g 231 | self.blue = b 232 | self.colorFormulation = .rgb 233 | self.rgbColorSpace = colorSpace 234 | 235 | } 236 | public init(name: String, colorSpace: RGBColorSpace, r: Double, g: Double, b: Double) { 237 | self.name = name 238 | self.id = .init() 239 | self.dateCreated = .init() 240 | self.red = r 241 | self.green = g 242 | self.blue = b 243 | self.colorFormulation = .rgb 244 | self.rgbColorSpace = colorSpace 245 | 246 | } 247 | public init(r: Double, g: Double, b: Double, a: Double) { 248 | self.id = .init() 249 | self.dateCreated = .init() 250 | self.red = r 251 | self.green = g 252 | self.blue = b 253 | self.alpha = a 254 | self.colorFormulation = .rgb 255 | } 256 | public init(name: String, r: Double, g: Double, b: Double, a: Double) { 257 | self.name = name 258 | self.id = .init() 259 | self.dateCreated = .init() 260 | self.red = r 261 | self.green = g 262 | self.blue = b 263 | self.alpha = a 264 | self.colorFormulation = .rgb 265 | } 266 | public init(colorSpace: RGBColorSpace, r: Double, g: Double, b: Double, a: Double) { 267 | self.id = .init() 268 | self.dateCreated = .init() 269 | self.red = r 270 | self.green = g 271 | self.blue = b 272 | self.alpha = a 273 | self.colorFormulation = .rgb 274 | self.rgbColorSpace = colorSpace 275 | 276 | } 277 | public init(name: String, colorSpace: RGBColorSpace, r: Double, g: Double, b: Double, a: Double) { 278 | self.name = name 279 | self.id = .init() 280 | self.dateCreated = .init() 281 | self.red = r 282 | self.green = g 283 | self.blue = b 284 | self.alpha = a 285 | self.colorFormulation = .rgb 286 | self.rgbColorSpace = colorSpace 287 | 288 | } 289 | // MARK: HSB Inits 290 | public init(hue: Double, saturation: Double, brightness: Double) { 291 | self.id = .init() 292 | self.dateCreated = .init() 293 | self.hue = hue 294 | self.saturation = saturation 295 | self.brightness = brightness 296 | self.colorFormulation = .hsb 297 | } 298 | public init(name: String, hue: Double, saturation: Double, brightness: Double) { 299 | self.id = .init() 300 | self.dateCreated = .init() 301 | self.name = name 302 | self.hue = hue 303 | self.saturation = saturation 304 | self.brightness = brightness 305 | self.colorFormulation = .hsb 306 | } 307 | public init(hue: Double, saturation: Double, brightness: Double, opacity: Double) { 308 | self.id = .init() 309 | self.dateCreated = .init() 310 | self.hue = hue 311 | self.saturation = saturation 312 | self.brightness = brightness 313 | self.alpha = opacity 314 | self.colorFormulation = .hsb 315 | } 316 | public init(name: String, hue: Double, saturation: Double, brightness: Double, opacity: Double) { 317 | self.id = .init() 318 | self.dateCreated = .init() 319 | self.name = name 320 | self.hue = hue 321 | self.saturation = saturation 322 | self.brightness = brightness 323 | self.alpha = opacity 324 | self.colorFormulation = .hsb 325 | } 326 | // MARK: CMYK Inits 327 | public init(cyan: Double, magenta: Double, yellow: Double, keyBlack: Double) { 328 | self.id = .init() 329 | self.dateCreated = .init() 330 | self.cyan = cyan 331 | self.magenta = magenta 332 | self.yellow = yellow 333 | self.keyBlack = keyBlack 334 | self.colorFormulation = .cmyk 335 | } 336 | public init(name: String, cyan: Double, magenta: Double, yellow: Double, keyBlack: Double) { 337 | self.id = .init() 338 | self.dateCreated = .init() 339 | self.name = name 340 | self.cyan = cyan 341 | self.magenta = magenta 342 | self.yellow = yellow 343 | self.keyBlack = keyBlack 344 | self.colorFormulation = .cmyk 345 | } 346 | // MARK: White Inits 347 | public init(white: Double) { 348 | self.id = .init() 349 | self.dateCreated = .init() 350 | self.white = white 351 | self.colorFormulation = .gray 352 | } 353 | public init(name: String, white: Double) { 354 | self.id = .init() 355 | self.dateCreated = .init() 356 | self.name = name 357 | self.white = white 358 | self.colorFormulation = .gray 359 | } 360 | public init(white: Double, opacity: Double) { 361 | self.id = .init() 362 | self.dateCreated = .init() 363 | self.white = white 364 | self.alpha = opacity 365 | self.colorFormulation = .gray 366 | } 367 | public init(name: String, white: Double, opacity: Double) { 368 | self.id = .init() 369 | self.dateCreated = .init() 370 | self.name = name 371 | self.white = white 372 | self.alpha = opacity 373 | self.colorFormulation = .gray 374 | } 375 | public init(_ token: ColorToken) { 376 | self.id = .init() 377 | self.dateCreated = .init() 378 | self.name = token.name 379 | self.alpha = token.alpha 380 | self.white = token.white 381 | self.rgbColorSpace = token.rgbColorSpace 382 | self.colorFormulation = token.colorFormulation 383 | self.red = token.red 384 | self.green = token.green 385 | self.blue = token.blue 386 | self.hue = token.hue 387 | self.saturation = token.saturation 388 | self.brightness = token.brightness 389 | self.cyan = token.cyan 390 | self.magenta = token.magenta 391 | self.yellow = token.yellow 392 | self.keyBlack = token.keyBlack 393 | 394 | } 395 | 396 | } 397 | 398 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 399 | public extension ColorToken { 400 | 401 | // MARK: - Color Scheme 402 | enum ColorScheme: String, CaseIterable { 403 | case analagous 404 | case monochromatic = "mono" 405 | case triad 406 | case complementary = "complement" 407 | } 408 | 409 | 410 | func colorScheme(_ type: ColorScheme) -> [ColorToken] { 411 | switch (type) { 412 | case .analagous: 413 | return analgousColors() 414 | case .monochromatic: 415 | return monochromaticColors() 416 | case .triad: 417 | return triadColors() 418 | default: 419 | return complementaryColors() 420 | } 421 | } 422 | 423 | func analgousColors() -> [ColorToken] { 424 | return [ColorToken(hue: (hue*360+30)/360, saturation: saturation-0.05, brightness: brightness-0.1, opacity: alpha), 425 | ColorToken(hue: (hue*360+15)/360, saturation: saturation-0.05, brightness: brightness-0.05, opacity: alpha), 426 | ColorToken(hue: (hue*360-15)/360, saturation: saturation-0.05, brightness: brightness-0.05, opacity: alpha), 427 | ColorToken(hue: (hue*360-30)/360, saturation: saturation-0.05, brightness: brightness-0.1, opacity: alpha)] 428 | } 429 | 430 | func monochromaticColors() -> [ColorToken] { 431 | return [ColorToken(hue: hue, saturation: saturation/2, brightness: brightness/3, opacity: alpha), 432 | ColorToken(hue: hue, saturation: saturation, brightness: brightness/2, opacity: alpha), 433 | ColorToken(hue: hue, saturation: saturation/3, brightness: 2*brightness/3, opacity: alpha), 434 | ColorToken(hue: hue, saturation: saturation, brightness: 4*brightness/5, opacity: alpha)] 435 | 436 | } 437 | 438 | func triadColors() -> [ColorToken] { 439 | return [ColorToken(hue: (120+hue*360)/360, saturation: 2*saturation/3, brightness: brightness-0.05, opacity: alpha), 440 | ColorToken(hue: (120+hue*360)/360, saturation: saturation, brightness: brightness, opacity: alpha), 441 | ColorToken(hue: (240+hue*360)/360, saturation: saturation, brightness: brightness, opacity: alpha), 442 | ColorToken(hue: (240+hue*360)/360, saturation: 2*saturation/3, brightness: brightness-0.05, opacity: alpha)] 443 | 444 | } 445 | 446 | func complementaryColors() -> [ColorToken] { 447 | return [ColorToken(hue: hue, saturation: saturation, brightness: 4*brightness/5, opacity: alpha), 448 | ColorToken(hue: hue, saturation: 5*saturation/7, brightness: brightness, opacity: alpha), 449 | ColorToken(hue: (180+hue*360)/360, saturation: saturation, brightness: brightness, opacity: alpha), 450 | ColorToken(hue: (180+hue*360)/360, saturation: 5*saturation/7, brightness: brightness, opacity: alpha)] 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Color Kit Logo 5 | 6 |

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

12 | 13 | **Color Kit** is a brand new SwiftUI library with the goal of providing all the essential building blocks to make the most *epic* color picking experience. Built with the developer in mind **Color Kit** has a simple API while still retaining the infinitely customizable components you need! 14 | 15 | 16 |

17 | Example Collage 18 |

19 | 20 | ## Why Use Color Kit? 21 | 22 | * You need to make the best color picking experience possible 23 | * You need the flexibility of cascading styles 24 | * You need to make your app quickly and with minimal headaches 25 | * You need to export your creations into code that works instantly 26 | 27 | ## What Components Does Color Kit Provide? 28 | 29 | * **Color Pickers** 30 | * **RGB** 31 | * **RGBA** 32 | * **HSB** 33 | * **HSBA** 34 | * **CMYK** 35 | * **Gray Scale** 36 | * **All In One** 37 | * **Gradient Pickers** 38 | * **Linear** 39 | * **Radial** 40 | * **Angular** 41 | * **All In One** 42 | * **Data Objects** 43 | * `ColorToken` 44 | * `GradientData` 45 | 46 | ## Getting Started 47 | 48 | ### Method 1 49 | 50 | Adding **Color Kit** as a dependency to an existing project 51 | 52 | 1. Copy that URL from the github repo 53 | 2. In Xcode -> File -> Swift Packages -> Add Package Dependencies 54 | 3. Paste the URL Into the box 55 | 4. Specify the minimum version number 1.0.2 56 | 5. Add `import ColorKit` to the top of the file you are adding a picker to. 57 | 6. Follow the implementation steps for the picker you wish to use (these will generally be found in the top level comments for the picker, you can also find them in the readme) 58 | 59 | 60 | ### Method 2 61 | 62 | Clone the [example project](https://github.com/kieranb662/Color-Kit-Examples) and play around with all the different examples to get a feel for the library. If you are really feeling adventurous try making your own custom styles for one of the gradient pickers. Its super easy and **Color Kit** even comes with snippets to help get you started. 63 | 64 | 65 | ## Components 66 | 67 | ### ColorToken 68 | 69 | `ColorToken` is a data object used to store the various color values that could be used with the pickers. I wont go into the possible initializers here, but I implore you to take a look at the source code. The values stored within `ColorToken` are: 70 | 71 | * `id: UUID` - a unique identifier created at initialization 72 | * `dateCreated: Date` - a timestamp created at initialization 73 | * `name: String` - The name you specify for the color (**Default**: "New") 74 | * `colorFormulation: ColorFormulation` - An enumeration describing which type of color is being used rgb, hsb, cmyk, grayscale 75 | * `rgbColorSpace: RGBColorSpace` - A wrapper enum for SwiftUI's RGBColorSpace (**Default**: sRGB) 76 | * `white: Double` - white value used with grayscale should be between 0 and 1 (**Default**: 0.5) 77 | * `red: Double` - red value used with RGB should be between 0 and 1 (**Default**: 0.5) 78 | * `green: Double` - green value used with RGB should be between 0 and 1 (**Default**: 0.5) 79 | * `blue: Double` - blue value used with RGB should be between 0 and 1 (**Default**: 0.5) 80 | * `hue: Double` - hue value used with HSB should be between 0 and 1 (**Default**: 0.5) 81 | * `saturation: Double` - saturation value used with HSB should be between 0 and 1 (**Default**: 0.5) 82 | * `brightness: Double` - brightness value used with HSB should be between 0 and 1 (**Default**: 0.5) 83 | * `cyan: Double` - cyan value used with CMYK should be between 0 and 1 (**Default**: 0.5) 84 | * `magenta: Double` - magenta value used with CMYK should be between 0 and 1 (**Default**: 0.5) 85 | * `yellow: Double` - yellow value used with CMYK should be between 0 and 1 (**Default**: 0.5) 86 | * `keyBlack: Double` - keyBlack value used with CMYK should be between 0 and 1 (**Default**: 0.5) 87 | * `alpha: Double` - alpha value used with any type of color formulation should be between 0 and 1 (**Default**: 1) 88 | 89 | 90 | SwiftUI's `Color` can be easily be generated by accessing `ColorToken.color` value. 91 | 92 | ## Color Pickers 93 | 94 | Each of the various color pickers and sliders are really just styled version of components from the [Sliders](https://github.com/kieranb662/Sliders) library. 95 | 96 |

97 | RGB Color Picker 98 | RGB Color Picker 99 | CMYK Color Picker 100 | Gray scale Color Picker 101 |

102 | 103 | 104 | ### Alpha Slider 105 | 106 | A pre-styled Slider used to change the alpha/opacity of a `ColorToken` The `AlphaSlider` view is really just a styled `LSlider` with a custom binding to a `ColorToken` 107 | 108 |

109 | AlphaSlider 110 | 111 | 112 | ### RGB Sliders 113 | 114 | A set of pre-styled RGB sliders that contain a gradient representing the color if that slider was dragged to either of its limits. The `RGBColorPicker` view is really just 3 styled `LSlider`'s with a custom binding to a `ColorToken` 115 | 116 |

117 | RGB Sliders 118 | 119 | ### Saturation Brightness TrackPad 120 | 121 | A styled `TrackPad` that uses two composited linear gradients to get the desired 2D saturation brightness gradient effect 122 | 123 |

124 | Saturation Brightness TrackPad 125 | 126 | 127 | ### Hue Slider 128 | 129 | A styled `LSlider` with a background gradient representing a full hue rotation. 130 | 131 |

132 | Hue Slider 133 | 134 | ### CMYK Sliders 135 | 136 | Conceptually similar to the RGB Sliders. These represent the values of a CMYK color 137 | 138 |

139 | CMYK Sliders 140 | 141 | ### Bonus Circular HSB Picker 142 | 143 | This one isn't a part of the library but is available as an example implementation within the **ColorKitExamples** project. It makes use of an `MTKView` to from the MetalKit framework to draw the circular gradient. 144 | 145 |

146 | CMYK Sliders 147 | 148 | ## Gradient Pickers 149 | 150 | ### Linear 151 | 152 | A Component view used to create and style a `Linear Gradient` to the users liking. 153 | The sub components that make up the gradient picker are 154 | 1. **Gradient**: The Linear gradient containing view 155 | 2. **Start Handle**: A draggable view representing the start location of the gradient 156 | 3. **End Handle**: A draggable view representing the end location of the gradient 157 | 4. **LinearStop**: A draggable view representing a gradient stop that is constrained to be located within the start and and handles locations 158 | 159 |

160 | Linear Gradient Picker Breakdown 161 | 162 | **Important**: You must create a `GradientManager` `ObservedObject` and then apply it to the `LinearGradientPicker` or the view containing it using the `environmentObject` method 163 | 164 | #### Styling The Picker 165 | In order to style the picker you must create a struct that conforms to the `LinearGradientPickerStyle` protocol. Conformance requires the implementation of 4 separate methods. To make this easier just copy and paste the following style based on the `DefaultLinearGradientPickerStyle`. After creating your custom style 166 | apply it by calling the `linearGradientPickerStyle` method on the `LinearGradientPicker` or a view containing it. 167 | 168 | ````Swift 169 | 170 | struct <#My Picker Style#>: LinearGradientPickerStyle { 171 | 172 | func makeGradient(gradient: LinearGradient) -> some View { 173 | RoundedRectangle(cornerRadius: 5) 174 | .fill(gradient) 175 | .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.white)) 176 | } 177 | 178 | func makeStartHandle(configuration: GradientHandleConfiguration) -> some View { 179 | Capsule() 180 | .foregroundColor(Color.white) 181 | .frame(width: 25, height: 75) 182 | .rotationEffect(configuration.angle + Angle(degrees: 90)) 183 | .animation(.none) 184 | .shadow(radius: 3) 185 | .opacity(configuration.isHidden ? 0 : 1) 186 | } 187 | 188 | func makeEndHandle(configuration: GradientHandleConfiguration) -> some View { 189 | Capsule() 190 | .foregroundColor(Color.white) 191 | .frame(width: 25, height: 75) 192 | .rotationEffect(configuration.angle + Angle(degrees: 90)) 193 | .animation(.none) 194 | .shadow(radius: 3) 195 | .opacity(configuration.isHidden ? 0 : 1) 196 | } 197 | 198 | func makeStop(configuration: GradientStopConfiguration) -> some View { 199 | Capsule() 200 | .foregroundColor(configuration.color) 201 | .frame(width: 20, height: 55) 202 | .overlay(Capsule().stroke( configuration.isSelected ? Color.yellow : Color.white )) 203 | .rotationEffect(configuration.angle + Angle(degrees: 90)) 204 | .animation(.none) 205 | .shadow(color: configuration.isSelected ? Color.white : Color.black, radius: 3) 206 | .opacity(configuration.isHidden ? 0 : 1) 207 | 208 | } 209 | } 210 | 211 | ```` 212 | 213 | ### Radial 214 | 215 | 216 | 217 | 218 | A Component view used to create and style a `RadialGradient` to the users liking. 219 | The sub components that make up the gradient picker are 220 | 1. **Gradient**: The Radial Gradient containing view 221 | 2. **Center Thumb**: A draggable view representing the center of the gradient 222 | 3. **StartHandle**: A draggable circle representing the start radius that grows larger/small as you drag away/closer from the center thumb 223 | 4. **EndHandle**: A draggable circle representing the end radius that grows larger/small as you drag away/closer from the center thumb 224 | 5. **RadialStop**: A draggable view contained to the gradient bar, represents the unit location of the stop 225 | 6. **Gradient Bar**: A slider like container filled with a linear gradient created with the gradient stops. 226 | 227 |

228 | Radial Gradient Picker Breakdown 229 | 230 | **Important**: You must create a `GradientManager` `ObservedObject` and then apply it to the `RadialGradientPicker` 231 | or the view containing it using the `environmentObject` method 232 | 233 | 234 | ## Styling The Picker 235 | In order to style the picker you must create a struct that conforms to the `RadialGradientPickerStyle` protocol. Conformance requires the implementation of 236 | 6 separate methods. To make this easier just copy and paste the following style based on the `DefaultRadialGradientPickerStyle`. After creating your custom style 237 | apply it by calling the `radialGradientPickerStyle` method on the `RadialGradientPicker` or a view containing it. 238 | 239 | ````Swift 240 | struct <#My Picker Style#>: RadialGradientPickerStyle { 241 | 242 | func makeGradient(gradient: RadialGradient) -> some View { 243 | RoundedRectangle(cornerRadius: 5) 244 | .fill(gradient) 245 | .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.white)) 246 | } 247 | 248 | func makeCenter(configuration: GradientCenterConfiguration) -> some View { 249 | Circle().fill(configuration.isActive ? Color.yellow : Color.white) 250 | .frame(width: 35, height: 35) 251 | .opacity(configuration.isHidden ? 0 : 1) 252 | .animation(.easeIn) 253 | } 254 | 255 | func makeStartHandle(configuration: GradientHandleConfiguration) -> some View { 256 | Circle() 257 | .stroke(Color.white.opacity(0.001), style: StrokeStyle(lineWidth: 10)) 258 | .overlay(Circle().stroke(Color.black, style: StrokeStyle(lineWidth: 1, dash: [10, 5]))) 259 | .opacity(configuration.isHidden ? 0 : 1) 260 | .animation(.easeIn) 261 | } 262 | 263 | func makeEndHandle(configuration: GradientHandleConfiguration) -> some View { 264 | Circle() 265 | .stroke(Color.white.opacity(0.001), style: StrokeStyle(lineWidth: 10)) 266 | .overlay(Circle().stroke(Color.white, style: StrokeStyle(lineWidth: 1, dash: [10, 5]))) 267 | .opacity(configuration.isHidden ? 0 : 1) 268 | .animation(.easeIn) 269 | } 270 | 271 | func makeStop(configuration: GradientStopConfiguration) -> some View { 272 | Group { 273 | if !configuration.isHidden { 274 | RoundedRectangle(cornerRadius: 5) 275 | .foregroundColor(configuration.color) 276 | .frame(width: 25, height: 45) 277 | .overlay(RoundedRectangle(cornerRadius: 5).stroke( configuration.isSelected ? Color.yellow : Color.white )) 278 | .shadow(color: configuration.isSelected ? Color.white : Color.black, radius: 3) 279 | .transition(AnyTransition.opacity) 280 | .animation(Animation.easeOut) 281 | } 282 | } 283 | } 284 | 285 | func makeBar(configuration: RadialGradientBarConfiguration) -> some View { 286 | Group { 287 | if !configuration.isHidden { 288 | RoundedRectangle(cornerRadius: 5) 289 | .fill(LinearGradient(gradient: configuration.gradient, startPoint: .leading, endPoint: .trailing)) 290 | .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.white)) 291 | .transition(AnyTransition.move(edge: .leading)) 292 | .animation(Animation.easeOut) 293 | } 294 | } 295 | } 296 | } 297 | ```` 298 | 299 | ### Angular 300 | 301 | A Component view used to create and style an `AngularGradient` to the users liking 302 | The sub components that make up the gradient picker are 303 | 1. **Gradient**: The Angular Gradient containing view 304 | 2. **Center Thumb**: A draggable view representing the location of the gradients center 305 | 3. **Start Handle**: A draggable view representing the start location of the gradient 306 | 4. **End Handle**: A draggable view representing the end location of the gradient 307 | 5. **AngularStop**: A draggable view representing a gradient stop that is constrained to be between the current stop and end angles locations 308 | 309 |

310 | Angular Gradient Picker Breakdown 311 | 312 | **Important**: You must create a `GradientManager` `ObservedObject` and then apply it to the `AngularGradientPicker` 313 | or the view containing it using the `environmentObject` method 314 | 315 | ## Styling The Picker 316 | 317 | In order to style the picker you must create a struct that conforms to the `AngularGradientPickerStyle` protocol. Conformance requires the implementation of 318 | 4 separate methods. To make this easier just copy and paste the following style based on the `DefaultAngularGradientPickerStyle`. After creating your custom style 319 | apply it by calling the `angularGradientPickerStyle` method on the `AngularGradientPicker` or a view containing it. 320 | 321 | ```Swift 322 | struct <#My Picker Style#>: AngularGradientPickerStyle { 323 | 324 | func makeGradient(gradient: AngularGradient) -> some View { 325 | RoundedRectangle(cornerRadius: 5) 326 | .fill(gradient) 327 | .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.white)) 328 | } 329 | 330 | func makeCenter(configuration: GradientCenterConfiguration) -> some View { 331 | Circle().fill(configuration.isActive ? Color.yellow : Color.white) 332 | .frame(width: 35, height: 35) 333 | .opacity(configuration.isHidden ? 0 : 1) 334 | } 335 | 336 | func makeStartHandle(configuration: GradientHandleConfiguration) -> some View { 337 | Capsule() 338 | .foregroundColor(Color.white) 339 | .frame(width: 30, height: 75) 340 | .rotationEffect(configuration.angle) 341 | .animation(.none) 342 | .shadow(radius: 3) 343 | .opacity(configuration.isHidden ? 0 : 1) 344 | } 345 | 346 | func makeEndHandle(configuration: GradientHandleConfiguration) -> some View { 347 | Capsule() 348 | .foregroundColor(Color.white) 349 | .frame(width: 30, height: 75) 350 | .rotationEffect(configuration.angle) 351 | .animation(.none) 352 | .shadow(radius: 3) 353 | .opacity(configuration.isHidden ? 0 : 1) 354 | } 355 | 356 | func makeStop(configuration: GradientStopConfiguration) -> some View { 357 | Group { 358 | if !configuration.isHidden { 359 | Circle() 360 | .foregroundColor(configuration.color) 361 | .frame(width: 25, height: 45) 362 | .overlay(Circle().stroke( configuration.isSelected ? Color.yellow : Color.white )) 363 | .shadow(color: configuration.isSelected ? Color.white : Color.black, radius: 3) 364 | .transition(AnyTransition.opacity) 365 | .animation(Animation.easeOut) 366 | } 367 | } 368 | } 369 | } 370 | ``` 371 | -------------------------------------------------------------------------------- /Sources/ColorKit/Gradient Picker/GradientStyles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientStyles.swift 3 | // MyExamples 4 | // 5 | // Created by Kieran Brown on 4/6/20. 6 | // Copyright © 2020 BrownandSons. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | // MARK: - Configuration Structures 11 | 12 | 13 | /// Used to style the dragging view that represents a gradients start or end value 14 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 15 | public struct GradientHandleConfiguration { 16 | public let isActive: Bool 17 | public let isDragging: Bool 18 | public let isHidden: Bool 19 | public let angle: Angle 20 | public init(_ isActive: Bool, _ isDragging: Bool, _ isHidden: Bool, _ angle: Angle) { 21 | self.isActive = isActive 22 | self.isDragging = isDragging 23 | self.isHidden = isHidden 24 | self.angle = angle 25 | } 26 | } 27 | /// Used to style a view representing a single stop in a gradient 28 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 29 | public struct GradientStopConfiguration { 30 | public let isActive: Bool 31 | public let isSelected: Bool 32 | public let isHidden: Bool 33 | public let color: Color 34 | public let angle: Angle 35 | 36 | public init(_ isActive: Bool, _ isSelected: Bool, _ isHidden: Bool, _ color: Color, _ angle: Angle) { 37 | self.isActive = isActive 38 | self.isSelected = isSelected 39 | self.isHidden = isHidden 40 | self.color = color 41 | self.angle = angle 42 | } 43 | } 44 | /// Used to style the draggable view representing the center of either an Angular or Radial Gradient 45 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 46 | public struct GradientCenterConfiguration { 47 | public let isActive: Bool 48 | public let isHidden: Bool 49 | } 50 | /// Used to style the slider bar the stops overlay in the `RadialGradientPicker` 51 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 52 | public struct RadialGradientBarConfiguration { 53 | public let gradient: Gradient 54 | public let isHidden: Bool 55 | } 56 | 57 | // MARK: - Default Styles 58 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 59 | public struct DefaultLinearGradientPickerStyle: LinearGradientPickerStyle { 60 | public init() {} 61 | public func makeGradient(gradient: LinearGradient) -> some View { 62 | RoundedRectangle(cornerRadius: 5) 63 | .fill(gradient) 64 | .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.white)) 65 | } 66 | public func makeStartHandle(configuration: GradientHandleConfiguration) -> some View { 67 | Capsule() 68 | .foregroundColor(Color.white) 69 | .frame(width: 25, height: 75) 70 | .rotationEffect(configuration.angle + Angle(degrees: 90)) 71 | .animation(.none) 72 | .shadow(radius: 3) 73 | .opacity(configuration.isHidden ? 0 : 1) 74 | } 75 | public func makeEndHandle(configuration: GradientHandleConfiguration) -> some View { 76 | Capsule() 77 | .foregroundColor(Color.white) 78 | .frame(width: 25, height: 75) 79 | .rotationEffect(configuration.angle + Angle(degrees: 90)) 80 | .animation(.none) 81 | .shadow(radius: 3) 82 | .opacity(configuration.isHidden ? 0 : 1) 83 | } 84 | public func makeStop(configuration: GradientStopConfiguration) -> some View { 85 | Capsule() 86 | .foregroundColor(configuration.color) 87 | .frame(width: 20, height: 55) 88 | .overlay(Capsule().stroke( configuration.isSelected ? Color.yellow : Color.white )) 89 | .rotationEffect(configuration.angle + Angle(degrees: 90)) 90 | .animation(.none) 91 | .shadow(color: configuration.isSelected ? Color.white : Color.black, radius: 3) 92 | .opacity(configuration.isHidden ? 0 : 1) 93 | 94 | } 95 | } 96 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 97 | public struct DefaultRadialGradientPickerStyle: RadialGradientPickerStyle { 98 | public init() {} 99 | public func makeGradient(gradient: RadialGradient) -> some View { 100 | RoundedRectangle(cornerRadius: 5) 101 | .fill(gradient) 102 | .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.white)) 103 | } 104 | public func makeCenter(configuration: GradientCenterConfiguration) -> some View { 105 | Circle() 106 | .fill(configuration.isActive ? Color.yellow : Color.white) 107 | .frame(width: 35, height: 35) 108 | .opacity(configuration.isHidden ? 0 : 1) 109 | .animation(.easeIn) 110 | } 111 | public func makeStartHandle(configuration: GradientHandleConfiguration) -> some View { 112 | Circle() 113 | .stroke(Color.white.opacity(0.001), style: StrokeStyle(lineWidth: 10)) 114 | .overlay(Circle().stroke(Color.black, style: StrokeStyle(lineWidth: 1, dash: [10, 5]))) 115 | .opacity(configuration.isHidden ? 0 : 1) 116 | .animation(.easeIn) 117 | } 118 | public func makeEndHandle(configuration: GradientHandleConfiguration) -> some View { 119 | Circle() 120 | .stroke(Color.white.opacity(0.001), style: StrokeStyle(lineWidth: 10)) 121 | .overlay(Circle().stroke(Color.white, style: StrokeStyle(lineWidth: 1, dash: [10, 5]))) 122 | .opacity(configuration.isHidden ? 0 : 1) 123 | .animation(.easeIn) 124 | } 125 | public func makeStop(configuration: GradientStopConfiguration) -> some View { 126 | Group { 127 | if !configuration.isHidden { 128 | RoundedRectangle(cornerRadius: 5) 129 | .foregroundColor(configuration.color) 130 | .frame(width: 25, height: 45) 131 | .overlay(RoundedRectangle(cornerRadius: 5).stroke( configuration.isSelected ? Color.yellow : Color.white )) 132 | .shadow(color: configuration.isSelected ? Color.white : Color.black, radius: 3) 133 | .transition(AnyTransition.opacity) 134 | .animation(Animation.easeOut) 135 | } 136 | } 137 | } 138 | public func makeBar(configuration: RadialGradientBarConfiguration) -> some View { 139 | Group { 140 | if !configuration.isHidden { 141 | RoundedRectangle(cornerRadius: 5) 142 | .fill(LinearGradient(gradient: configuration.gradient, startPoint: .leading, endPoint: .trailing)) 143 | .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.white)) 144 | .transition(AnyTransition.move(edge: .leading)) 145 | .animation(Animation.easeOut) 146 | } 147 | } 148 | } 149 | } 150 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 151 | public struct DefaultAngularGradientPickerStyle: AngularGradientPickerStyle { 152 | public init() {} 153 | public func makeGradient(gradient: AngularGradient) -> some View { 154 | RoundedRectangle(cornerRadius: 5) 155 | .fill(gradient) 156 | .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.white)) 157 | } 158 | public func makeCenter(configuration: GradientCenterConfiguration) -> some View { 159 | Circle() 160 | .fill(configuration.isActive ? Color.yellow : Color.white) 161 | .frame(width: 35, height: 35) 162 | .opacity(configuration.isHidden ? 0 : 1) 163 | } 164 | public func makeStartHandle(configuration: GradientHandleConfiguration) -> some View { 165 | Capsule() 166 | .foregroundColor(Color.white) 167 | .frame(width: 30, height: 75) 168 | .rotationEffect(configuration.angle) 169 | .animation(.none) 170 | .shadow(radius: 3) 171 | .opacity(configuration.isHidden ? 0 : 1) 172 | } 173 | public func makeEndHandle(configuration: GradientHandleConfiguration) -> some View { 174 | Capsule() 175 | .foregroundColor(Color.white) 176 | .frame(width: 30, height: 75) 177 | .rotationEffect(configuration.angle) 178 | .animation(.none) 179 | .shadow(radius: 3) 180 | .opacity(configuration.isHidden ? 0 : 1) 181 | } 182 | public func makeStop(configuration: GradientStopConfiguration) -> some View { 183 | Group { 184 | if !configuration.isHidden { 185 | Circle() 186 | .foregroundColor(configuration.color) 187 | .frame(width: 25, height: 45) 188 | .overlay(Circle().stroke( configuration.isSelected ? Color.yellow : Color.white )) 189 | .shadow(color: configuration.isSelected ? Color.white : Color.black, radius: 3) 190 | .transition(AnyTransition.opacity) 191 | .animation(Animation.easeOut) 192 | } 193 | } 194 | } 195 | } 196 | 197 | // MARK: - Style Setup 198 | 199 | // MARK: Linear 200 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 201 | public protocol LinearGradientPickerStyle { 202 | associatedtype GradientView: View 203 | associatedtype StartHandle: View 204 | associatedtype EndHandle: View 205 | associatedtype Stop: View 206 | 207 | func makeGradient(gradient: LinearGradient) -> Self.GradientView 208 | func makeStartHandle(configuration: GradientHandleConfiguration) -> Self.StartHandle 209 | func makeEndHandle(configuration: GradientHandleConfiguration) -> Self.EndHandle 210 | func makeStop(configuration: GradientStopConfiguration) -> Self.Stop 211 | } 212 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 213 | public extension LinearGradientPickerStyle { 214 | func makeGradientTypeErased(gradient: LinearGradient) -> AnyView { 215 | AnyView(self.makeGradient(gradient: gradient)) 216 | } 217 | func makeStartHandleTypeErased(configuration: GradientHandleConfiguration) -> AnyView { 218 | AnyView(self.makeStartHandle(configuration: configuration)) 219 | } 220 | func makeEndHandleTypeErased(configuration: GradientHandleConfiguration) -> AnyView { 221 | AnyView(self.makeEndHandle(configuration: configuration)) 222 | } 223 | func makeStopTypeErased(configuration: GradientStopConfiguration) -> AnyView { 224 | AnyView(self.makeStop(configuration: configuration)) 225 | } 226 | } 227 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 228 | public struct AnyLinearGradientPickerStyle: LinearGradientPickerStyle { 229 | private let _makeGradient: (LinearGradient) -> AnyView 230 | public func makeGradient(gradient: LinearGradient) -> some View { 231 | return self._makeGradient(gradient) 232 | } 233 | private let _makeStartHandle: (GradientHandleConfiguration) -> AnyView 234 | public func makeStartHandle(configuration: GradientHandleConfiguration) -> some View { 235 | return self._makeStartHandle(configuration) 236 | } 237 | 238 | private let _makeEndHandle: (GradientHandleConfiguration) -> AnyView 239 | public func makeEndHandle(configuration: GradientHandleConfiguration) -> some View { 240 | return self._makeEndHandle(configuration) 241 | } 242 | 243 | private let _makeStop: (GradientStopConfiguration) -> AnyView 244 | public func makeStop(configuration: GradientStopConfiguration) -> some View { 245 | return self._makeStop(configuration) 246 | } 247 | 248 | public init(_ style: ST) { 249 | self._makeGradient = style.makeGradientTypeErased 250 | self._makeStartHandle = style.makeStartHandleTypeErased 251 | self._makeEndHandle = style.makeEndHandleTypeErased 252 | self._makeStop = style.makeStopTypeErased 253 | } 254 | } 255 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 256 | public struct LinearGradientPickerStyleKey: EnvironmentKey { 257 | public static let defaultValue: AnyLinearGradientPickerStyle = AnyLinearGradientPickerStyle(DefaultLinearGradientPickerStyle()) 258 | } 259 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 260 | extension EnvironmentValues { 261 | public var linearGradientPickerStyle: AnyLinearGradientPickerStyle { 262 | get { 263 | return self[LinearGradientPickerStyleKey.self] 264 | } 265 | set { 266 | self[LinearGradientPickerStyleKey.self] = newValue 267 | } 268 | } 269 | } 270 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 271 | extension View { 272 | public func linearGradientPickerStyle(_ style: S) -> some View where S: LinearGradientPickerStyle { 273 | self.environment(\.linearGradientPickerStyle, AnyLinearGradientPickerStyle(style)) 274 | } 275 | } 276 | 277 | // MARK: Radial 278 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 279 | public protocol RadialGradientPickerStyle { 280 | associatedtype GradientView: View 281 | associatedtype Center: View 282 | associatedtype StartHandle: View 283 | associatedtype EndHandle: View 284 | associatedtype Stop: View 285 | associatedtype GradientBar: View 286 | 287 | func makeGradient(gradient: RadialGradient) -> Self.GradientView 288 | func makeCenter(configuration: GradientCenterConfiguration) -> Self.Center 289 | func makeStartHandle(configuration: GradientHandleConfiguration) -> Self.StartHandle 290 | func makeEndHandle(configuration: GradientHandleConfiguration) -> Self.EndHandle 291 | func makeStop(configuration: GradientStopConfiguration) -> Self.Stop 292 | func makeBar(configuration: RadialGradientBarConfiguration) -> Self.GradientBar 293 | 294 | } 295 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 296 | public extension RadialGradientPickerStyle { 297 | func makeGradientTypeErased(gradient: RadialGradient) -> AnyView { 298 | AnyView(self.makeGradient(gradient: gradient)) 299 | } 300 | func makeCenterTypeErased(configuration: GradientCenterConfiguration) -> AnyView { 301 | AnyView(self.makeCenter(configuration: configuration)) 302 | } 303 | func makeStartHandleTypeErased(configuration: GradientHandleConfiguration) -> AnyView { 304 | AnyView(self.makeStartHandle(configuration: configuration)) 305 | } 306 | func makeEndHandleTypeErased(configuration: GradientHandleConfiguration) -> AnyView { 307 | AnyView(self.makeEndHandle(configuration: configuration)) 308 | } 309 | func makeStopTypeErased(configuration: GradientStopConfiguration) -> AnyView { 310 | AnyView(self.makeStop(configuration: configuration)) 311 | } 312 | 313 | func makeBarTypeErased(configuration: RadialGradientBarConfiguration) -> AnyView { 314 | AnyView(self.makeBar(configuration: configuration)) 315 | } 316 | } 317 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 318 | public struct AnyRadialGradientPickerStyle: RadialGradientPickerStyle { 319 | private let _makeGradient: (RadialGradient) -> AnyView 320 | public func makeGradient(gradient: RadialGradient) -> some View { 321 | return self._makeGradient(gradient) 322 | } 323 | private let _makeCenter: (GradientCenterConfiguration) -> AnyView 324 | public func makeCenter(configuration: GradientCenterConfiguration) -> some View { 325 | return self._makeCenter(configuration) 326 | } 327 | 328 | private let _makeStartHandle: (GradientHandleConfiguration) -> AnyView 329 | public func makeStartHandle(configuration: GradientHandleConfiguration) -> some View { 330 | return self._makeStartHandle(configuration) 331 | } 332 | 333 | private let _makeEndHandle: (GradientHandleConfiguration) -> AnyView 334 | public func makeEndHandle(configuration: GradientHandleConfiguration) -> some View { 335 | return self._makeEndHandle(configuration) 336 | } 337 | 338 | private let _makeStop: (GradientStopConfiguration) -> AnyView 339 | public func makeStop(configuration: GradientStopConfiguration) -> some View { 340 | return self._makeStop(configuration) 341 | } 342 | private let _makeBar: (RadialGradientBarConfiguration) -> AnyView 343 | public func makeBar(configuration: RadialGradientBarConfiguration) -> some View { 344 | return self._makeBar(configuration) 345 | } 346 | 347 | 348 | 349 | public init(_ style: ST) { 350 | self._makeGradient = style.makeGradientTypeErased 351 | self._makeCenter = style.makeCenterTypeErased 352 | self._makeStartHandle = style.makeStartHandleTypeErased 353 | self._makeEndHandle = style.makeEndHandleTypeErased 354 | self._makeStop = style.makeStopTypeErased 355 | self._makeBar = style.makeBarTypeErased 356 | } 357 | } 358 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 359 | public struct RadialGradientPickerStyleKey: EnvironmentKey { 360 | public static let defaultValue: AnyRadialGradientPickerStyle = AnyRadialGradientPickerStyle(DefaultRadialGradientPickerStyle()) 361 | } 362 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 363 | extension EnvironmentValues { 364 | public var radialGradientPickerStyle: AnyRadialGradientPickerStyle { 365 | get { 366 | return self[RadialGradientPickerStyleKey.self] 367 | } 368 | set { 369 | self[RadialGradientPickerStyleKey.self] = newValue 370 | } 371 | } 372 | } 373 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 374 | extension View { 375 | public func radialGradientPickerStyle(_ style: S) -> some View where S: RadialGradientPickerStyle { 376 | self.environment(\.radialGradientPickerStyle, AnyRadialGradientPickerStyle(style)) 377 | } 378 | } 379 | 380 | // MARK: Angular 381 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 382 | public protocol AngularGradientPickerStyle { 383 | associatedtype GradientView: View 384 | associatedtype Center: View 385 | associatedtype StartHandle: View 386 | associatedtype EndHandle: View 387 | associatedtype Stop: View 388 | 389 | func makeGradient(gradient: AngularGradient) -> Self.GradientView 390 | func makeCenter(configuration: GradientCenterConfiguration) -> Self.Center 391 | func makeStartHandle(configuration: GradientHandleConfiguration) -> Self.StartHandle 392 | func makeEndHandle(configuration: GradientHandleConfiguration) -> Self.EndHandle 393 | func makeStop(configuration: GradientStopConfiguration) -> Self.Stop 394 | 395 | } 396 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 397 | public extension AngularGradientPickerStyle { 398 | func makeGradientTypeErased(gradient: AngularGradient) -> AnyView { 399 | AnyView(self.makeGradient(gradient: gradient)) 400 | } 401 | func makeCenterTypeErased(configuration: GradientCenterConfiguration) -> AnyView { 402 | AnyView(self.makeCenter(configuration: configuration)) 403 | } 404 | func makeStartHandleTypeErased(configuration: GradientHandleConfiguration) -> AnyView { 405 | AnyView(self.makeStartHandle(configuration: configuration)) 406 | } 407 | func makeEndHandleTypeErased(configuration: GradientHandleConfiguration) -> AnyView { 408 | AnyView(self.makeEndHandle(configuration: configuration)) 409 | } 410 | func makeStopTypeErased(configuration: GradientStopConfiguration) -> AnyView { 411 | AnyView(self.makeStop(configuration: configuration)) 412 | } 413 | } 414 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 415 | public struct AnyAngularGradientPickerStyle: AngularGradientPickerStyle { 416 | private let _makeGradient: (AngularGradient) -> AnyView 417 | public func makeGradient(gradient: AngularGradient) -> some View { 418 | return self._makeGradient(gradient) 419 | } 420 | private let _makeCenter: (GradientCenterConfiguration) -> AnyView 421 | public func makeCenter(configuration: GradientCenterConfiguration) -> some View { 422 | return self._makeCenter(configuration) 423 | } 424 | 425 | private let _makeStartHandle: (GradientHandleConfiguration) -> AnyView 426 | public func makeStartHandle(configuration: GradientHandleConfiguration) -> some View { 427 | return self._makeStartHandle(configuration) 428 | } 429 | 430 | private let _makeEndHandle: (GradientHandleConfiguration) -> AnyView 431 | public func makeEndHandle(configuration: GradientHandleConfiguration) -> some View { 432 | return self._makeEndHandle(configuration) 433 | } 434 | 435 | private let _makeStop: (GradientStopConfiguration) -> AnyView 436 | public func makeStop(configuration: GradientStopConfiguration) -> some View { 437 | return self._makeStop(configuration) 438 | } 439 | 440 | 441 | public init(_ style: ST) { 442 | self._makeGradient = style.makeGradientTypeErased 443 | self._makeCenter = style.makeCenterTypeErased 444 | self._makeStartHandle = style.makeStartHandleTypeErased 445 | self._makeEndHandle = style.makeEndHandleTypeErased 446 | self._makeStop = style.makeStopTypeErased 447 | 448 | } 449 | } 450 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 451 | public struct AngularGradientPickerStyleKey: EnvironmentKey { 452 | public static let defaultValue: AnyAngularGradientPickerStyle = AnyAngularGradientPickerStyle(DefaultAngularGradientPickerStyle()) 453 | } 454 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 455 | extension EnvironmentValues { 456 | public var angularGradientPickerStyle: AnyAngularGradientPickerStyle { 457 | get { 458 | return self[AngularGradientPickerStyleKey.self] 459 | } 460 | set { 461 | self[AngularGradientPickerStyleKey.self] = newValue 462 | } 463 | } 464 | } 465 | @available(iOS 13.0, macOS 10.15, watchOS 6.0 , *) 466 | extension View { 467 | public func angularGradientPickerStyle(_ style: S) -> some View where S: AngularGradientPickerStyle { 468 | self.environment(\.angularGradientPickerStyle, AnyAngularGradientPickerStyle(style)) 469 | } 470 | } 471 | --------------------------------------------------------------------------------