Trait scarlet::color::Color

source ·
pub trait Color: Sized {
Show 14 methods // Required methods fn from_xyz(xyz: XYZColor) -> Self; fn to_xyz(&self, illuminant: Illuminant) -> XYZColor; // Provided methods fn convert<T: Color>(&self) -> T { ... } fn hue(&self) -> f64 { ... } fn set_hue(&mut self, new_hue: f64) { ... } fn lightness(&self) -> f64 { ... } fn set_lightness(&mut self, new_lightness: f64) { ... } fn chroma(&self) -> f64 { ... } fn set_chroma(&mut self, new_chroma: f64) { ... } fn saturation(&self) -> f64 { ... } fn set_saturation(&mut self, new_sat: f64) { ... } fn grayscale(&self) -> Self where Self: Sized { ... } fn distance<T: Color>(&self, other: &T) -> f64 { ... } fn visually_indistinguishable<T: Color>(&self, other: &T) -> bool { ... }
}
Expand description

A trait that represents any color representation that can be converted to and from the CIE 1931 XYZ color space. See module-level documentation for more information and examples.

Required Methods§

source

fn from_xyz(xyz: XYZColor) -> Self

Converts from a color in CIE 1931 XYZ to the given color type.

Example
let rgb1 = RGBColor::from_hex_code("#ffffff")?;
// any illuminant would work: Scarlet takes care of that automatically
let rgb2 = RGBColor::from_xyz(XYZColor::white_point(Illuminant::D65));
assert_eq!(rgb1.to_string(), rgb2.to_string());
source

fn to_xyz(&self, illuminant: Illuminant) -> XYZColor

Converts from the given color type to a color in CIE 1931 XYZ space. Because most color types don’t include illuminant information, it is provided instead, as an enum. For most applications, D50 or D65 is a good choice.

Example
// CIELAB is implicitly D50
let lab = CIELABColor{l: 100., a: 0., b: 0.};
// sRGB is implicitly D65
let rgb = RGBColor{r: 1., g: 1., b: 1.};
// conversion to a different illuminant keeps their difference
let lab_xyz = lab.to_xyz(Illuminant::D75);
let rgb_xyz = rgb.to_xyz(Illuminant::D75);
assert!(!lab_xyz.approx_equal(&rgb_xyz));
// on the other hand, CIELCH is in D50, so its white will be the same as CIELAB
let lch_xyz = CIELCHColor{l: 100., c: 0., h: 0.}.to_xyz(Illuminant::D75);
assert!(lab_xyz.approx_equal(&lch_xyz));

Provided Methods§

source

fn convert<T: Color>(&self) -> T

Converts generic colors from one representation to another. This is done by going back and forth from the CIE 1931 XYZ space, using the illuminant D50 (although this should not affect the results). Just like collect() and other methods in the standard library, the use of type inference will usually allow for clean syntax, but occasionally the turbofish is necessary.

Example
let xyz = XYZColor{x: 0.2, y: 0.6, z: 0.3, illuminant: Illuminant::D65};
// how would this look like as the closest hex code?

// the following two lines are equivalent. The first is preferred for simple variable
// allocation, but in more complex scenarios sometimes it's unnecessarily cumbersome
let rgb1: RGBColor = xyz.convert();
let rgb2 = xyz.convert::<RGBColor>();
assert_eq!(rgb1.to_string(), rgb2.to_string());
println!("{}", rgb1.to_string());
source

fn hue(&self) -> f64

Gets the generally most accurate version of hue for a given color: the hue coordinate in CIELCH. There are generally considered four “unique hues” that humans perceive as not decomposable into other hues (when mixing additively): these are red, yellow, green, and blue. These unique hues have values of 0, 90, 180, and 270 degrees respectively, with other colors interpolated between them. This returned value will never be outside the range 0 to 360. For more information, you can start at the Wikpedia page.

This generally shouldn’t differ all that much from HSL or HSV, but it is slightly more accurate to human perception and so is generally superior. This should be preferred over manually converting to HSL or HSV.

Example

One problem with using RGB to work with lightness and hue is that it fails to account for hue shifts as lightness changes, such as the difference between yellow and brown. When this causes a shift from red towards blue, it’s called the Purkinje effect. This example demonstrates how this can trip up color manipulation if you don’t use more perceptually accurate color spaces.

