use std::f64::EPSILON;
use std::str::FromStr;
use bound::Bound;
use color::{Color, RGBColor, XYZColor};
use coord::Coord;
use csscolor::{parse_hsl_hsv_tuple, CSSParseError};
use illuminants::Illuminant;
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct HSVColor {
pub h: f64,
pub s: f64,
pub v: f64,
}
impl Color for HSVColor {
fn from_xyz(xyz: XYZColor) -> HSVColor {
let rgb = RGBColor::from_xyz(xyz);
let components = [rgb.r, rgb.g, rgb.b];
let max_c = components.iter().cloned().fold(-1.0, f64::max);
let min_c = components.iter().cloned().fold(2.0, f64::min);
let chroma = max_c - min_c;
let hue = if chroma == 0.0 {
0.0
} else if (max_c - rgb.r).abs() < EPSILON {
(((rgb.g - rgb.b) / chroma) % 6.0) * 60.0
} else if (max_c - rgb.g).abs() < EPSILON {
((rgb.b - rgb.r) / chroma) * 60.0 + 120.0
} else {
((rgb.r - rgb.g) / chroma) * 60.0 + 240.0
};
let value = max_c;
let saturation = if value == 0.0 {
0.0
} else {
chroma / value
};
HSVColor {
h: hue,
s: saturation,
v: value,
}
}
fn to_xyz(&self, illuminant: Illuminant) -> XYZColor {
let chroma = self.s * self.v;
let x = chroma * (1.0 - ((self.h / 60.0) % 2.0 - 1.0).abs());
let (r1, g1, b1) = if self.h <= 60.0 {
(chroma, x, 0.0)
} else if self.h <= 120.0 {
(x, chroma, 0.0)
} else if self.h <= 180.0 {
(0.0, chroma, x)
} else if self.h <= 240.0 {
(0.0, x, chroma)
} else if self.h <= 300.0 {
(x, 0.0, chroma)
} else {
(chroma, 0.0, x)
};
let offset = self.v - chroma;
let r = r1 + offset;
let g = g1 + offset;
let b = b1 + offset;
RGBColor { r, g, b }.to_xyz(illuminant)
}
}
impl From<Coord> for HSVColor {
fn from(c: Coord) -> HSVColor {
HSVColor {
h: c.x,
s: c.y,
v: c.z,
}
}
}
impl From<HSVColor> for Coord {
fn from(val: HSVColor) -> Self {
Coord {
x: val.h,
y: val.s,
z: val.v,
}
}
}
impl Bound for HSVColor {
fn bounds() -> [(f64, f64); 3] {
[(0., 360.), (0., 1.), (0., 1.)]
}
}
impl FromStr for HSVColor {
type Err = CSSParseError;
fn from_str(s: &str) -> Result<HSVColor, CSSParseError> {
if !s.starts_with("hsv(") {
return Err(CSSParseError::InvalidColorSyntax);
}
let tup: String = s.chars().skip(3).collect::<String>();
match parse_hsl_hsv_tuple(&tup) {
Ok(res) => Ok(HSVColor {
h: res.0,
s: res.1,
v: res.2,
}),
Err(_e) => Err(_e),
}
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[test]
fn test_hsl_rgb_conversion() {
let red_rgb = RGBColor {
r: 1.,
g: 0.,
b: 0.,
};
let red_hsv: HSVColor = red_rgb.convert();
assert!(red_hsv.h.abs() <= 0.0001);
assert!((red_hsv.s - 1.0) <= 0.0001);
assert!((red_hsv.v - 1.0) <= 0.0001);
let lavender_hsv = HSVColor {
h: 243.5,
s: 0.568,
v: 0.925,
};
let lavender_rgb: RGBColor = lavender_hsv.convert();
assert_eq!(lavender_rgb.to_string(), "#6E66EC");
}
#[test]
fn test_hsv_string_parsing() {
let red_hsv: HSVColor = "hsv(0, 120%, 50%)".parse().unwrap();
assert!(red_hsv.h.abs() <= 0.0001);
assert!((red_hsv.s - 1.0) <= 0.0001);
assert!((red_hsv.v - 0.5) <= 0.0001);
let lavender_hsv: HSVColor = "hsv(-445, 24%, 1000%)".parse().unwrap();
let lavender_rgb: RGBColor = lavender_hsv.convert();
assert_eq!(lavender_rgb.to_string(), "#E6C2FF");
assert!("hsv(254%, 0, 0)".parse::<HSVColor>().is_err());
}
}