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());
}
}