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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
//! This file implements most of the standard color functions that essentially work on 3D space,
//! including Euclidean distance, midpoints, and more. All of these methods work on
//! [`Color`](color/trait.Color.html) types that implement `Into<Coord>` and `From<Coord>`, and some
//! don't require `From<Coord>`. This makes it easy to provide these for custom
//! [`Color`](color/trait.Color.html) types.

use super::geo::prelude::*;
use super::geo::{Closest, LineString, Point};
use color::{Color, XYZColor};
use colors::cieluvcolor::CIELUVColor;
use coord::Coord;
use visual_gamut::read_cie_spectral_data;

/// Some errors that might pop up when dealing with colors as coordinates.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ColorCalcError {
    /// Returned when the number of weights given and the number of colors being averaged differ.
    MismatchedWeights,
}

/// A trait that indicates that the current Color can be embedded in 3D space. This also requires
/// `Clone` and `Copy`: there shouldn't be any necessary information outside of the coordinate data.
pub trait ColorPoint: Color + Into<Coord> + From<Coord> + Clone + Copy {
    /// Gets the Euclidean distance between these two points when embedded in 3D space. This should
    /// **not** be used as an analog of color similarity: use the [`distance()`] function for
    /// that.
    ///
    /// [`distance()`]: ../color/trait.Color.html#method.distance
    fn euclidean_distance(self, other: Self) -> f64 {
        let c1: Coord = self.into();
        let c2: Coord = other.into();
        c1.euclidean_distance(&c2)
    }

    /// Gets the *weighted midpoint* of two colors in a space as a new
    /// [`Color`](../color/trait.Color.html). This is defined as the color corresponding to the point
    /// along the line segment connecting the two points such that the distance to the second point
    /// is the weight, which for most applications needs to be between 0 and 1. For example, a
    /// weight of 0.9 would make the midpoint one-tenth as much affected by the second points as the
    /// first.
    fn weighted_midpoint(self, other: Self, weight: f64) -> Self {
        let c1: Coord = self.into();
        let c2: Coord = other.into();
        Self::from(c1.weighted_midpoint(&c2, weight))
    }

    /// Like `weighted_midpoint`, but with `weight = 0.5`: essentially, the
    /// [`Color`](../color/trait.Color.html) representing the midpoint of the two inputs in 3D space.
    fn midpoint(self, other: Self) -> Self {
        let c1: Coord = self.into();
        let c2: Coord = other.into();
        Self::from(c1.midpoint(&c2))
    }

    /// Returns the weighted average of a given set of colors. Weights will be normalized so that they
    /// sum to 1. Each component of the final value will be calculated by summing the components of
    /// each of the input colors multiplied by their given weight.
    /// # Errors
    /// Returns `ColorCalcError::MismatchedWeights` if the number of colors (`self` and anything in
    /// `others`) and the number of weights mismatch.
    fn weighted_average(
        self,
        others: Vec<Self>,
        weights: Vec<f64>,
    ) -> Result<Self, ColorCalcError> {
        if others.len() + 1 != weights.len() {
            Err(ColorCalcError::MismatchedWeights)
        } else {
            let c1: Coord = self.into();
            let norm: f64 = weights.iter().sum();
            let mut coord = c1 * weights[0] / norm;
            for i in 1..weights.len() {
                coord = coord + others[i - 1].into() * weights[i] / norm;
            }
            Ok(Self::from(coord))
        }
    }
    /// Returns the arithmetic mean of a given set of colors. Equivalent to `weighted_average` in the
    /// case where each weight is the same.
    fn average(self, others: Vec<Self>) -> Coord {
        let c1: Coord = self.into();
        let other_cs: Vec<Coord> = others.iter().map(|x| (*x).into()).collect();
        c1.average(&other_cs)
    }

    /// Returns `true` if the color is outside the range of human vision. Uses the CIE 1931 standard
    /// observer spectral data.
    fn is_imaginary(&self) -> bool {
        let (_wavelengths, xyz_data) = read_cie_spectral_data();
        // convert to chromaticity coordinates
        // use the explicit formulae instead of CIELUVColor to reduce rounding errors
        // we only care about those coordinates
        let uv_func = |xyz: XYZColor| {
            let denom = xyz.x + 15.0 * xyz.y + 3.0 * xyz.z;
            (4.0 * xyz.x / denom, 9.0 * xyz.y / denom)
        };
        let self_uv: (f64, f64) = uv_func(self.convert());
        let uv_data: Vec<(f64, f64)> = xyz_data.into_iter().map(uv_func).collect();
        let self_point = Point::new(self_uv.0, self_uv.1);

        // this is an annoying algorithm, so I'm using a crate instead
        let line: LineString<f64> = uv_data.into();
        line.contains(&self_point)
    }

