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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
//! This module implements the CIELUV color specification, which was adopted concurrently with
//! CIELAB. CIELUV is very similar to CIELAB, but with the difference that u and v are roughly
//! equivalent to red and green and luminance is then used to calculate the blue part.

use color::{Color, XYZColor};
use coord::Coord;
use illuminants::Illuminant;

/// A similar color system to CIELAB, adapted at the same time and with similar goals. It attempts to
/// be an easy-to-convert color space from XYZ that approaches perceptual uniformity. U and V
/// represent chromaticity and roughly equate to CIELAB's A and B, but they're scaled differently and
/// act slightly differently. These coordinates are often referred to as the CIE 1976 UCS (uniform
/// chromaticity scale) diagram, and they're good descriptors of chromaticity.
/// # Example
///
/// ```
/// # use scarlet::prelude::*;
/// # use scarlet::colors::{CIELUVColor};
/// # use scarlet::color::XYZColor;
/// // D50 is the implied illuminant and white point
/// let white: CIELUVColor = XYZColor::white_point(Illuminant::D50).convert();
/// assert_eq!(white.l, 100.);
/// assert_eq!(white.u, 0.);
/// assert_eq!(white.v, 0.);
/// ```
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct CIELUVColor {
    /// The luminance component of LUV. Ranges from 0 to 100 by definition.
    pub l: f64,
    /// The component of LUV that roughly equates to how red the color is vs. how green it is. Ranges
    /// from 0 to 100 in most visible colors, where 0 is bright green and 100 is bright red.
    pub u: f64,
    /// The component of LUV that roughly equates to how yellow vs. blue the color is. Ranges from 0 to
    /// 100 in most visible colors, where 0 is bright blue and 100 is bright yellow.
    pub v: f64,
}

impl Color for CIELUVColor {
    /// Given an XYZ color, gets a new CIELUV color. This is CIELUV D50, so anything else is
    /// chromatically adapted before conversion.
    fn from_xyz(xyz: XYZColor) -> CIELUVColor {
        // this is not bad: LUV is meant to be easy from XYZ
        // https://en.wikipedia.org/wiki/CIELUV

        // do u and v chromaticity conversions on whitepoint and on given color
        // because cieluv chromatic adaptation sucks, use the good one
        let xyz_c = xyz.color_adapt(Illuminant::D50);
        let wp = XYZColor::white_point(Illuminant::D50);
        let denom = |color: XYZColor| color.x + 15.0 * color.y + 3.0 * color.z;
        let u_func = |color: XYZColor| 4.0 * color.x / denom(color);
        let v_func = |color: XYZColor| 9.0 * color.y / denom(color);

        let u_prime_n = u_func(wp);
        let v_prime_n = v_func(wp);

        let u_prime = u_func(xyz_c);
        let v_prime = v_func(xyz_c);

        let delta: f64 = 6.0 / 29.0; // like CIELAB

        // technically this next division should do nothing: idk if it gets factored out at compile
        // time, but it's just insurance if someone ever decides not to normalize whitepoints to Y=1
        let y_scaled = xyz_c.y / wp.y; // ranges from 0-1
        let l = if y_scaled <= delta.powf(3.0) {
            (2.0 / delta).powf(3.0) * y_scaled
        } else {
            116.0 * y_scaled.powf(1.0 / 3.0) - 16.0
        };

        let u = 13.0 * l * (u_prime - u_prime_n);
        let v = 13.0 * l * (v_prime - v_prime_n);
        CIELUVColor { l, u, v }
    }
    /// Returns a new `XYZColor` that matches the given color. Note that Scarlet uses CIELUV D50 to
    /// get around compatibility issues, so any other illuminant will be chromatically adapted after
    /// initial conversion (using the `color_adapt()` function).
    fn to_xyz(&self, illuminant: Illuminant) -> XYZColor {
        // https://en.wikipedia.org/wiki/CIELUV literally has the equations in order
        // pretty straightforward
        let wp = XYZColor::white_point(Illuminant::D50);
        let denom = |color: XYZColor| color.x + 15.0 * color.y + 3.0 * color.z;
        let u_func = |color: XYZColor| 4.0 * color.x / denom(color);
        let v_func = |color: XYZColor| 9.0 * color.y / denom(color);
        let u_prime_n = u_func(wp);
        let v_prime_n = v_func(wp);

        let u_prime = self.u / (13.0 * self.l) + u_prime_n;
        let v_prime = self.v / (13.0 * self.l) + v_prime_n;

        let delta: f64 = 6.0 / 29.0;

        let y = if self.l <= 8.0 {
            wp.y * self.l * (delta / 2.0).powf(3.0)
        } else {
            wp.y * ((self.l + 16.0) / 116.0).powf(3.0)
        };

        let x = y * 9.0 * u_prime / (4.0 * v_prime);
        let z = y * (12.0 - 3.0 * u_prime - 20.0 * v_prime) / (4.0 * v_prime);
        XYZColor {
            x,
            y,
            z,
            illuminant: Illuminant::D50,
        }
        .color_adapt(illuminant)
    }
}

impl From<Coord> for CIELUVColor {
    fn from(c: Coord) -> CIELUVColor {
        CIELUVColor {
            l: c.x,
            u: c.y,
            v: c.z,
        }
    }
}

impl From<CIELUVColor> for Coord {
    fn from(val: CIELUVColor) -> Self {
        Coord {
            x: val.l,
            y: val.u,
            z: val.v,
        }
    }
}

#[cfg(test)]
mod tests {
    #[allow(unused_imports)]
    use super::*;
    use consts::TEST_PRECISION;

    #[test]
    fn test_cieluv_xyz_conversion_d50() {
        let xyz = XYZColor {
            x: 0.3,
            y: 0.53,
            z: 0.65,
            illuminant: Illuminant::D50,
        };
        let luv: CIELUVColor = xyz.convert();
        let xyz2: XYZColor = luv.convert();
        assert!(xyz2.approx_equal(&xyz));
        assert!(xyz.distance(&xyz2) <= TEST_PRECISION);
    }

    #[test]
    fn test_cieluv_xyz_conversion_d65() {
        let xyz = XYZColor {
            x: 0.3,
            y: 0.53,
            z: 0.65,
            illuminant: Illuminant::D65,
        };
        let luv: CIELUVColor = xyz.convert();
        let xyz2: XYZColor = luv.convert();
        assert!(xyz2.approx_visually_equal(&xyz));
        assert!(xyz.distance(&xyz2) <= TEST_PRECISION);
    }
}