|
1 | 1 |
|
2 |
| -// This struct is a namespace for static functions and properties which return known color maps. |
3 |
| -// All of these functions and properties also _return_ a `ColorMap`, |
4 |
| -// instances of which are used as an interface type |
5 |
| -// rather than existentials of `ColorMapProtocol` directly |
6 |
| -// (again, so users can have access to convenient, known color maps). |
7 |
| - |
| 2 | +/// An object which maps a value in the range `0...1` to a `Color`. |
| 3 | +/// |
| 4 | +/// This object wraps a conformer to `ColorMapProtocol`, and is used instead of `ColorMapProtocol` directly |
| 5 | +/// in order to provide a cleaner interface and to automatically clamp out-of-range values. |
| 6 | +/// Common color maps are available as static functions on this type, allowing code such as: |
| 7 | +/// `x.colorMap = .fiveColorHeatMap`. |
| 8 | +/// |
8 | 9 | public struct ColorMap: ColorMapProtocol {
|
9 |
| - var base: ColorMapProtocol |
10 |
| - |
11 |
| - public init(_ base: ColorMapProtocol) { |
12 |
| - if let existing = base as? ColorMap { self = existing } |
13 |
| - else { self.base = base } |
14 |
| - } |
15 |
| - |
16 |
| - public func colorForOffset(_ offset: Float) -> Color { |
17 |
| - return base.colorForOffset(offset) |
18 |
| - } |
| 10 | + var base: ColorMapProtocol |
| 11 | + |
| 12 | + public init(_ base: ColorMapProtocol) { |
| 13 | + // If base is already wrapped, do not re-wrap it. |
| 14 | + if let existing = base as? ColorMap { self = existing } |
| 15 | + else { self.base = base } |
| 16 | + } |
| 17 | + public func colorForOffset(_ offset: Double) -> Color { |
| 18 | + let offset = min(max(offset, 0), 1) |
| 19 | + return base.colorForOffset(offset) |
| 20 | + } |
19 | 21 | }
|
20 | 22 |
|
| 23 | +/// An object which maps a value in the range `0...1` to a `Color`. |
| 24 | +/// |
21 | 25 | public protocol ColorMapProtocol {
|
22 |
| - func colorForOffset(_ offset: Float) -> Color |
| 26 | + func colorForOffset(_ offset: Double) -> Color |
| 27 | +} |
| 28 | +extension ColorMapProtocol { |
| 29 | + func colorForOffset(_ offset: Float) -> Color { |
| 30 | + return colorForOffset(Double(offset)) |
| 31 | + } |
23 | 32 | }
|
24 | 33 |
|
25 |
| -// Linear Gradients. |
| 34 | +// MARK: - Color Transformations. |
26 | 35 |
|
27 |
| -struct LinearGradient: ColorMapProtocol { |
28 |
| - struct Stop { |
29 |
| - var color: Color |
30 |
| - var position: Double |
| 36 | +private struct ColorTransformer: ColorMapProtocol { |
| 37 | + var base: ColorMapProtocol |
| 38 | + var transform: (Color)->Color |
| 39 | + init(_ base: ColorMapProtocol, transform: @escaping (Color)->Color) { |
| 40 | + self.base = base; self.transform = transform |
| 41 | + } |
| 42 | + func colorForOffset(_ offset: Double) -> Color { |
| 43 | + return transform(base.colorForOffset(offset)) |
| 44 | + } |
| 45 | +} |
| 46 | + |
| 47 | +extension ColorMap { |
| 48 | + |
| 49 | + /// Returns a `ColorMap` whose output is transformed by the given closure. |
| 50 | + /// |
| 51 | + public func withTransform(_ transform: @escaping (Color)->Color) -> ColorMap { |
| 52 | + return ColorMap(ColorTransformer(base, transform: transform)) |
| 53 | + } |
| 54 | + |
| 55 | + /// Returns a `ColorMap` whose output colors' alpha components are given by `alpha`. |
| 56 | + /// |
| 57 | + public func withAlpha(_ alpha: Float) -> ColorMap { |
| 58 | + return withTransform { $0.withAlpha(alpha) } |
| 59 | + } |
| 60 | + |
| 61 | + /// Returns a `ColorMap` whose output colors are lightened by the given `amount`. |
| 62 | + /// |
| 63 | + public func lightened(by amount: Float) -> ColorMap { |
| 64 | + return withTransform { $0.linearBlend(with: .white, offset: amount) } |
| 65 | + } |
| 66 | + |
| 67 | + /// Returns a `ColorMap` whose output colors are darkened by the given `amount`. |
| 68 | + /// |
| 69 | + public func darkened(by amount: Float) -> ColorMap { |
| 70 | + return withTransform { $0.linearBlend(with: .black, offset: amount) } |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +// MARK: - Offset Transformations. |
| 75 | + |
| 76 | +private struct ColorMapOffsetTransformer: ColorMapProtocol { |
| 77 | + var base: ColorMapProtocol |
| 78 | + var transform: (Double)->Double |
| 79 | + init(_ base: ColorMapProtocol, transform: @escaping (Double)->Double) { |
| 80 | + self.base = base; self.transform = transform |
| 81 | + } |
| 82 | + func colorForOffset(_ offset: Double) -> Color { |
| 83 | + // Ensure that we don't transform the offset out of bounds. |
| 84 | + var transformedOffset = transform(offset) |
| 85 | + transformedOffset = min(max(transformedOffset, 0), 1) |
| 86 | + return base.colorForOffset(transformedOffset) |
| 87 | + } |
| 88 | +} |
| 89 | + |
| 90 | +extension ColorMap { |
31 | 91 |
|
32 |
| - init(_ color: Color, at pos: Double) { |
33 |
| - self.color = color; self.position = pos |
34 |
| - } |
35 |
| - } |
36 |
| - |
37 |
| - var stops: [Stop] |
38 |
| - |
39 |
| - init(stops: [Stop]) { |
40 |
| - self.stops = stops.sorted { $0.position < $1.position } |
41 |
| - } |
42 |
| - |
43 |
| - init(start: Color, end: Color) { |
44 |
| - self.init(stops: [Stop(start, at: 0), Stop(end, at: 1)]) |
45 |
| - } |
46 |
| - |
47 |
| - func colorForOffset(_ offset: Float) -> Color { |
48 |
| - return colorForOffset(Double(offset)) |
49 |
| - } |
50 |
| - |
51 |
| - func colorForOffset(_ offset: Double) -> Color { |
52 |
| - guard (0...1).contains(offset), |
53 |
| - let rightStopIdx = stops.firstIndex(where: { $0.position > offset }) else { |
54 |
| - return stops.last?.color ?? .black |
55 |
| - } |
56 |
| - let rightStop = stops[rightStopIdx] |
57 |
| - guard rightStopIdx > stops.startIndex else { return rightStop.color } |
58 |
| - let leftStop = stops[stops.index(before: rightStopIdx)] |
59 |
| - assert(leftStop.position <= offset) |
60 |
| - |
61 |
| - let distance = rightStop.position - leftStop.position |
62 |
| - guard distance > 0 else { return rightStop.color } |
| 92 | + private func withOffsetTransform(_ transform: @escaping (Double) -> Double) -> ColorMap { |
| 93 | + return ColorMap(ColorMapOffsetTransformer(base, transform: transform)) |
| 94 | + } |
63 | 95 |
|
64 |
| - let offset = Float((offset - leftStop.position) / distance) |
65 |
| - return leftStop.color.linearBlend(with: rightStop.color, offset: offset) |
66 |
| - } |
| 96 | + /// Returns a `ColorMap` whose output at offset `x` is equal to this `ColorMap`'s output at `1 - x`. |
| 97 | + /// |
| 98 | + public func reversed() -> ColorMap { |
| 99 | + return withOffsetTransform { 1 - $0 } |
| 100 | + } |
| 101 | +} |
| 102 | + |
| 103 | +// MARK: - Single Colors. |
| 104 | + |
| 105 | +private struct SingleColorMap: ColorMapProtocol { |
| 106 | + var color: Color |
| 107 | + func colorForOffset(_ offset: Double) -> Color { |
| 108 | + return color |
| 109 | + } |
67 | 110 | }
|
68 | 111 |
|
69 | 112 | extension ColorMap {
|
70 |
| - |
71 |
| - public static func linear(_ start: Color, _ end: Color) -> ColorMap { |
72 |
| - return ColorMap(LinearGradient(start: start, end: end)) |
73 |
| - } |
74 |
| - |
75 |
| - public static let fiveColorHeatMap = ColorMap(LinearGradient(stops: [ |
76 |
| - LinearGradient.Stop(Color(0, 0, 1, 1), at: 0), |
77 |
| - LinearGradient.Stop(Color(0, 1, 1, 1), at: 0.25), |
78 |
| - LinearGradient.Stop(Color(0, 1, 0, 1), at: 0.5), |
79 |
| - LinearGradient.Stop(Color(1, 1, 0, 1), at: 0.75), |
80 |
| - LinearGradient.Stop(Color(1, 0, 0, 1), at: 1), |
81 |
| - ])) |
82 |
| - |
83 |
| - public static let sevenColorHeatMap = ColorMap(LinearGradient(stops: [ |
84 |
| - LinearGradient.Stop(Color(0, 0, 0, 1), at: 0), |
85 |
| - LinearGradient.Stop(Color(0, 0, 1, 1), at: 0.1666666667), |
86 |
| - LinearGradient.Stop(Color(0, 1, 1, 1), at: 0.3333333333), |
87 |
| - LinearGradient.Stop(Color(0, 1, 0, 1), at: 0.5), |
88 |
| - LinearGradient.Stop(Color(1, 1, 0, 1), at: 0.6666666667), |
89 |
| - LinearGradient.Stop(Color(1, 0, 0, 1), at: 0.8333333333), |
90 |
| - LinearGradient.Stop(Color(1, 1, 1, 1), at: 1) |
91 |
| - ])) |
| 113 | + |
| 114 | + /// Returns a `ColorMap` which always returns the same color. |
| 115 | + /// |
| 116 | + public static func color(_ color: Color) -> ColorMap { |
| 117 | + return ColorMap(SingleColorMap(color: color)) |
| 118 | + } |
| 119 | +} |
| 120 | + |
| 121 | +// MARK: - Linear Gradients. |
| 122 | + |
| 123 | +/// A position along a gradient. |
| 124 | +/// |
| 125 | +public struct GradientStop { |
| 126 | + public var color: Color |
| 127 | + public var position: Double |
| 128 | + public init(_ color: Color, at position: Double) { |
| 129 | + self.color = color; self.position = position |
| 130 | + } |
92 | 131 | }
|
93 | 132 |
|
94 |
| -// Transforming. |
95 |
| - |
96 |
| -struct ColorTransformer<T>: ColorMapProtocol where T: ColorMapProtocol { |
97 |
| - var base: T |
98 |
| - var transform: (inout Color)->Void |
99 |
| - init(_ base: T, transform: @escaping (inout Color)->Void) { |
100 |
| - self.base = base; self.transform = transform |
101 |
| - } |
102 |
| - func colorForOffset(_ offset: Float) -> Color { |
103 |
| - var color = base.colorForOffset(offset) |
104 |
| - transform(&color) |
105 |
| - return color |
106 |
| - } |
| 133 | +private struct LinearGradient: ColorMapProtocol { |
| 134 | + var stops: [GradientStop] |
| 135 | + |
| 136 | + init(stops: [GradientStop]) { |
| 137 | + self.stops = stops.sorted { $0.position < $1.position } |
| 138 | + } |
| 139 | + init(start: Color, end: Color) { |
| 140 | + self.init(stops: [GradientStop(start, at: 0), GradientStop(end, at: 1)]) |
| 141 | + } |
| 142 | + func colorForOffset(_ offset: Double) -> Color { |
| 143 | + guard let rightStopIdx = stops.firstIndex(where: { $0.position > offset }) else { |
| 144 | + return stops.last?.color ?? .black |
| 145 | + } |
| 146 | + let rightStop = stops[rightStopIdx] |
| 147 | + guard rightStopIdx > stops.startIndex else { return rightStop.color } |
| 148 | + let leftStop = stops[stops.index(before: rightStopIdx)] |
| 149 | + assert(leftStop.position <= offset) |
| 150 | + |
| 151 | + let distance = rightStop.position - leftStop.position |
| 152 | + guard distance > 0 else { return rightStop.color } |
| 153 | + |
| 154 | + let offset = (offset - leftStop.position) / distance |
| 155 | + return leftStop.color.linearBlend(with: rightStop.color, offset: Float(offset)) |
| 156 | + } |
107 | 157 | }
|
108 | 158 |
|
109 | 159 | extension ColorMap {
|
110 |
| - |
111 |
| - public func withAlpha(_ alpha: Float) -> ColorMap { |
112 |
| - return ColorMap(ColorTransformer(self) { $0.a = alpha }) |
113 |
| - } |
114 |
| - |
115 |
| - public func lightened(by: Float) -> ColorMap { |
116 |
| - return ColorMap(ColorTransformer(self) { $0 = $0.linearBlend(with: .white, offset: by) }) |
117 |
| - } |
118 |
| - |
119 |
| - public func darkened(by: Float) -> ColorMap { |
120 |
| - return ColorMap(ColorTransformer(self) { $0 = $0.linearBlend(with: .black, offset: by) }) |
121 |
| - } |
| 160 | + |
| 161 | + /// Returns a `ColorMap` whose output is a linear gradient with the given stops. |
| 162 | + /// |
| 163 | + public static func linearGradient(_ stops: [GradientStop]) -> ColorMap { |
| 164 | + return ColorMap(LinearGradient(stops: stops)) |
| 165 | + } |
| 166 | + |
| 167 | + /// Returns a `ColorMap` whose output is a linear gradient between the given colors. |
| 168 | + /// |
| 169 | + public static func linearGradient(_ start: Color, _ end: Color) -> ColorMap { |
| 170 | + return ColorMap(LinearGradient(start: start, end: end)) |
| 171 | + } |
| 172 | + |
| 173 | + /// A standard, five-color heat map. |
| 174 | + /// |
| 175 | + public static let fiveColorHeatMap = ColorMap.linearGradient([ |
| 176 | + GradientStop(Color(0, 0, 1, 1), at: 0), |
| 177 | + GradientStop(Color(0, 1, 1, 1), at: 0.25), |
| 178 | + GradientStop(Color(0, 1, 0, 1), at: 0.5), |
| 179 | + GradientStop(Color(1, 1, 0, 1), at: 0.75), |
| 180 | + GradientStop(Color(1, 0, 0, 1), at: 1), |
| 181 | + ]) |
| 182 | + |
| 183 | + /// A standard, seven-color heat map. |
| 184 | + /// |
| 185 | + public static let sevenColorHeatMap = ColorMap.linearGradient([ |
| 186 | + GradientStop(Color(0, 0, 0, 1), at: 0), |
| 187 | + GradientStop(Color(0, 0, 1, 1), at: 0.1666666667), |
| 188 | + GradientStop(Color(0, 1, 1, 1), at: 0.3333333333), |
| 189 | + GradientStop(Color(0, 1, 0, 1), at: 0.5), |
| 190 | + GradientStop(Color(1, 1, 0, 1), at: 0.6666666667), |
| 191 | + GradientStop(Color(1, 0, 0, 1), at: 0.8333333333), |
| 192 | + GradientStop(Color(1, 1, 1, 1), at: 1) |
| 193 | + ]) |
122 | 194 | }
|
| 195 | + |
0 commit comments