    /// Returns the closest color that can be seen by the human eye. If the color is not imaginary,
    /// returns itself.
    fn closest_real_color(&self) -> Self {
        // if real color, return itself
        if !self.is_imaginary() {
            *self
        } else {
            let (_wavelengths, xyz_data) = read_cie_spectral_data();
            // convert to chromaticity coordinates
            // use the explicit formulae instead of CIELUVColor to reduce rounding errors
            // we only care about those coordinates
            let uv_func = |xyz: XYZColor| {
                let denom = xyz.x + 15.0 * xyz.y + 3.0 * xyz.z;
                (4.0 * xyz.x / denom, 9.0 * xyz.y / denom)
            };
            // we need to keep luminance data to convert back, so we use CIELUV explicitly
            let mut self_luv: CIELUVColor = self.convert();
            let self_uv = (self_luv.u, self_luv.v);
            let uv_data: Vec<(f64, f64)> = xyz_data.into_iter().map(uv_func).collect();
            let self_point = Point::new(self_uv.0, self_uv.1);

            // this is also an annoying algorithm: just use the crate
            let line: LineString<f64> = uv_data.into();
            let closest_point = line.closest_point(&self_point);
            // convert back into original type
            match closest_point {
                Closest::Intersection(p) => {
                    self_luv.u = p.x();
                    self_luv.v = p.y();
                }
                Closest::SinglePoint(p) => {
                    self_luv.u = p.x();
                    self_luv.v = p.y();
                }
                Closest::Indeterminate => {
                    // should never happen
                    panic!("Indeterminate closest point! Please report this error");
                }
            }
            self_luv.convert()
        }
    }

    /// Returns a Vector of colors that starts with this color, ends with the given other color, and
    /// evenly transitions between colors. The given `n` is the number of additional colors to add.
    fn gradient_scale(&self, other: &Self, n: usize) -> Vec<Self> {
        let mut grad_scale = Vec::new();
        // n + 2 total colors: scale this range to [0, 1] inside the loop
        for i in 0..n + 2 {
            let weight = i as f64 / (n + 1) as f64;
            grad_scale.push((*other).weighted_midpoint(*self, weight));
        }
        grad_scale
    }

    /// Returns a pointer to a function that maps floating-point values from 0 to 1 to colors, such
    /// that 0 returns `self`, 1 returns `other`, and anything in between returns a mix (calculated
    /// linearly). Although it is possible to extrapolate outside of the range [0, 1], this is not
    /// a guarantee and may change without warning. For more fine-grained control of gradients, see
    /// the [`GradientColorMap`](../colormap/struct.GradientColorMap.html) struct.
    ///
    /// # Examples
    /// ```rust
    /// use scarlet::color::RGBColor;
    /// use scarlet::colorpoint::ColorPoint;
    /// let start = RGBColor::from_hex_code("#11457c").unwrap();
    /// let end = RGBColor::from_hex_code("#774bdc").unwrap();
    /// let grad = start.gradient(&end);
    /// let color_at_start = grad(0.).to_string(); // #11457C
    /// let color_at_end = grad(1.).to_string(); // #774BDC
    /// let color_at_third = grad(2./6.).to_string(); // #33479C
    /// ```
    fn gradient(&self, other: &Self) -> Box<dyn Fn(f64) -> Self> {
        let c1: Coord = (*self).into();
        let c2: Coord = (*other).into();
        Box::new(move |x| Self::from(c2.weighted_midpoint(&c1, x)))
    }

    /// Returns a pointer to a function that maps floating-point values from 0 to 1 to colors, such
    /// that 0 returns `self`, 1 returns `other`, and anything in between returns a mix (calculated
    /// by the cube root of the given value). Although it is possible to extrapolate outside of the
    /// range [0, 1], this is not a guarantee and may change without warning. For more fine-grained
    /// control of gradients, see the [`GradientColorMap`](../colormap/struct.GradientColorMap.html) struct.
    ///
    /// # Examples
    /// ```rust
    /// use scarlet::color::RGBColor;
    /// use scarlet::colorpoint::ColorPoint;
    /// let start = RGBColor::from_hex_code("#11457c").unwrap();
    /// let end = RGBColor::from_hex_code("#774bdc").unwrap();
    /// let grad = start.cbrt_gradient(&end);
    /// let color_at_start = grad(0.).to_string(); // #11457C
    /// let color_at_end = grad(1.).to_string(); // #774BDC
    /// let color_at_third = grad(2./6.).to_string(); // #5849BF
    /// ```
    fn cbrt_gradient(&self, other: &Self) -> Box<dyn Fn(f64) -> Self> {
        let c1: Coord = (*self).into();
        let c2: Coord = (*other).into();
        Box::new(move |x| Self::from(c2.weighted_midpoint(&c1, x.cbrt())))
    }

