[][src]Trait scarlet::color::Color

pub trait Color: Sized {
    fn from_xyz(_: XYZColor) -> Self;
fn to_xyz(&self, illuminant: Illuminant) -> XYZColor; 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 { ... } }

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

fn from_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());

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));
Loading content...

Provided methods

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

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.);

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

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.

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

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.);

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

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

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

fn grayscale(&self) -> Self where
    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);

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

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
Loading content...

Implementors

impl Color for RGBColor[src]

impl Color for XYZColor[src]

impl Color for AdobeRGBColor[src]

fn from_xyz(xyz: XYZColor) -> AdobeRGBColor[src]

Converts a given XYZ color to Adobe RGB. Adobe RGB is implicitly D65, so any color will be converted to D65 before conversion. Values outside of the Adobe RGB gamut will be clipped.

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

Converts from Adobe RGB to an XYZ color in a given illuminant (via chromatic adaptation).

impl Color for CIELABColor[src]

fn from_xyz(xyz: XYZColor) -> CIELABColor[src]

Converts a given CIE XYZ color to CIELAB. Because CIELAB is implicitly in a given illuminant space, and because the linear conversions within CIELAB that it uses conflict with the transform used in the rest of Scarlet, this is explicitly CIELAB D50: any other illuminant is converted to D50 outside of CIELAB conversion. This in line with programs like Photoshop, which also use CIELAB D50.

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

Returns an XYZ color that corresponds to the CIELAB color. Note that, because implicitly every CIELAB color is D50, conversion is done by first converting to a D50 XYZ color and then using a chromatic adaptation transform.

impl Color for CIELCHColor[src]

fn from_xyz(xyz: XYZColor) -> CIELCHColor[src]

Converts from XYZ to LCH by way of CIELAB.

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

Converts from LCH back to XYZ by way of CIELAB, chromatically adapting it as CIELAB does.

impl Color for CIELCHuvColor[src]

fn from_xyz(xyz: XYZColor) -> CIELCHuvColor[src]

Converts from XYZ to CIELCHuv through CIELUV.

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

Gets the XYZ color that corresponds to this one, through CIELUV.

impl Color for CIELUVColor[src]

fn from_xyz(xyz: XYZColor) -> CIELUVColor[src]

Given an XYZ color, gets a new CIELUV color. This is CIELUV D50, so anything else is chromatically adapted before conversion.

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

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).

impl Color for HSLColor[src]

fn from_xyz(xyz: XYZColor) -> HSLColor[src]

Converts from XYZ to HSL through RGB: thus, there is a limited precision because RGB colors are limited to integer values of R, G, and B.

impl Color for HSVColor[src]

fn from_xyz(xyz: XYZColor) -> HSVColor[src]

Converts to HSV by going through sRGB.

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

Converts from HSV back to XYZ. Any illuminant other than D65 is computed using chromatic adaptation.

impl Color for ROMMRGBColor[src]

fn from_xyz(xyz: XYZColor) -> ROMMRGBColor[src]

Converts a given XYZ color to the closest representable ROMM RGB color. As the ROMM RGB space uses D50 as a reference white, any other illuminant is chromatically adapted first.

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

Converts back from ROMM RGB to XYZ. As ROMM RGB uses D50, any other illuminant given will be chromatically adapted to from D50. This implementation is not from a spec: it's just the mathematical inverse of the from_xyz function, as best as the library author can compute it. This is the most likely function to give mismatches with other libraries or contain errors.

Loading content...