let bright_red = RGBColor{r: 0.9, g: 0., b: 0.};
// One would think that adding or subtracting red here would keep the hue constant
let darker_red = RGBColor{r: 0.3, g: 0., b: 0.};
// However, note that the hue has shifted towards the blue end of the spectrum: in this case,
// closer to 0 by a substantial amount
println!("{} {}", bright_red.hue(), darker_red.hue());
assert!(bright_red.hue() - darker_red.hue() >= 8.);
source

fn set_hue(&mut self, new_hue: f64)

Sets a perceptually-accurate version hue of a color, even if the space itself does not have a conception of hue. This uses the CIELCH version of hue. To use another one, simply convert and set it manually. If the given hue is not between 0 and 360, it is shifted in that range by adding multiples of 360.

Example

This example shows that RGB primaries are not exact standins for the hue they’re named for, and using Scarlet can improve color accuracy.

let blue = RGBColor{r: 0., g: 0., b: 1.};
// this is a setter, so we make a copy first so we have two colors
let mut red = blue;
red.set_hue(0.); // "ideal" red
// not the same red as RGB's red!
println!("{}", red.to_string());
assert!(!red.visually_indistinguishable(&RGBColor{r: 1., g: 0., b: 0.}));
source

fn lightness(&self) -> f64

Gets a perceptually-accurate version of lightness as a value from 0 to 100, where 0 is black and 100 is pure white. The exact value used is CIELAB’s definition of luminance, which is generally considered a very good standard. Note that this is nonlinear with respect to the physical amount of light emitted: a material with 18% reflectance has a lightness value of 50, not 18.

Examples

HSL and HSV are often used to get luminance. We’ll see why this can be horrifically inaccurate.

HSL uses the average of the largest and smallest RGB components. This doesn’t account for the fact that some colors have inherently more or less brightness (for instance, yellow looks much brighter than purple). This is sometimes called chroma: we would say that purple has high chroma. (In Scarlet, chroma usually means something else: check the chroma method for more info.)

let purple = HSLColor{h: 300., s: 0.8, l: 0.5};
let yellow = HSLColor{h: 60., s: 0.8, l: 0.5};
// these have completely different perceptual luminance values
println!("{} {}", purple.lightness(), yellow.lightness());
assert!(yellow.lightness() - purple.lightness() >= 30.);

HSV has to take the cake: it simply uses the maximum RGB component. This means that for highly-saturated colors with high chroma, it gives results that aren’t even remotely close to the true perception of lightness.

let purple = HSVColor{h: 300., s: 1., v: 1.};
let white = HSVColor{h: 300., s: 0., v: 1.};
println!("{} {}", purple.lightness(), white.lightness());
assert!(white.lightness() - purple.lightness() >= 39.);

Hue has only small differences across different color systems, but as you can see lightness is a completely different story. HSL/HSV and CIELAB can disagree by up to a third of the entire range of lightness! This means that any use of HSL or HSV for luminance is liable to be extraordinarily inaccurate if used for widely different chromas. Thus, use of this method is always preferred unless you explicitly need HSL or HSV.

source

fn set_lightness(&mut self, new_lightness: f64)

Sets a perceptually-accurate version of lightness, which ranges between 0 and 100 for visible colors. Any values outside of this range will be clamped within it.

Example

As we saw in the lightness method, purple and yellow tend to trip up HSV and HSL: the color system doesn’t account for how much brighter the color yellow is compared to the color purple. What would equiluminant purple and yellow look like? We can find out.

let purple = HSLColor{h: 300., s: 0.8, l: 0.8};
let mut yellow = HSLColor{h: 60., s: 0.8, l: 0.8};
// increasing purple's brightness to yellow results in colors outside the HSL gamut, so we'll
// do it the other way
yellow.set_lightness(purple.lightness());
// note that the hue has to shift a little, at least according to HSL, but they barely disagree
println!("{}", yellow.h); // prints 60.611 or thereabouts
// the L component has to shift a lot to achieve perceptual equiluminance, as well as a ton of
// desaturation, because a saturated dark yellow is really more like brown and is a different
// hue or out of gamut
assert!(purple.l - yellow.l > 0.15);
// essentially, the different hue and saturation is worth .15 luminance
assert!(yellow.s < 0.4);  // saturation has decreased a lot
source

