cube_helix/lib.rs
1//! Dave Green's 'cubehelix' colour scheme.
2//! See cubehelix homepage at:
3//! https://www.mrao.cam.ac.uk/~dag/CUBEHELIX/
4//! Calculation from:
5//! http://astron-soc.in/bulletin/11June/289392011.pdf
6//! # Examples
7//! ```
8//! use cube_helix::CubeHelix;
9//! # fn main() {
10//! // Get default values
11//! let ch: CubeHelix = Default::default();
12//! // Returns color white: (255,255,255)
13//! let colors = ch.get_color(1.0);
14//! # }
15//! ```
16
17use std::f64::consts::PI;
18
19// RGB perceived intensity const
20/// Red perceived intensity cons
21const RPIC: (f64, f64) = (-0.14861, 1.78277);
22/// Green perceived intensity cons
23const GPIC: (f64, f64) = (-0.29227, -0.90649);
24/// Blue perceived intensity cons
25const BPIC: (f64, f64) = (1.97294, 0.0);
26
27/// # Examples
28/// Using default values:
29/// ```
30/// use cube_helix::CubeHelix;
31/// # fn main() {
32/// // Get default values
33/// let ch: CubeHelix = Default::default();
34/// // Returns color white: (255,255,255)
35/// let color = ch.get_color(1.0);
36/// assert_eq!(color.0, 255);
37/// assert_eq!(color.1, 255);
38/// assert_eq!(color.2, 255);
39/// # }
40/// ```
41/// Using defaults partially:
42/// ```
43/// use cube_helix::CubeHelix;
44/// // Override 'start' and 'rotation' values
45/// let ch = CubeHelix { start: 0.2, rotations: 1.5, ..Default::default() };
46/// // Returns color black: (0,0,0)
47/// let color = ch.get_color(0.0);
48/// assert_eq!(color.0, 0);
49/// assert_eq!(color.1, 0);
50/// assert_eq!(color.2, 0);
51/// ```
52#[derive(Debug)]
53pub struct CubeHelix {
54 /// Gamma factor.
55 pub gamma: f64,
56 /// Starting angle: 0.5 -> purple
57 pub start: f64,
58 /// Rotations (1.5 : R -> G -> B -> R).
59 /// Negative value will 'spin' in opposite direction.
60 pub rotations: f64,
61 /// Value from 0..1 to control saturation.
62 /// Zero value is completelly grayscale.
63 pub saturation: f64,
64 /// Lowest value in value range. This value represents black.
65 pub min: f64,
66 /// Highest value in value range. This value represents white.
67 pub max: f64,
68}
69
70/// Default values to produce the same gradient as D.A Green's paper in Figure 1.
71/// gamma: 1.0
72/// start: 0.5
73/// rotations: -1.5
74/// saturation: 1.0
75/// min: 0.0
76/// max: 1.0
77impl Default for CubeHelix {
78 fn default() -> CubeHelix {
79 CubeHelix {
80 gamma: 1.0,
81 start: 0.5,
82 rotations: -1.5,
83 saturation: 1.0,
84 min: 0.0,
85 max: 1.0,
86 }
87 }
88}
89
90/// Adds color calculation to CubeHelix struct
91impl CubeHelix {
92 /// Calculates CubeHelix color for given value.
93 /// Value must be in the min..max range.
94 /// Returns a tuple with three values: (red: u8, green: u8, blue: u8).
95 /// # Examples
96 /// ```
97 /// use cube_helix::CubeHelix;
98 /// # fn main() {
99 /// // Use range 0..100 - defalts otherwise
100 /// let ch = CubeHelix { min: 0.0, max: 100.0, ..Default::default() };
101 /// // Get color in the middle. Returns color: (174,97,158)
102 /// let color = ch.get_color(50.0);
103 /// assert_eq!(color.0, 174);
104 /// assert_eq!(color.1, 97);
105 /// assert_eq!(color.2, 158);
106 /// # }
107 /// ```
108 pub fn get_color(&self, value: f64) -> (u8, u8, u8) {
109 let rgb: (f64, f64, f64) = calc(self, value);
110 ((rgb.0 * 255.0) as u8, (rgb.1 * 255.0) as u8, (rgb.2 * 255.0) as u8)
111 }
112}
113
114/// Use cubehelix color calculation without CubeHelix struct.
115/// Returns a tuple with three values (red: u8, green: u8, blue: u8)
116/// # Examples
117/// ```
118/// use cube_helix::color;
119/// # fn main() {
120/// // Returns color (181, 104, 101)
121/// let color = color(1.0, 0.2, -1.5, 1.0, 0.0, 1.0, 0.5);
122/// assert_eq!(color.0, 181);
123/// assert_eq!(color.1, 104);
124/// assert_eq!(color.2, 101);
125/// # }
126/// ```
127pub fn color(
128 gamma: f64,
129 start: f64,
130 rotations: f64,
131 saturation: f64,
132 min: f64,
133 max: f64,
134 value: f64,
135) -> (u8, u8, u8) {
136 let rgb: (f64, f64, f64) = calc(
137 &CubeHelix {gamma, start, rotations, saturation, min, max},
138 value,
139 );
140 ((rgb.0 * 255.0) as u8, (rgb.1 * 255.0) as u8, (rgb.2 * 255.0) as u8)
141}
142
143fn calc(cube_helix: &CubeHelix, value: f64) -> (f64, f64, f64) {
144 // normalize value to min-max range
145 let x: f64 = normalize(value, cube_helix.min, cube_helix.max);
146 // Apply gamma factor to emphasise low or high intensity values
147 let lambda: f64 = x * cube_helix.gamma;
148 // Calculate amplitude of deviation
149 let amplitude: f64 = amplitude(lambda, cube_helix.saturation);
150 // Calculate angle of deviation
151 let phi: f64 = phi(cube_helix, value);
152
153 // Calculate rgb values
154 let red: f64 = color_calc(lambda, amplitude, phi, RPIC);
155 let green: f64 = color_calc(lambda, amplitude, phi, GPIC);
156 let blue: f64 = color_calc(lambda, amplitude, phi, BPIC);
157
158 (red, green, blue)
159}
160
161// lambda: given value normalized and gamma corrected
162// (value between 0-1 via normalized)
163// amplitude: deviation of amplitude in the black and white line
164// phi: deviation of angle in the black and white diagonal line
165// color_const: perceived intesity constant tuple for red, green or blue
166fn color_calc(lambda: f64, amplitude: f64, phi: f64, color_const: (f64, f64)) -> f64 {
167 lambda + amplitude * (color_const.0 * phi.cos() + color_const.1 * phi.sin())
168}
169
170// phi: deviation of angle in the black and white diagonal line
171// φ = 2π(s/3 + rλ)
172fn phi(cube_helix: &CubeHelix, value: f64) -> f64 {
173 2.0 * PI * (cube_helix.start / 3.0 + cube_helix.rotations * value)
174}
175
176// amplitude: deviation of amplitude in the black and white line
177//
178// Calculate amplitude and angle of deviation from the black
179// to white diagonal in the plane of constant
180// perceived intensity.
181// a = hλ(1 − λ)/2
182fn amplitude(lambda: f64, saturation: f64) -> f64 {
183 saturation * lambda * (1.0 - lambda) / 2.0
184}
185
186// Normalize value in the range of min..max to 0..1
187fn normalize(value: f64, min: f64, max: f64) -> f64 {
188 if value < min || value > max {
189 panic!("Value: {:?} not in range: {:?} - {:?}", value, min, max);
190 }
191 if min > max {
192 panic!("Incorrect range: min > max");
193 }
194 if (min - max).signum() == 0.0 {
195 panic!("Incorrect range: {:?} - {:?}", min, max);
196 }
197 (value - min) / (max - min)
198}
199
200//Unit tests for functions and structs in this file.
201#[cfg(test)]
202mod cube_helix_tests {
203 use super::*;
204
205 // Struct tests
206 #[test]
207 fn defaults() {
208 let ch: CubeHelix = Default::default();
209 assert_eq!(ch.gamma, 1.0);
210 assert_eq!(ch.start, 0.5);
211 assert_eq!(ch.rotations, -1.5);
212 assert_eq!(ch.saturation, 1.0);
213 assert_eq!(ch.min, 0.0);
214 assert_eq!(ch.max, 1.0);
215 }
216
217 #[test]
218 fn partial_defaults() {
219 let ch = CubeHelix {
220 start: 0.2,
221 rotations: 1.5,
222 ..Default::default()
223 };
224 assert_eq!(ch.gamma, 1.0);
225 assert_eq!(ch.start, 0.2);
226 assert_eq!(ch.rotations, 1.5);
227 assert_eq!(ch.saturation, 1.0);
228 assert_eq!(ch.min, 0.0);
229 assert_eq!(ch.max, 1.0);
230 }
231
232 #[test]
233 fn odd_start_and_rotations() {
234 let ch = CubeHelix {
235 start: 77.2,
236 rotations: -21.5,
237 ..Default::default()
238 };
239 let color = ch.get_color(0.3);
240 assert_eq!(color.0, 124);
241 assert_eq!(color.1, 54);
242 assert_eq!(color.2, 65);
243
244 }
245
246 // Color call tests
247 #[test]
248 fn partial_fn_call() {
249 let color = color(1.0, 0.2, -1.5, 1.0, 0.0, 1.0, 0.5);
250 assert_eq!(color.0, 181);
251 assert_eq!(color.1, 104);
252 assert_eq!(color.2, 101);
253 }
254
255 #[test]
256 fn max_is_white() {
257 let ch: CubeHelix = Default::default();
258 let colors = ch.get_color(1.0);
259 assert_eq!(colors.0, 255);
260 assert_eq!(colors.1, 255);
261 assert_eq!(colors.2, 255);
262 }
263
264 #[test]
265 fn min_is_black() {
266 let ch: CubeHelix = Default::default();
267 let colors = ch.get_color(0.0);
268 assert_eq!(colors.0, 0);
269 assert_eq!(colors.1, 0);
270 assert_eq!(colors.2, 0);
271 }
272
273 // Tests for normalize
274 #[test]
275 fn normalize_with_defaults() {
276 assert_eq!(normalize(0.5, 0.0, 1.0), 0.5);
277 assert_eq!(normalize(0.2, 0.0, 1.0), 0.2);
278 assert_eq!(normalize(0.0, 0.0, 1.0), 0.0);
279 assert_eq!(normalize(1.0, 0.0, 1.0), 1.0);
280 }
281
282 // Panic: if user tries to normalize value that is not in his min-max range,
283 // the given rgb value would most likely be unwanted.
284 #[test]
285 #[should_panic]
286 fn normalize_with_value_not_in_range() {
287 assert_eq!(normalize(-0.5, 0.0, 1.0), 666.0);
288 }
289
290 #[test]
291 fn normalize_with_negative_to_positive_range() {
292 assert_eq!(normalize(0.0, -0.5, 0.5), 0.5);
293 assert_eq!(normalize(0.5, -0.5, 0.5), 1.0);
294 assert_eq!(normalize(-0.5, -0.5, 0.5), 0.0);
295 assert_eq!(normalize(0.2, -0.5, 0.5), 0.7);
296 }
297
298 // It is better to panic rather than have implicit behaviour.
299 // If min and max value are the same: division with zero value would occur
300 // Possible implicit behaviour could be just to return value 1.0.
301 #[test]
302 #[should_panic]
303 fn normalize_with_incorrect_range() {
304 assert_eq!(normalize(1.1, 1.1, 1.1), 0.0);
305 }
306}