1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
//! This file implements the CIELCH color space, a cylindrical transformation of CIELAB that uses //! chroma and hue instead of two opponent color axes. Be careful not to confuse this color with //! CIEHCL, which uses CIELUV internally. use super::cielabcolor::CIELABColor; use color::{Color, XYZColor}; use coord::Coord; use illuminants::Illuminant; /// A cylindrical form of CIELAB, analogous to the relationship between HSL and RGB. /// # Example /// /// ``` /// # use scarlet::prelude::*; /// # use scarlet::colors::CIELCHColor; /// // hue-shift red to yellow, keeping same brightness: really ends up to be brown /// let red = RGBColor{r: 0.7, g: 0.1, b: 0.1}; /// let red_lch: CIELCHColor = red.convert(); /// let mut yellow = red_lch; /// yellow.h = yellow.h + 40.; /// println!("{}", red.to_string()); /// println!("{}", yellow.convert::<RGBColor>().to_string()); /// // prints #B31A1A /// // #835000 /// ``` #[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub struct CIELCHColor { /// The luminance component, identical to CIELAB's and CIELUV's. Ranges between 0 and 100. pub l: f64, /// The chroma component. Chroma is defined as the difference from the grayscale color of the same /// luminance (in CIELAB, essentially the distance away from the line a = b = 0). It is /// perceptually uniform in the sense that a gradient of chroma looks visually /// even. Importantly, it is not linear with respect to additive color mixing: superimposing two /// colors that are not of the exact same hue will not add together their chromas. In the /// cylindrical space, this is equivalent to radius. It ranges from 0 to roughly 150 for most /// colors that are physically possible, although keep in mind that the space is not a cylinder /// and for most luminance values chroma ranges much smaller. pub c: f64, /// The hue component, in degrees. The least complicated and the most familiar: essentially the /// angle in cylindrical coordinates, it ranges from 0 degrees to 360. 90 degrees corresponds to /// yellow, 180 corresponds to green, 270 to blue, and 360 to red. pub h: f64, } impl Color for CIELCHColor { /// Converts from XYZ to LCH by way of CIELAB. fn from_xyz(xyz: XYZColor) -> CIELCHColor { // first get LAB coordinates let lab = CIELABColor::from_xyz(xyz); let l = lab.l; // the same in both spaces // now we have to do some math // radius is sqrt(a^2 + b^2) // angle is atan2(a, b) // Rust does this ez let c = lab.b.hypot(lab.a); // don't forget to convert to degrees let unbounded_h = lab.b.atan2(lab.a).to_degrees(); // and now add or subtract 360 to get within range (0, 360) // should only need to be done once let h = if unbounded_h < 0.0 { unbounded_h + 360.0 } else if unbounded_h > 360.0 { unbounded_h - 360.0 } else { unbounded_h }; CIELCHColor { l, c, h } } /// Converts from LCH back to XYZ by way of CIELAB, chromatically adapting it as CIELAB does. fn to_xyz(&self, illuminant: Illuminant) -> XYZColor { // go back to a and b // more math: a = c cos h, b = c sin h // Rust also has something for this which is hella cool let (sin, cos) = self.h.to_radians().sin_cos(); CIELABColor { l: self.l, a: self.c * cos, b: self.c * sin, } .to_xyz(illuminant) } } impl From<Coord> for CIELCHColor { fn from(c: Coord) -> CIELCHColor { CIELCHColor { l: c.x, c: c.y, h: c.z, } } } impl Into<Coord> for CIELCHColor { fn into(self) -> Coord { Coord { x: self.l, y: self.c, z: self.h, } } } #[cfg(test)] mod tests { #[allow(unused_imports)] use super::*; use consts::TEST_PRECISION; #[test] fn test_lch_xyz_conversion_same_illuminant() { let xyz = XYZColor { x: 0.2, y: 0.42, z: 0.23, illuminant: Illuminant::D50, }; let lch: CIELCHColor = xyz.convert(); let xyz2: XYZColor = lch.convert(); assert!(xyz2.approx_equal(&xyz)); assert!(xyz.distance(&xyz2) <= TEST_PRECISION); } #[test] fn test_lch_xyz_conversion_different_illuminant() { let xyz = XYZColor { x: 0.2, y: 0.42, z: 0.23, illuminant: Illuminant::D55, }; let lch: CIELCHColor = xyz.convert(); let xyz2: XYZColor = lch.convert(); assert!(xyz2.approx_visually_equal(&xyz)); assert!(xyz.distance(&xyz2) <= TEST_PRECISION); } }