    /// Returns a pointer to a function that maps floating-point values from 0 to 1 to colors with
    /// padding `lower_pad` and `upper_pad` such that an input of 0 returns the gradient at
    /// `lower_pad`, an input of 1 returns the gradient at `upper_pad`, and values in-between are
    /// mapped linearly inside that range. For more fine-grained control over gradients, see the
    /// [`GradientColorMap`](../colormap/struct.GradientColorMap.html) struct.
    ///
    /// # Examples
    /// ```rust
    /// use scarlet::color::RGBColor;
    /// use scarlet::colorpoint::ColorPoint;
    /// let start = RGBColor::from_hex_code("#11457c").unwrap();
    /// let end = RGBColor::from_hex_code("#774bdc").unwrap();
    ///
    /// // the following would be equivalent to start.gradient(&end);
    /// let normal_grad = start.padded_gradient(&end, 0., 1.);
    ///
    /// let padded_grad = start.padded_gradient(&end, 1. / 6., 5. / 6.);
    /// // 0.25 is 1/4 of the way between 1/6 and 5/6, so it's equivalent to a 2/6 call
    /// assert_eq!(padded_grad(0.25).to_string(), normal_grad(1./3.).to_string());
    /// ```
    fn padded_gradient(
        &self,
        other: &Self,
        lower_pad: f64,
        upper_pad: f64,
    ) -> Box<dyn Fn(f64) -> Self> {
        let c1: Coord = (*self).into();
        let c2: Coord = (*other).into();
        let length = upper_pad - lower_pad;
        Box::new(move |x| Self::from(c2.weighted_midpoint(&c1, length * x + lower_pad)))
    }
}

impl<T: Color + Into<Coord> + From<Coord> + Copy + Clone> ColorPoint for T {
    // nothing to do
}

#[cfg(test)]
mod tests {
    #[allow(unused_imports)]
    use super::*;
    use color::RGBColor;
    use colors::cielabcolor::CIELABColor;

    #[test]
    fn test_cielab_distance() {
        // pretty much should work the same for any type, so why not just CIELAB?
        let lab1 = CIELABColor {
            l: 10.5,
            a: -45.0,
            b: 40.0,
        };
        let lab2 = CIELABColor {
            l: 54.2,
            a: 65.0,
            b: 100.0,
        };
        assert!((lab1.euclidean_distance(lab2) - 132.70150715).abs() <= 1e-7);
    }
    #[test]
    fn test_grad_scale() {
        let start = RGBColor::from_hex_code("#11457c").unwrap();
        let end = RGBColor::from_hex_code("#774bdc").unwrap();
        let grad_hexes: Vec<String> = start
            .gradient_scale(&end, 5)
            .iter()
            .map(|x| x.to_string())
            .collect();
        assert_eq!(
            grad_hexes,
            vec!["#11457C", "#22468C", "#33479C", "#4448AC", "#5549BC", "#664ACC", "#774BDC",]
        );
    }
    #[test]
    fn test_grad_func() {
        let start = RGBColor::from_hex_code("#11457c").unwrap();
        let end = RGBColor::from_hex_code("#774bdc").unwrap();
        let grad = start.gradient(&end);
        assert_eq!(grad(1.).to_string(), "#774BDC");
        assert_eq!(grad(0.).to_string(), "#11457C");
        assert_eq!(grad(2. / 6.).to_string(), "#33479C");
    }
    #[test]
    fn test_cbrt_grad_func() {
        let start = RGBColor::from_hex_code("#11457c").unwrap();
        let end = RGBColor::from_hex_code("#774bdc").unwrap();
        let grad = start.cbrt_gradient(&end);
        assert_eq!(grad(1.).to_string(), "#774BDC");
        assert_eq!(grad(0.).to_string(), "#11457C");
        assert_eq!(grad(2. / 6.).to_string(), "#5849BF");
    }
    #[test]
    fn test_padded_grad_func() {
        let start = RGBColor::from_hex_code("#11457c").unwrap();
        let end = RGBColor::from_hex_code("#774bdc").unwrap();
        let grad = start.gradient(&end);
        let equiv_pad_grad = start.padded_gradient(&end, 0., 1.);
        assert_eq!(grad(1.).to_string(), equiv_pad_grad(1.).to_string());
        assert_eq!(grad(0.2).to_string(), equiv_pad_grad(0.2).to_string());
        assert_eq!(grad(0.3).to_string(), equiv_pad_grad(0.3).to_string());
        assert_eq!(grad(0.4).to_string(), equiv_pad_grad(0.4).to_string());

        let middle_pad_grad = start.padded_gradient(&end, 0.25, 0.75);
        assert_eq!(grad(0.5).to_string(), middle_pad_grad(0.5).to_string());
        assert_eq!(grad(0.75).to_string(), middle_pad_grad(1.).to_string());
        assert_eq!(grad(0.25).to_string(), middle_pad_grad(0.).to_string());
    }
}