use std::f64::consts::PI;
const RPIC: (f64, f64) = (-0.14861, 1.78277);
const GPIC: (f64, f64) = (-0.29227, -0.90649);
const BPIC: (f64, f64) = (1.97294, 0.0);
#[derive(Debug)]
pub struct CubeHelix {
pub gamma: f64,
pub start: f64,
pub rotations: f64,
pub saturation: f64,
pub min: f64,
pub max: f64,
}
impl Default for CubeHelix {
fn default() -> CubeHelix {
CubeHelix {
gamma: 1.0,
start: 0.5,
rotations: -1.5,
saturation: 1.0,
min: 0.0,
max: 1.0,
}
}
}
impl CubeHelix {
pub fn get_color(&self, value: f64) -> (u8, u8, u8) {
let rgb: (f64, f64, f64) = calc(self, value);
((rgb.0 * 255.0) as u8, (rgb.1 * 255.0) as u8, (rgb.2 * 255.0) as u8)
}
}
pub fn color(
gamma: f64,
start: f64,
rotations: f64,
saturation: f64,
min: f64,
max: f64,
value: f64,
) -> (u8, u8, u8) {
let rgb: (f64, f64, f64) = calc(
&CubeHelix {gamma, start, rotations, saturation, min, max},
value,
);
((rgb.0 * 255.0) as u8, (rgb.1 * 255.0) as u8, (rgb.2 * 255.0) as u8)
}
fn calc(cube_helix: &CubeHelix, value: f64) -> (f64, f64, f64) {
let x: f64 = normalize(value, cube_helix.min, cube_helix.max);
let lambda: f64 = x * cube_helix.gamma;
let amplitude: f64 = amplitude(lambda, cube_helix.saturation);
let phi: f64 = phi(cube_helix, value);
let red: f64 = color_calc(lambda, amplitude, phi, RPIC);
let green: f64 = color_calc(lambda, amplitude, phi, GPIC);
let blue: f64 = color_calc(lambda, amplitude, phi, BPIC);
(red, green, blue)
}
fn color_calc(lambda: f64, amplitude: f64, phi: f64, color_const: (f64, f64)) -> f64 {
lambda + amplitude * (color_const.0 * phi.cos() + color_const.1 * phi.sin())
}
fn phi(cube_helix: &CubeHelix, value: f64) -> f64 {
2.0 * PI * (cube_helix.start / 3.0 + cube_helix.rotations * value)
}
fn amplitude(lambda: f64, saturation: f64) -> f64 {
saturation * lambda * (1.0 - lambda) / 2.0
}
fn normalize(value: f64, min: f64, max: f64) -> f64 {
if value < min || value > max {
panic!("Value: {:?} not in range: {:?} - {:?}", value, min, max);
}
if min > max {
panic!("Incorrect range: min > max");
}
if (min - max).signum() == 0.0 {
panic!("Incorrect range: {:?} - {:?}", min, max);
}
(value - min) / (max - min)
}
#[cfg(test)]
mod cube_helix_tests {
use super::*;
#[test]
fn defaults() {
let ch: CubeHelix = Default::default();
assert_eq!(ch.gamma, 1.0);
assert_eq!(ch.start, 0.5);
assert_eq!(ch.rotations, -1.5);
assert_eq!(ch.saturation, 1.0);
assert_eq!(ch.min, 0.0);
assert_eq!(ch.max, 1.0);
}
#[test]
fn partial_defaults() {
let ch = CubeHelix {
start: 0.2,
rotations: 1.5,
..Default::default()
};
assert_eq!(ch.gamma, 1.0);
assert_eq!(ch.start, 0.2);
assert_eq!(ch.rotations, 1.5);
assert_eq!(ch.saturation, 1.0);
assert_eq!(ch.min, 0.0);
assert_eq!(ch.max, 1.0);
}
#[test]
fn odd_start_and_rotations() {
let ch = CubeHelix {
start: 77.2,
rotations: -21.5,
..Default::default()
};
let color = ch.get_color(0.3);
assert_eq!(color.0, 124);
assert_eq!(color.1, 54);
assert_eq!(color.2, 65);
}
#[test]
fn partial_fn_call() {
let color = color(1.0, 0.2, -1.5, 1.0, 0.0, 1.0, 0.5);
assert_eq!(color.0, 181);
assert_eq!(color.1, 104);
assert_eq!(color.2, 101);
}
#[test]
fn max_is_white() {
let ch: CubeHelix = Default::default();
let colors = ch.get_color(1.0);
assert_eq!(colors.0, 255);
assert_eq!(colors.1, 255);
assert_eq!(colors.2, 255);
}
#[test]
fn min_is_black() {
let ch: CubeHelix = Default::default();
let colors = ch.get_color(0.0);
assert_eq!(colors.0, 0);
assert_eq!(colors.1, 0);
assert_eq!(colors.2, 0);
}
#[test]
fn normalize_with_defaults() {
assert_eq!(normalize(0.5, 0.0, 1.0), 0.5);
assert_eq!(normalize(0.2, 0.0, 1.0), 0.2);
assert_eq!(normalize(0.0, 0.0, 1.0), 0.0);
assert_eq!(normalize(1.0, 0.0, 1.0), 1.0);
}
#[test]
#[should_panic]
fn normalize_with_value_not_in_range() {
assert_eq!(normalize(-0.5, 0.0, 1.0), 666.0);
}
#[test]
fn normalize_with_negative_to_positive_range() {
assert_eq!(normalize(0.0, -0.5, 0.5), 0.5);
assert_eq!(normalize(0.5, -0.5, 0.5), 1.0);
assert_eq!(normalize(-0.5, -0.5, 0.5), 0.0);
assert_eq!(normalize(0.2, -0.5, 0.5), 0.7);
}
#[test]
#[should_panic]
fn normalize_with_incorrect_range() {
assert_eq!(normalize(1.1, 1.1, 1.1), 0.0);
}
}