#color-palette #palette #color-conversion #preset #interpolation #conversion #color

ratio-color

Ratio's color palette management in Rust, built on 'palette' and 'enterpolation'

11 releases

0.4.5 Aug 2, 2024
0.4.4 Jan 29, 2024
0.4.3 Jun 21, 2023
0.4.1 May 30, 2023
0.1.0 Apr 3, 2023

#408 in Algorithms

44 downloads per month
Used in ratio-dsm

MIT/Apache

64KB
2K SLoC

Ratio Color

Color palette library.

Changelog

This repository keeps a CHANGELOG.md according to the recommendations by Keep a Changelog.

Contributions and license

To get contributing, feel free to fork, pick up an issue or file your own and get going for your first merge! We'll be more than happy to help.

You might have noticed that this crate follows the "MIT-OR-Apache-2.0" license and some of our other crates follow the "GPL-3.0-or-later" license. This is intentional. Some crates, we consider as "just utilities" that we would like to offer to the entire Rust community without any question asked or guarantees given. This is such a utility crate.


lib.rs:

A library to make palette management for plotting projects easier. It is built on the crate palette that allows for creating, mixing and generally working with colors and pixels.

A short categorical example utilizing the Bold preset:

use ratio_color::{Categorical, Container, Palette, key_to_order};
use palette::Srgba;
let keys = ["foo", "bar", "baz", "quux"];
let categorical: Palette<&str, Srgba<u8>> = Palette::with_preset(
    &Categorical::Bold,
    key_to_order(keys)
);
assert_eq!(categorical.get("foo").expect("a color"), &Srgba::new(57, 105, 172, 255));

A short numerical example utilizing several numerical presets:

use ratio_color::{key_to_order, ColorAtFraction, Container, LinearGradient, Numerical, Palette};
use palette::Srgba;
let keys = ["foo", "bar", "baz", "quux"];
let numerical: Palette<&str, LinearGradient> = Palette::with_presets(
    &[Numerical::SeqMagma, Numerical::SeqViridis, Numerical::CycEdge, Numerical::DivSpectral],
    key_to_order(keys)
);

let gradient: &LinearGradient = numerical.get("bar").expect("a gradient");
assert_eq!(gradient, &LinearGradient::from(Numerical::SeqMagma));
let color: Srgba<u8> = gradient.color_at(0.5);
assert_eq!(color, Srgba::<u8>::new(183, 56, 120, 255));

The key struct of this crate is the Palette for which the Container trait has been implemented. A container is a mix of a slice of values, usually a vector, and a BTreeMap of keys to access them in a number of ways using the Selector enum to obtain either a suitable index for the slice of values or an override value.

In terms of palettes this corresponds to a palette having a fixed number of colors or gradients to choose from (the values). When binding colors to keys, the default behavior would be to use the key's order in the mapping to determine the index of the palette's color to use using Selector::KeyOrder. However, you might want to use a specific color from the given palette for a given key using Selector::Index or even a complete overridden value not present in the usual palette Selector::Value.

The Palette is a generic struct with an implementation of Container for any orderable key and any value.

Categorical palette

For our example palette, we will use red, green, and blue as our default colors, or values. Pink will be used as a special case later on. Next we create a set of keys for which we want to have colors available on demand. Just as an example, we will use "sun", "trees", "sky", "ground", "ditto", and "zzz". Note that we have more keys than default colors! For some, we will assign a fixed color by using an index. Others receive a color by their key's order with respect to the other keys. And finally, an exception will be made for ditto.

use std::collections::BTreeMap;
use ratio_color::{Container, Palette, Selector};
use palette::Srgba;

// Create some categorical palette colors.
let red: Srgba<u8> = Srgba::new(255, 0, 0, 100);
let green: Srgba<u8> = Srgba::new(0, 255, 0, 200);
let blue: Srgba<u8> = Srgba::new(0, 0, 255, 255);

// Bundle them together in a vec to supply to the categorical palette later on.
let values = vec![red.clone(), green.clone(), blue.clone()];

