Palette 0.7.6

28 Apr 2024 — Permalink

It’s time for another Palette update. This one brings both something from color science, and something from traditional color theory.

For those who aren’t familiar with it, Palette is a Rust library for working with colors, including color conversion and color management. Its features range from common tasks, like color blending or converting HSL to RGB, to more advanced topics. It leverages Rust’s type system, by encoding color space information as type parameters, to be able to prevent mistakes and to keep it open to extension and customization.

CIE CAM16 And CAM16-UCS

It’s now possible to use the CIE CAM16 color appearance model and the CAM16-UCS color space with Palette. This feature has been slow cooking for quite a while, requiring some internal refactoring and other changes before being ready for use. Most of those changes (most visibly, the delta E traits) have been gradually introduced in earlier releases.

CAM16 is a bit different from other types in Palette, in the sense that it takes the viewing conditions into account. This requires additional input to be provided when converting to and from most other color types. These parameters describe the lightness of the environment and the area surrounding the color, among other things. The same color may, for example, be experienced differently in a movie theatre, compared to on the wall of an art gallery.

use palette::{
    Srgb, FromColor,
    cam16::{Cam16, Parameters},
};

// These parameters represent the viewing conditions. This example uses a
// medium grey (the default) environment with 40 cd/m^2 (nits) brightness.
// It can be customized further, depending on one's needs.
let mut example_parameters = Parameters::default_static_wp(40.0);

// Getting CAM16 from sRGB, or most other color spaces, goes via XYZ.
let rgb = Srgb::new(0.3f32, 0.8, 0.1);
let cam16 = Cam16::from_xyz(rgb.into_color(), example_parameters);

The Cam16 struct contains the full set of CAM16 attributes, which makes it a bit unwieldy if you only need a few of them. To help with that, there are also a set of partial CAM16 types, that only contain the hue, a luminosity attribute (lightness or brightness) and a chromaticity parameter (colorfulness, chroma or saturation). Each of them can reproduce the original color, using the original viewing conditions.

To add a color space that’s more suitable for image processing, there’s also CAM16-UCS, represented by Cam16UcsJmh and Cam16UcsJab. They’re derived from CAM16 lightness and colorfulness, but made more perceptually uniform. This makes changes to the color’s numerical values correspond closer to the resulting perceived change.

use palette::{
    Srgb, FromColor, IntoColor,
    cam16::{Cam16Jmh, Parameters, Cam16UcsJmh},
};

// Customize these according to the viewing conditions:
let mut example_parameters = Parameters::default_static_wp(40.0);

// CAM16-UCS from sRGB, via the closest partial CAM16. It's also fine
// to go via `Cam16`, but this is a demo example.
let rgb = Srgb::new(0.3f32, 0.8, 0.1);
let cam16 = Cam16Jmh::from_xyz(rgb.into_color(), example_parameters);
let mut ucs = Cam16UcsJmh::from_color(cam16);

// Let's decrease the lightness:
ucs.lightness *= 0.8;

// Converting back to sRGB, again using a partial CAM16 representation. This
// time to avoid reconstructing the full attribute set.
let cam16 = Cam16Jmh::from_color(ucs).into_xyz(example_parameters);
let rgb = Srgb::from_color(cam16);

There are still some rough edges, but it should have the basic functionality in place. Getting this far has already required quite a bit of research and API design. The next step for making it better is likely to take a step back and improve the overall conversion API. Feel free to get in touch with feedback and suggestions. It helps!

Traditional Color Theory

This update adds a set of traits for generating color schemes, using traditional color theory. These schemes are based on a color wheel with 12 equidistant colors. Starting from any color as a primary, there are now traits for calculating the complementary, split complementary, analogous, triadic, and tetradic colors.

The following example makes a split complementary color scheme, based on this page’s title color:

use palette::{Hsl, color_theory::SplitComplementary};

let primary = Hsl::new_srgb(0.0f32, 0.65, 0.6);
let (complementary1, complementary2) = primary.split_complementary();

let hues = (
    primary.hue.into_positive_degrees(),
    complementary1.hue.into_positive_degrees(),
    complementary2.hue.into_positive_degrees(),
);

assert_eq!(hues, (0.0, 150.0, 210.0));

The third color has the same hue as this page’s headings.

These traits are primarily implemented for types that have a hue component. Some of them, such as Complementary, are also implemented for Lab, Oklab, and other color spaces with a similar shape.

Other Changes

  • From implementations have been added for changing Rgb component types between u8, f32 and f64. This was already possible, via from_format and into_format, but their names are less than obvious.

    let rgb: Srgb<f32> = Srgb::new(171u8, 193, 35).into();
    
  • The angle conversion from f32 to u8 used to incorrectly result in 0 or 1. It has been corrected to span the full range of u8.

Wrapping Up

I want to give a big thank you to everyone who contributed or took part in discussions. Your help is always appreciated!

Palette can be found on GitHub, and on crates.io.

Thank you for reading!