fn chroma(&self) -> f64

Gets a perceptually-accurate version of chroma, defined as colorfulness relative to a similarly illuminated white. This has no explicit upper bound, but is always positive and generally between 0 and 180 for visible colors. This is done using the CIELCH model.

Example

Chroma differs from saturation in that it doesn’t account for lightness as much as saturation: there are just fewer colors at really low light levels, and so most colors appear less colorful. This can either be the desired measure of this effect, or it can be more suitable to use saturation. A comparison:

let dark_purple = RGBColor{r: 0.4, g: 0., b: 0.4};
let bright_purple = RGBColor{r: 0.8, g: 0., b: 0.8};
println!("{} {}", dark_purple.chroma(), bright_purple.chroma());
// chromas differ widely: about 57 for the first and 94 for the second
assert!(bright_purple.chroma() - dark_purple.chroma() >= 35.);
source

fn set_chroma(&mut self, new_chroma: f64)

Sets a perceptually-accurate version of chroma, defined as colorfulness relative to a similarly illuminated white. Uses CIELCH’s defintion of chroma for implementation. Any value below 0 will be clamped up to 0, but because the upper bound depends on the hue and lightness no clamping will be done. This means that this method has a higher chance than normal of producing imaginary colors and any output from this method should be checked.

Example

We can use the purple example from above, and see what an equivalent chroma to the dark purple would look like at a high lightness.

let dark_purple = RGBColor{r: 0.4, g: 0., b: 0.4};
let bright_purple = RGBColor{r: 0.8, g: 0., b: 0.8};
let mut changed_purple = bright_purple;
changed_purple.set_chroma(dark_purple.chroma());
println!("{} {}", bright_purple.to_string(), changed_purple.to_string());
// prints #CC00CC #AC4FA8
source

fn saturation(&self) -> f64

Gets a perceptually-accurate version of saturation, defined as chroma relative to lightness. Generally ranges from 0 to around 10, although exact bounds are tricky. from This means that e.g., a very dark purple could be very highly saturated even if it does not seem so relative to lighter colors. This is computed using the CIELCH model and computing chroma divided by lightness: if the lightness is 0, the saturation is also said to be 0. There is no official formula except ones that require more information than this model of colors has, but the CIELCH formula is fairly standard.

Example
let red = RGBColor{r: 1., g: 0.2, b: 0.2};
let dark_red = RGBColor{r: 0.7, g: 0., b: 0.};
assert!(dark_red.saturation() > red.saturation());
assert!(dark_red.chroma() < red.chroma());
source

fn set_saturation(&mut self, new_sat: f64)

Sets a perceptually-accurate version of saturation, defined as chroma relative to lightness. Does this without modifying lightness or hue. Any negative value will be clamped to 0, but because the maximum saturation is not well-defined any positive value will be used as is: this means that this method is more likely than others to produce imaginary colors. Uses the CIELCH color space. Generally, saturation ranges from 0 to about 1, but it can go higher.

Example
let red = RGBColor{r: 0.5, g: 0.2, b: 0.2};
let mut changed_red = red;
changed_red.set_saturation(1.5);
println!("{} {}", red.to_string(), changed_red.to_string());
// prints #803333 #8B262C
source

fn grayscale(&self) -> Selfwhere Self: Sized,

Returns a new Color of the same type as before, but with chromaticity removed: effectively, a color created solely using a mix of black and white that has the same lightness as before. This uses the CIELAB luminance definition, which is considered a good standard and is perceptually accurate for the most part.

Example
let rgb = RGBColor{r: 0.7, g: 0.5, b: 0.9};
let hsv = HSVColor{h: 290., s: 0.5, v: 0.8};
// type annotation is superfluous: just note how grayscale works within the type of a color.
let rgb_grey: RGBColor = rgb.grayscale();
let hsv_grey: HSVColor = hsv.grayscale();
// saturation may not be truly zero because of different illuminants and definitions of grey,
// but it's pretty close
println!("{:?} {:?}", hsv_grey, rgb_grey);
assert!(hsv_grey.s < 0.001);
// ditto for RGB
assert!((rgb_grey.r - rgb_grey.g).abs() <= 0.01);
assert!((rgb_grey.r - rgb_grey.b).abs() <= 0.01);
assert!((rgb_grey.g - rgb_grey.b).abs() <= 0.01);
source

