) -> 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 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
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 |
98 |
99 |
100 |
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 |
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 |
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 |
125 |
126 |
127 | ### Hue Slider
128 |
129 | A styled `LSlider` with a background gradient representing a full hue rotation.
130 |
131 |
132 |
133 |
134 | ### CMYK Sliders
135 |
136 | Conceptually similar to the RGB Sliders. These represent the values of a CMYK color
137 |
138 |
139 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------