It’s time for another release of Palette!
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 gradients 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.
New Conversion Traits
Starting with the largest breaking change, the new traits for converting between color spaces have finally been implemented. After thinking a lot about the pros and cons, the decision was made to move away from From
and Into
in favor of traits specifically made for converting colors.
The new traits are
-
FromColor
andIntoColor
for converting with automatic clamping.
This is always lossy, including in theFromColor<Self> for Self
case. -
TryFromColor
andTryIntoColor
that returns aResult
instead of clamping. -
FromColorUnclamped
andIntoColorUnclamped
for converting without any clamping at all.
Here’s a small example of FromColor
and IntoColor
from the README:
use palette::{FromColor, IntoColor, Hsl, Lch, Srgb};
let my_rgb = Srgb::new(0.8, 0.3, 0.3);
let mut my_lch = Lch::from_color(my_rgb);
my_lch.hue += 180.0;
let mut my_hsl: Hsl = my_lch.into_color();
my_hsl.lightness *= 0.6;
let my_new_rgb = Srgb::from_color(my_hsl);
…and a short migration guide:
- If you were using
From
orInto
or the oldFromColor
orIntoColor
:- You can retain the old behavior with
FromColorUnclamped
orIntoColorUnclamped
, - or consider using the new
FromColor
orIntoColor
to get automatic clamping.
- You can retain the old behavior with
- If you were using the temporary
ConvertFrom
orConvertInto
-
convert_{from,into}
corresponds toFromColor
andIntoColor
, -
convert_unclamped_{from,into}
corresponds toFromColorUnclamped
andIntoColorUnclamped
, -
try_convert_{from,into}
corresponds toTryFromColor
andTryIntoColor
.
-
There may also be differences in type inference and the number of required conversion steps. Other than that, you should be able to do the same things a before. Feel free to open a discussion thread or a bug report if you have questions or if it doesn’t work as expected.
Other Breaking Changes
The Limited
trait is now named Clamp
to better reflect what it does. It’s implemented for every color space type and is a requirement for the blanket implementation of FromColor
.
The Component
trait has been reworked and there are now two traits: Component
and FloatComponent
. Component
is implemented by all kinds of color component types while FloatComponent
is for components that aren’t integers.
The conversion between different component types is also different. The traits FromComponent
and IntoComponent
have been added to allow more flexible and optimized conversions.
New Color Spaces
A handful of new color spaces have been added:
-
Oklab
andOklch
- Oklab is similar to CIE L*a*b* but tries to improve on some of its shortcomings. See Björn Ottosson’s page on it for details. -
Luv
,Lchuv
andHsluv
- HSLuv is described as “a human-friendly alternative to HSL”. HSLuv is based on CIE 1976 L*u*v*, soLuv
andLchuv
were also added in the process.
These color spaces are made to be as perceptually uniform as possible, meaning the numbers and how they are changed aligns well with how we perceive the resulting colors. For example, two colors with the same numerical lightness should have the same perceived lightness.
RGB, HSV, HSL and HWB Improvements
Rgb
with u8
components implements FromStr
for parsing hexadecimal strings, such as #a1f
, #421ee7
, c0ffee
, etc. The #
is optional and the length can be either 3 or 6 in this initial implementation.
Rgb
and Rgba
with u8
components can now also be converted to and from u32
. A new helper struct, called Packed
, has been added to represent RGB colors in u32
format. It supports multiple different channel orderings, and is meant to be used as a temporary representation when reading and writing colors represented as u32
:
use palette::{Packed, Pixel, Srgba};
use palette::rgb::channels::Argb;
let raw = &[0x7F0080u32, 0x60BBCC];
// Cast the slice to `&[Packed<Argb>]` without copying
let colors = Packed::<Argb>::from_raw_slice(raw);
let first_color: Srgba<u8> = colors[0].into();
Hsv
, Hsl
and Hwb
have been made “encoding aware”, meaning they can represent non-linear RGB colors. They were previously only linear, due to an incorrect assumption, resulting in some situations where they could not give the same result as in other graphics software. At least not without workarounds. This has been fixed in 0.6.0 and it’s now possible to choose their encoding.
Gradient Improvements
The Gradient
type has gotten a few fixes and improvements. The internal storage type has been made generic, which will allow the default Vec
to be replaced with other types. An array, for example:
use palette::{Gradient, LinSrgb};
let gradient = Gradient::from([
(0.0, LinSrgb::new(0.00, 0.05, 0.20)), // A pair of position and color.
(0.2, LinSrgb::new(0.70, 0.10, 0.20)),
(1.0, LinSrgb::new(0.95, 0.90, 0.30)),
]);
let taken_colors: Vec<_> = gradient.take(10).collect();
Making the Gradient
storage generic has also made it possible to add a few pre-defined gradients in the gradient::named
module. They can be used as convenient alternatives to designing custom gradients.
The Take
iterator for gradients has also gotten a couple of improvements. It has been changed to always include the end of the range, instead of stopping shortly before. It does also implement the DoubleEndedIterator
trait that allows it to be reversed.
Color Difference and Color Contrast
Lab
and Lch
implement methods for calculating their CIEDE2000 color difference. This is available via the ColorDifference
trait and can give better results than the simpler option of using the Euclidean distance between two colors.
There is also a new trait, called RelativeContrast
, for calculating the WCAG 2.1 contrast ratio between two colors. This is a method some accessibility tools and guidelines use for determining if there’s enough color contrast between background and foreground elements.
use std::str::FromStr;
use palette::{Srgb, RelativeContrast};
// The rustdoc "DARK" theme background and text colors
let background: Srgb<f32> = Srgb::from_str("#353535")?.into_format();
let foreground = Srgb::from_str("#ddd")?.into_format();
assert!(background.has_enhanced_contrast_text(&foreground));
Integration With Other Crates
Every color space type implements Zeroable
and Pod
from the bytemuck
crate, behind the "bytemuck"
cargo feature (disabled by default). It allows more ways of casting between different data formats.
Every color space type can also be randomly generated, using rand
crate. This is available behind the "random"
cargo feature (disabled by default). It’s however implemented in a way that tries to compensate for biases in some the color spaces, as opposed to just randomizing each component independently. This means that random values in HSV, HSL and HWB will be uniformly distributed in RGB, for example.
This image shows the difference. The colums are for RGB, HSV, HSL and HWB respectively. The first row has the components randomized independency. The second row is randomized with bias compensation.
Benchmarking and Optimizations
Some parts of the library, including matrix math, some color conversion, and component type conversion have been optimized. This was initially meant to lay a foundation for benchmarking and to pick some low hanging fruit. Some of the measurements showed between 10% and 50% reduction in computation time (even more in some cases), so there should hopefully be a noticeable improvement.
I’m a bit careful with my claims, since the actual result may be affected by other factors than what were measured. Our micro benchmarks looked very promising but your mileage may vary.
Other Notable Fixes and Improvements
The Shade
(for lightening and darkening) and Saturate
traits have been improved to be in line with what you may find in other graphics software. Their new form supports both fixed and relative changes:
use approx::assert_relative_eq;
use palette::{Hsl, Shade};
let color = Hsl::new(0.0, 1.0, 0.5);
// Moves the lightness value 50% of the distance towards the maximum
assert_relative_eq!(color.lighten(0.5).lightness, 0.75);
let color = Hsl::new(0.0, 1.0, 0.4);
// Adds 0.2 to the lightness value
assert_relative_eq!(color.lighten_fixed(0.2).lightness, 0.6);
All color space types implement PartialEq
and Eq
for component types that support them.
All color space types have functions to get minimum and maximum values of their components.
A WithAlpha
trait has been added to make it easier to add or set transparency values.
use palette::{Srgb, WithAlpha};
let color = Srgb::new(255u8, 0, 255);
// This results in an `Alpha<Srgb<u8>, f32>`
let transparent = color.with_alpha(0.3f32);
assert_eq!(transparent.alpha, 0.3);
// This changes the transparency to 0.8
let transparent = transparent.with_alpha(0.8);
assert_eq!(transparent.alpha, 0.8);
Wrapping Up
I want to give a big thank you to everyone who contributed and spearheaded many of these changes.
Palette can be found on GitHub, and on crates.io.
Thank you for reading and I hope you will enjoy this batch of changes!