// Initialize a BTreeMap to map from key to a selector.
let mut selectors = BTreeMap::new();
selectors.insert("sun", Selector::Index(0)); // red
selectors.insert("trees", Selector::Index(1)); // green
selectors.insert("sky", Selector::Index(2)); // blue
selectors.insert("ground", Selector::KeyOrder); // what will this be?
selectors.insert("zzz", Selector::KeyOrder); // I'm probably last.

// Ditto's are always pink, so they get a special value.
let pink: Srgba<u8> = Srgba::new(255, 125, 200, 255);
selectors.insert("ditto", Selector::Value(pink.clone()));

// Create the palette.
let palette = Palette::new(values, selectors);

// Let's check the contents!
assert_eq!(
    palette.get("sun").expect("a color"),
    &red,
    "our sun is red"
);
assert_eq!(
    palette.get("ditto").expect("a color"),
    &pink,
    "a ditto is pink"
);
assert_eq!(
    palette.get("ground").expect("a color"),
    &green,
    "the ground is green, because it's key is the second one by order (ditto, *ground*, sky, sun, trees, zzz)"
);
assert_eq!(
    palette.get("zzz").expect("a color"),
    &blue,
    "even though I'm sixth, I'll be 2 (blue) instead (index=5, per modulo 3 makes 2)"
)

So now we can safely get our colors for our given keys. The Palette's generic nature, means we can use any orderable key, and also any value. Adding or removing a key later on is done by mutating the Palette::selectors property or whatever property you decide to hook up to the Container::selectors trait function.

In order to not necessarily clog your error stack, we rely on the basic behavior of maps an vectors by returning values as an Option rather than throwing errors.

Numerical palette

Numerical palettes are completely similar to categorical palettes as far as storage and access go. The palette crate no longer provides us with continuous color scales directly. As a replacement, we introduce the ColorAtFraction trait and LinearGradient struct, that uses the enterpolation crate to interpolate between linearized colors instead.

use palette::Srgba;
use ratio_color::{key_to_order, ColorAtFraction, LinearGradient, Palette, Container};

// Create basic colors.
let transparent: Srgba<u8> = Srgba::new(0, 0, 0, 0);
let red: Srgba<u8> = Srgba::new(255, 0, 0, 255);
let green: Srgba<u8> = Srgba::new(0, 255, 0, 255);
let blue: Srgba<u8> = Srgba::new(0, 0, 255, 255);

// Create three gradients.
let to_red =
    LinearGradient::new([transparent.clone(), red.clone()], None).expect("a gradient");
let to_green =
    LinearGradient::new([transparent.clone(), green.clone()], None).expect("a gradient");
let to_blue =
    LinearGradient::new([transparent.clone(), blue.clone()], None).expect("a gradient");

// Get a color from a linear gradient and convert it back into a regular Srgba.
// Note that blueness interpolation in the linear color space does not equal regular "mean"
// taking, but the alpha channel does.
let halfway_blue = Srgba::<u8>::from_linear(to_blue.color_at(0.5));
assert_eq!(halfway_blue, Srgba::<u8>::new(0, 0, 188, 128));

// Let's assert the endpoints for completeness' sake.
let start: Srgba<u8> = to_blue.color_at(0.0);
let end: Srgba<u8> = to_blue.color_at(1.0);
assert_eq!(&start, &transparent);
assert_eq!(&end, &blue);

// Store these in an palette for these keys.
let keys = ["foo", "bar", "baz", "quux"];
// Let keys resolve to their order in the BTreeMap.
// Actual palette creation.
let palette = Palette::new(
    [to_red.clone(), to_green.clone(), to_blue.clone()],
    key_to_order(keys)
);

// Test accessing and using a gradient.
let full_red: Srgba<u8> = palette.get("quux").expect("a gradient").color_at(1.0);
assert_eq!(&full_red, &red);
assert_eq!(
    <LinearGradient as ColorAtFraction<Srgba<u8>>>::color_at(
        palette.get("quux").expect("a gradient"),
        0.66
    ),
    Srgba::<u8>::new(212, 0, 0, 168),
        "Access quux at %66 using the fully qualified path to access the method."
);

Dependencies

~3MB
~61K SLoC