Skip to content

Commit

Permalink
Document CAM16-UCS types
Browse files Browse the repository at this point in the history
  • Loading branch information
Ogeon committed Apr 14, 2024
1 parent a737906 commit 93176c6
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 24 deletions.
2 changes: 1 addition & 1 deletion palette/src/cam16/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ impl<T> Surround<T> {

/// The degree of discounting of (or adaptation to) the illuminant.
///
/// See also: https://en.wikipedia.org/wiki/CIECAM02#CAT02.
/// See also: <https://en.wikipedia.org/wiki/CIECAM02#CAT02>.
#[derive(Clone, Copy)]
#[non_exhaustive]
pub enum Discounting<T> {
Expand Down
4 changes: 4 additions & 0 deletions palette/src/cam16/partial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ macro_rules! make_partial_cam16 {
///
/// // Direct conversion has the same result as going via full CAM16.
/// assert_eq!(partial_from_rgb, partial_from_full);
///
/// // It's also possible to convert from (and to) arrays and tuples:
#[doc = concat!("let partial_from_array = ", stringify!($name), "::from([50.0f32, 80.0, 120.0]);")]
#[doc = concat!("let partial_from_tuple = ", stringify!($name), "::from((50.0f32, 80.0, 120.0));")]
/// ```
#[derive(Clone, Copy, Debug, Default, ArrayCast, WithAlpha, FromColorUnclamped)]
#[palette(
Expand Down
98 changes: 82 additions & 16 deletions palette/src/cam16/ucs_jab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,38 @@ use super::Cam16UcsJmh;
/// `Alpha`](crate::Alpha#Cam16UcsJaba).
pub type Cam16UcsJaba<T> = Alpha<Cam16UcsJab<T>, T>;

/// The Cartesian form of CAM16-UCS, or J'a'b'.
/// The Cartesian form of CAM16-UCS, or J' a' b'.
///
/// CAM16-UCS is a perceptually uniform color space, based on CAM16 lightness
/// and colorfulness. Its polar counterpart is [`Cam16UcsJmh`].
///
/// # Creating a Value
///
/// ```
/// use palette::{
/// Srgb, FromColor, IntoColor,
/// cam16::{Cam16, Parameters, Cam16UcsJab},
/// };
///
/// let ucs = Cam16UcsJab::new(50.0f32, 80.0, -30.0);
///
/// // `new` is also `const`:
/// const UCS: Cam16UcsJab<f32> = Cam16UcsJab::new(50.0, 80.0, -30.0);
///
/// // Customize these according to the viewing conditions:
/// let mut example_parameters = Parameters::default_static_wp();
/// example_parameters.adapting_luminance = 40.0;
/// example_parameters.background_luminance = 0.2;
///
/// // CAM16-UCS from sRGB, or most other color spaces:
/// let rgb = Srgb::new(0.3f32, 0.8, 0.1);
/// let cam16 = Cam16::from_xyz(rgb.into_color(), example_parameters);
/// let ucs_from_rgb = Cam16UcsJab::from_color(cam16);
///
/// // It's also possible to convert from (and to) arrays and tuples:
/// let ucs_from_array = Cam16UcsJab::from([50.0f32, 80.0, -30.0]);
/// let ucs_from_tuple = Cam16UcsJab::from((50.0f32, 80.0, -30.0));
/// ```
#[derive(Clone, Copy, Debug, Default, WithAlpha, ArrayCast, FromColorUnclamped)]
#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
#[palette(
Expand All @@ -28,28 +59,37 @@ pub type Cam16UcsJaba<T> = Alpha<Cam16UcsJab<T>, T>;
)]
#[repr(C)]
pub struct Cam16UcsJab<T> {
/// The [lightness](https://en.wikipedia.org/wiki/Lightness) (J') of the color.
/// The lightness (J') of the color.
///
/// It's derived from [`Cam16::lightness`][crate::cam16::Cam16::lightness]
/// and ranges from `0.0` to `100.0`.
pub lightness: T,

/// The redness/greenness (a') of the color.
///
/// It's derived from [`Cam16::hue`][crate::cam16::Cam16::hue] and
/// [`Cam16::colorfulness`][crate::cam16::Cam16::colorfulness].
pub a: T,

/// The yellowness/blueness (b') of the color.
///
/// It's derived from [`Cam16::hue`][crate::cam16::Cam16::hue] and
/// [`Cam16::colorfulness`][crate::cam16::Cam16::colorfulness].
pub b: T,
}

impl<T> Cam16UcsJab<T> {
/// Create a CIE L\*a\*b\* color.
/// Create a CAM16-UCS J' a' b' color.
pub const fn new(lightness: T, a: T, b: T) -> Self {
Self { lightness, a, b }
}

/// Convert to a `(L\*, a\*, b\*)` tuple.
/// Convert to a `(J', a', b')` tuple.
pub fn into_components(self) -> (T, T, T) {
(self.lightness, self.a, self.b)
}

/// Convert from a `(L\*, a\*, b\*)` tuple.
/// Convert from a `(J', a', b')` tuple.
pub fn from_components((lightness, a, b): (T, T, T)) -> Self {
Self::new(lightness, a, b)
}
Expand All @@ -71,35 +111,47 @@ where

/// Return an `a` value minimum that includes the sRGB gamut.
///
/// ***Note:** This is entirely arbitrary and only for use in random
/// generation. Colorfulness doesn't have a well defined upper bound.*
/// <p class="warning">
/// This is entirely arbitrary and only for use in random generation.
/// Colorfulness doesn't have a well defined upper bound, which makes
/// a' unbounded.
/// </p>
pub fn min_srgb_a() -> T {
// Based on a plot from https://facelessuser.github.io/coloraide/colors/cam16_ucs/
T::from_f64(-50.0)
}

/// Return an `a` value maximum that includes the sRGB gamut.
///
/// ***Note:** This is entirely arbitrary and only for use in random
/// generation. Colorfulness doesn't have a well defined upper bound.*
/// <p class="warning">
/// This is entirely arbitrary and only for use in random generation.
/// Colorfulness doesn't have a well defined upper bound, which makes
/// a' unbounded.
/// </p>
pub fn max_srgb_a() -> T {
// Based on a plot from https://facelessuser.github.io/coloraide/colors/cam16_ucs/
T::from_f64(50.0)
}

/// Return a `b` value minimum that includes the sRGB gamut.
///
/// ***Note:** This is entirely arbitrary and only for use in random
/// generation. Colorfulness doesn't have a well defined upper bound.*
/// <p class="warning">
/// This is entirely arbitrary and only for use in random generation.
/// Colorfulness doesn't have a well defined upper bound, which makes
/// b' unbounded.
/// </p>
pub fn min_srgb_b() -> T {
// Based on a plot from https://facelessuser.github.io/coloraide/colors/cam16_ucs/
T::from_f64(-50.0)
}

/// Return a `b` value maximum that includes the sRGB gamut.
///
/// ***Note:** This is entirely arbitrary and only for use in random
/// generation. Colorfulness doesn't have a well defined upper bound.*
/// <p class="warning">
/// This is entirely arbitrary and only for use in random generation.
/// Colorfulness doesn't have a well defined upper bound, which makes
/// b' unbounded.
/// </p>
pub fn max_srgb_b() -> T {
// Based on a plot from https://facelessuser.github.io/coloraide/colors/cam16_ucs/
T::from_f64(50.0)
Expand All @@ -108,20 +160,20 @@ where

///<span id="Cam16UcsJaba"></span>[`Cam16UcsJaba`](crate::cam16::Cam16UcsJaba) implementations.
impl<T, A> Alpha<Cam16UcsJab<T>, A> {
/// Create a CIE L\*a\*b\* color with transparency.
/// Create a CAM16-UCS J' a' b' color with transparency.
pub const fn new(lightness: T, a: T, b: T, alpha: A) -> Self {
Self {
color: Cam16UcsJab::new(lightness, a, b),
alpha,
}
}

/// Convert to a `(L\*, a\*, b\*, a)` tuple.
/// Convert to a `(J', a', b', a)` tuple.
pub fn into_components(self) -> (T, T, T, A) {
(self.color.lightness, self.color.a, self.color.b, self.alpha)
}

/// Convert from a `(L\*, a\*, b\*, a)` tuple.
/// Convert from a `(J', a', b', a)` tuple.
pub fn from_components((lightness, a, b, alpha): (T, T, T, A)) -> Self {
Self::new(lightness, a, b, alpha)
}
Expand Down Expand Up @@ -249,6 +301,8 @@ impl_rand_traits_cartesian!(

#[cfg(test)]
mod test {
use crate::{cam16::Cam16Jmh, convert::FromColorUnclamped};

use super::Cam16UcsJab;

#[test]
Expand All @@ -266,6 +320,18 @@ mod test {
}
}

#[cfg(feature = "approx")]
#[test]
fn cam16_roundtrip() {
let ucs = Cam16UcsJab::new(50.0f64, 80.0, -30.0);
let cam16 = Cam16Jmh::from_color_unclamped(ucs);
assert_relative_eq!(
Cam16UcsJab::from_color_unclamped(cam16),
ucs,
epsilon = 0.0000000000001
);
}

raw_pixel_conversion_tests!(Cam16UcsJab<>: lightness, a, b);
raw_pixel_conversion_fail_tests!(Cam16UcsJab<>: lightness, a, b);

Expand Down
65 changes: 58 additions & 7 deletions palette/src/cam16/ucs_jmh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,37 @@ use super::{Cam16Jmh, Cam16UcsJab};
pub type Cam16UcsJmha<T> = Alpha<Cam16UcsJmh<T>, T>;

/// The polar form of CAM16-UCS, or J'M'h'.
///
/// CAM16-UCS is a perceptually uniform color space, based on CAM16 lightness
/// and colorfulness. Its cartesian counterpart is [`Cam16UcsJab`].
///
/// # Creating a Value
///
/// ```
/// use palette::{
/// Srgb, FromColor, IntoColor, hues::Cam16Hue,
/// cam16::{Cam16, Parameters, Cam16UcsJmh},
/// };
///
/// let ucs = Cam16UcsJmh::new(50.0f32, 80.0, 120.0);
///
/// // There's also `new_const`:
/// const UCS: Cam16UcsJmh<f32> = Cam16UcsJmh::new_const(50.0, 80.0, Cam16Hue::new(120.0));
///
/// // Customize these according to the viewing conditions:
/// let mut example_parameters = Parameters::default_static_wp();
/// example_parameters.adapting_luminance = 40.0;
/// example_parameters.background_luminance = 0.2;
///
/// // CAM16-UCS from sRGB, or most other color spaces:
/// let rgb = Srgb::new(0.3f32, 0.8, 0.1);
/// let cam16 = Cam16::from_xyz(rgb.into_color(), example_parameters);
/// let ucs_from_rgb = Cam16UcsJmh::from_color(cam16);
///
/// // It's also possible to convert from (and to) arrays and tuples:
/// let ucs_from_array = Cam16UcsJmh::from([50.0f32, 80.0, 120.0]);
/// let ucs_from_tuple = Cam16UcsJmh::from((50.0f32, 80.0, 120.0));
/// ```
#[derive(Clone, Copy, Debug, Default, WithAlpha, ArrayCast, FromColorUnclamped)]
#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
#[palette(
Expand All @@ -27,13 +58,21 @@ pub type Cam16UcsJmha<T> = Alpha<Cam16UcsJmh<T>, T>;
)]
#[repr(C)]
pub struct Cam16UcsJmh<T> {
/// The [lightness](https://en.wikipedia.org/wiki/Lightness) (J') of the color.
/// The lightness (J') of the color.
///
/// It's derived from [`Cam16::lightness`][crate::cam16::Cam16::lightness]
/// and ranges from `0.0` to `100.0`.
pub lightness: T,

/// The [colorfulness](https://en.wikipedia.org/wiki/Colorfulness) (M') of the color.
/// The colorfulness (M') of the color.
///
/// It's derived from [`Cam16::colorfulness`][crate::cam16::Cam16::colorfulness].
pub colorfulness: T,

/// The [hue](https://en.wikipedia.org/wiki/Hue) (h') of the color.
/// The hue (h') of the color.
///
/// It's the same as [`Cam16::hue`][crate::cam16::Cam16::hue], despite the
/// h' notation.
#[palette(unsafe_same_layout_as = "T")]
pub hue: Cam16Hue<T>,
}
Expand Down Expand Up @@ -89,9 +128,11 @@ where

/// Return a `colorfulness` value maximum that includes the sRGB gamut.
///
/// ***Note:** This is entirely arbitrary and only for use in `Lighten`,
/// `Darken` and random generation. Colorfulness doesn't have a well defined
/// upper bound.*
/// <p class="warning">
/// This is entirely arbitrary and only for use in `Lighten`, `Darken` and
/// random generation. Colorfulness doesn't have a well defined upper
/// bound.
/// </p>
pub fn max_srgb_colorfulness() -> T {
// Based on a plot from https://facelessuser.github.io/coloraide/colors/cam16_ucs/
T::from_f64(50.0)
Expand Down Expand Up @@ -260,7 +301,10 @@ impl_rand_traits_cylinder!(

#[cfg(test)]
mod test {
use crate::cam16::Cam16UcsJmh;
use crate::{
cam16::{Cam16Jmh, Cam16UcsJmh},
convert::FromColorUnclamped,
};

#[cfg(feature = "approx")]
use crate::color_difference::DeltaE;
Expand All @@ -286,6 +330,13 @@ mod test {
}
}

#[test]
fn cam16_roundtrip() {
let ucs = Cam16UcsJmh::new(50.0f64, 80.0, 120.0);
let cam16 = Cam16Jmh::from_color_unclamped(ucs);
assert_eq!(Cam16UcsJmh::from_color_unclamped(cam16), ucs);
}

raw_pixel_conversion_tests!(Cam16UcsJmh<>: lightness, colorfulness, hue);
raw_pixel_conversion_fail_tests!(Cam16UcsJmh<>: lightness, colorfulness, hue);

Expand Down

0 comments on commit 93176c6

Please sign in to comment.