fn distance<T: Color>(&self, other: &T) -> f64

Returns a metric of the distance between the given color and another that attempts to accurately reflect human perception. This is done by using the CIEDE2000 difference formula, the current international and industry standard. The result, being a distance, will never be negative: it has no defined upper bound, although anything larger than 100 would be very extreme. A distance of 1.0 is conservatively the smallest possible noticeable difference: anything that is below 1.0 is almost guaranteed to be indistinguishable to most people.

It’s important to note that, just like chromatic adaptation, there’s no One True Function for determining color difference. This is a best effort by the scientific community, but individual variance, difficulty of testing, and the idiosyncrasies of human vision make this difficult. For the vast majority of applications, however, this should work correctly. It works best with small differences, so keep that in mind: it’s relatively hard to quantify whether bright pink and brown are more or less similar than bright blue and dark red.

For more, check out the associated guide.

Examples

Using the distance between points in RGB space, or really any color space, as a way of measuring difference runs into some problems, which we can examine using a more accurate function. The main problem, as the below image shows (source), is that our sensitivity to color variance shifts a lot depending on what hue the colors being compared are. (In the image, the ellipses are drawn ten times as large as the smallest perceptible difference: the larger the ellipse, the less sensitive the human eye is to changes in that region.) Perceptual uniformity is the goal for color spaces like CIELAB, but this is a failure point.

MacAdam ellipses showing areas of indistinguishability scaled by a factor of 10. The green ellipses are much wider than the blue.

The other problem is that our sensitivity to lightness also shifts a lot depending on the conditions: we’re not as at distinguishing dark grey from black, but better at distinguishing very light grey from white. We can examine these phenomena using Scarlet.

let dark_grey = RGBColor{r: 0.05, g: 0.05, b: 0.05};
let black = RGBColor{r: 0.0, g: 0.0, b: 0.0};
let light_grey = RGBColor{r: 0.95, g: 0.95, b: 0.95};
let white = RGBColor{r: 1., g: 1., b: 1.,};
// RGB already includes a factor to attempt to compensate for the color difference due to
// lighting. As we'll see, however, it's not enough to compensate for this.
println!("{} {} {} {}", dark_grey.to_string(), black.to_string(), light_grey.to_string(),
white.to_string());
// prints #0D0D0D #000000 #F2F2F2 #FFFFFF
//
// noticeable error: not very large at this scale, but the effect exaggerates for very similar colors
assert!(dark_grey.distance(&black) < 0.9 * light_grey.distance(&white));
let mut green1 = RGBColor{r: 0.05, g: 0.9, b: 0.05};
let mut green2 = RGBColor{r: 0.05, g: 0.91, b: 0.05};
let blue1 = RGBColor{r: 0.05, g: 0.05, b: 0.9};
let blue2 = RGBColor{r: 0.05, g: 0.05, b: 0.91};
// to remove the effect of lightness on color perception, equalize them
green1.set_lightness(blue1.lightness());
green2.set_lightness(blue2.lightness());
// In RGB these have the same difference. This formula accounts for the perceptual distance, however.
println!("{} {} {} {}", green1.to_string(), green2.to_string(), blue1.to_string(),
blue2.to_string());
// prints #0DE60D #0DEB0D #0D0DE6 #0D0DEB
//
// very small error, but nonetheless roughly 1% off
assert!(green1.distance(&green2) / blue1.distance(&blue2) < 0.992);
source

fn visually_indistinguishable<T: Color>(&self, other: &T) -> bool

Using the metric that two colors with a CIEDE2000 distance of less than 1 are indistinguishable, determines whether two colors are visually distinguishable from each other. For more, check out this guide.

Examples

let color1 = RGBColor::from_hex_code("#123456").unwrap();
let color2 = RGBColor::from_hex_code("#123556").unwrap();
let color3 = RGBColor::from_hex_code("#333333").unwrap();

assert!(color1.visually_indistinguishable(&color2)); // yes, they are visually indistinguishable
assert!(color2.visually_indistinguishable(&color1)); // yes, the same two points
assert!(!color1.visually_indistinguishable(&color3)); // not visually distinguishable

Implementors§