Palette 0.2.0: A Different Shade

31 Jan 2016 — Permalink

It has now been nearly three weeks since version 0.1.0 of Palette, a linear color calculation and conversion library for Rust, was released, but it has already gone through some big and important changes, so I thought it would be a good idea to describe them in a bit more detail. Let’s take a look at what’s new.

Slicing Gradients

Palette provides a Gradient type for linear color interpolation. The basic idea is that you can define a series of control points (colors mapped to numbers), and the Gradient will mix them to give you any color between them. The colors beyond the endpoints of the series will be the same as at the nearest endpoint.

The new slicing feature lets you create an other gradient, which is based on the original one, but with a different domain (or range). Everything outside this new domain will have the same color as at the nearest of its endpoints, so it’s the same behavior as before, but with new endpoints. Let’s take a look at an example:

//Create a gradient from red to blue. The default domain is 0.0 to 1.0.
let g1 = Gradient::new(vec![Rgb::new(1.0, 0.0, 0.0), Rgb::new(0.0, 0.0, 1.0)]);

//Slice it to only include the first half.
let g2 = g1.slice(..0.5);

assert_eq!(g1.get(0.0), g2.get(0.0)); //Ok
assert_eq!(g1.get(0.3), g2.get(0.3)); //Ok
assert_eq!(g1.get(0.5), g2.get(0.5)); //Ok
assert_eq!(g1.get(0.7), g2.get(0.7)); //Error!

The reason for the error is that 0.7 is outside the domain of g2, so it gives RGB(0.5, 0.0, 0.5) (the color at 0.5) instead of RGB(0.3, 0.0, 0.7).

See the documentation for more info about Gradient.

Color Arithmetics

One of the two main purposes of Palette is doing maths with colors and that has now been made a lot simpler. The basic arithmetic operations (+, -, * and /) has been implemented for each color space, and allows both another color and a plain number as the right hand side.

let a = Rgb::new(1.0, 0.5, 0.3);
let b = Rgb::new(0.1, 0.5, 0.2);

//It's now possible to do this:
let c = a * b;

//...instead of this:
let c = Rgb::new(a.red * b.red, a.green * b.green, a.blue * b.blue);

//...and this:
let c = a * 0.5;

//...instead of this:
let c = Rgb::new(a.red * 0.5, a.green * 0.5, a.blue * 0.5);

The only exceptions are the hue based colors (HSV, HSL, etc.), since multiplication and division of the hue isn’t well defined. They do still implement addition and subtraction, so they are not completely left outside.

Working With Any Float

The colors were initially represented by f32 components, which may be too limited for some high precision applications, so every single color type was rewritten to be based on any T: num::Float. The difference isn’t directly apparent, since the default is still f32, but it’s now possible to use Rgb<f64> or even Rgb<MyIncreatibelFloatType>.

The operation traits, like Mix and Shade, has been given an associated type, called Scalar. This corresponds to the type parameter T in the color spaces and allows some simplification when they are used together, like in Gradient<C: Mix + Clone>.

Separating Transparency From Color

The second big change was the separation of the transparency (alpha) component from the colors. The alpha was initially a mandatory part of each color, but this came with increased memory usage if many colors are to be stored, so some kind of decoupling was necessary.

One idea was to make duplicates of each color type, where one has the alpha component and the other doesn’t. This is simple and can easily be automated. The other idea was to make more use of the type system and create a transparent wrapper type which would carry the alpha component. This ended up being the final solution and comes with some interesting possibilities.

The new system introduces an Alpha<C, T: Float> type, which is defined as

pub struct Alpha<C, T: Float> {
    color: C,
    alpha: T,
}

This type implements all of the operation traits, as well as Deref and DerefMut to also expose the content of color. This allows operations on color + alpha as a whole, as well as isolated operations on just color without any conversion. Take a look at this example from the documentation:

use palette::{Rgb, Rgba};

let mut c1 = Rgba::new(1.0, 0.5, 0.5, 0.8);
let c2 = Rgb::new(0.5, 1.0, 1.0);

c1.color = c1.color * c2; //Leave the alpha as it is
c1.blue += 0.2; //The color components can easily be accessed
c1 = c1 * 0.5; //Scale both the color and the alpha

The Rgba type is an alias for Alpha<Rgb<T>, T> and it implements the necessary functions for it to behave in the same way as Rgb. There are also aliases for the other color spaces.

You can read a bit more about Alpha in the documentation.

Separation of Pixel Encodings

An other important change is the separation of sRGB and gamma correction related things from the Rgb type. The distinction between linear and non- linear colors is now encoded into the type system as the Srgb and GammaRgb types. They are meant as transition types when converting from a linear color to some kind of pixel representation, and back. This change has also resulted in a pixel module, where all the pixel format related types can be found.

See the documentation for more.

New Constructor Names

Last, but not least, is a short PSA regarding the new constructor names. The separation of transparency, and the new RGB types made the old constructor naming convention (like Rgb::rgb(...)) unnecessary. Each type (Rgb, Rgba, Srgb, etc.) can now be constructed with a new function, and sometimes with an additional new_u8 function. The Color type is mostly as before, though, with the exception of the transparency variants.

What’s Next?

The next phase will focus on adding some new features. There are already some to-do issues that has been scheduled for version 0.2.1, including named colors, blending and the addition of xyY support. A further goal is to implement variable white points and chromatic adaption.

Palette can be found on GitHub, where contributions are most welcome, and on crates.io.

Thank you for reading this, and have fun making colorful creations!