Skip to content

Commit

Permalink
Remove default conversion between XYZ and CAM16
Browse files Browse the repository at this point in the history
  • Loading branch information
Ogeon committed Nov 19, 2023
1 parent ceb19cd commit 404ac2e
Show file tree
Hide file tree
Showing 16 changed files with 1,158 additions and 865 deletions.
9 changes: 3 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,13 +232,9 @@ The central trait for color conversion is `FromColorUnclamped`. This should be d

#### In The `palette_derive` Crate

Add the type's name to `COLOR_TYPES` in `lib.rs`. This includes it in the list of possible derived conversions.
Add the type's name to its color group (typically `BASE_COLORS`) in `color_types.rs`. This includes it in the list of possible derived conversions. The `preferred_source` tells the library how to find a path to the color type. Each color space has a "parent" space that all connects to `Xyz`. For example, `Hwb` connects to `Hsv`, which connects to `Rgb`, which connects to `Xyz`. The derived conversion code will backtrack as far towards `Xyz` as it needs, until it finds a way to convert to your type.

Add the type's name and another type's name to `PREFERRED_CONVERSION_SOURCE` in `lib.rs`. This tells the library how to find a path to the color type. Each color space has a "parent" space that all connects to `Xyz`. For example, `Hwb` connects to `Hsv`, which connects to `Rgb`, which connects to `Xyz`. The derived conversion code will backtrack as far towards `Xyz` as it needs, until it finds a way to convert to your type.

Add the type's name and a Cargo feature to `REQUIRED_COLOR_FEATURES` in `lib.rs` if it's optional. Optional color spaces should have a `#[cfg(feature = "feature_name")]` attribute on each of the above additions.

Other special casing may be needed in other parts of the code, depending on the type.
Other special casing may be needed in other parts of the code, depending on the type. This part can be confusing, so feel free to ask!

#### In The `palette` Crate

Expand All @@ -248,6 +244,7 @@ In addition to that, add the following in the attribute:

* `component = "T"` to point out the component type.
* `white_point = "Wp"` and other meta info to point out the white point type, if necessary. See the list in the documentation or follow the error hints.
* `color_group = "group"` if it's not part of the bas group, such as `"cam16"` if it's a CIE CAM16 derivative.
* `skip_derives(Xyz, Hsv, Hsl)` with all color types you want to convert from with manual implementations.

Add manual conversions for at least one color type and list it in `skip_derives`. `Xyz` is assumed if `skip_derives` is omitted. These are the minimum requirements:
Expand Down
74 changes: 0 additions & 74 deletions palette/src/cam16.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
//! Types for the CIE CAM16 color appearance model.
use crate::{
angle::{RealAngle, SignedAngle},
bool_mask::LazySelect,
num::{Abs, Arithmetics, One, PartialCmp, Powf, Real, Signum, Sqrt, Trigonometry, Zero},
white_point::{self},
Xyz,
};

pub use full::*;
pub use parameters::*;
pub use partial::*;
Expand All @@ -25,69 +17,3 @@ mod parameters;
mod partial;
mod ucs_jab;
mod ucs_jmh;

/// Converts a color to CAM16, using a set of parameters.
pub trait IntoCam16<WpParam, T> {
/// Convert `self` into CAM16, with `parameters` that describe the viewing
/// conditions.
fn into_cam16(self, parameters: BakedParameters<WpParam, T>) -> Cam16<T>;
}

impl<WpParam, T> IntoCam16<WpParam, T> for Xyz<WpParam::StaticWp, T>
where
WpParam: WhitePointParameter<T>,
T: Real + Arithmetics + Powf + Sqrt + Abs + Signum + Trigonometry + RealAngle + Clone,
{
fn into_cam16(self, parameters: BakedParameters<WpParam, T>) -> Cam16<T> {
math::xyz_to_cam16(self.with_white_point(), parameters.inner)
}
}

/// Converts CAM16 to a color, using a set of parameters.
pub trait FromCam16<WpParam, T>: Sized {
/// Convert `cam16` into `Self`, with `parameters` that describe the viewing
/// conditions.
fn from_cam16(cam16: Cam16<T>, parameters: BakedParameters<WpParam, T>) -> Self {
Self::from_partial_cam16(DynPartialCam16::from(cam16), parameters)
}

/// Convert the partially specified `cam16` into `Self`, with `parameters`
/// that describe the viewing conditions.
fn from_partial_cam16<L, C>(
cam16: PartialCam16<T, L, C>,
parameters: BakedParameters<WpParam, T>,
) -> Self
where
L: Cam16Luminance<T>,
C: Cam16Chromaticity<T>;
}

impl<WpParam, T> FromCam16<WpParam, T> for Xyz<WpParam::StaticWp, T>
where
WpParam: WhitePointParameter<T>,
T: Real
+ One
+ Zero
+ Sqrt
+ Powf
+ Abs
+ Signum
+ Arithmetics
+ Trigonometry
+ RealAngle
+ SignedAngle
+ PartialCmp
+ Clone,
T::Mask: LazySelect<Xyz<white_point::Any, T>>,
{
fn from_partial_cam16<L, C>(
cam16: PartialCam16<T, L, C>,
parameters: BakedParameters<WpParam, T>,
) -> Self
where
L: Cam16Luminance<T>,
C: Cam16Chromaticity<T>,
{
math::cam16_to_xyz(cam16.into_dynamic(), parameters.inner).with_white_point()
}
}
160 changes: 117 additions & 43 deletions palette/src/cam16/full.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
use crate::{
convert::FromColorUnclamped,
angle::{RealAngle, SignedAngle},
bool_mask::LazySelect,
hues::Cam16Hue,
num::{Clamp, ClampAssign, Zero},
white_point::WhitePoint,
Xyz,
num::{
Abs, Arithmetics, Clamp, ClampAssign, One, PartialCmp, Powf, Real, Signum, Sqrt,
Trigonometry, Zero,
},
white_point, Xyz,
};

use super::{BakedParameters, IntoCam16, StaticWp};
use super::{
BakedParameters, Cam16Chromaticity, Cam16Luminance, PartialCam16, WhitePointParameter,
};

/// The CIE CAM16 color appearance model.
///
/// It's a set of six technically defined attributes that describe the
/// appearance of a color under certain viewing conditions, and it's a successor
/// of [CIECAM02](https://en.wikipedia.org/wiki/CIECAM02). The viewing
/// conditions are defined using [`Parameters`][super::Parameters] and two set
/// of `Cam16` attributes are only really comparable if they were calculated
/// from the same set of viewing condition parameters. The implementations of
/// [`FromColor`][crate::FromColor], [`IntoColor`][crate::IntoColor], etc. use
/// `Parameters::default()` as their viewing conditions. See
/// [`FromCam16`][super::FromCam16] and [`IntoCam16`] for options with more
/// control over the parameters.
/// conditions are defined using [`Parameters`][super::Parameters], and two sets
/// of parameters can be used to translate the appearance of a color from one
/// set of viewing conditions to another.
///
/// The use of the viewing conditions parameters sets `Cam16` and its derived
/// types apart from most other color types in this library. It's, for example,
/// not possible to use [`FromColor`][crate::FromColor] and friends to convert
/// to and from other types, since that would require default viewing conditions
/// to exist. Instead, the explicit [`Cam16::from_xyz`] and [`Cam16::into_xyz`]
/// are there to bridge the gap.
///
/// Not all attributes are needed to be known to convert _from_ CAM16, since
/// they are correlated and derived from each other. This library provides a
/// separate [`PartialCam16`][super::PartialCam16] to make it easier to specify
/// Not all attributes are used when converting _from_ CAM16, since they are
/// correlated and derived from each other. This library provides a separate
/// [`PartialCam16`][super::PartialCam16] to make it easier to correctly specify
/// a minimum attribute set.
#[derive(Clone, Copy, Debug, WithAlpha, FromColorUnclamped)]
#[palette(palette_internal, component = "T", skip_derives(Xyz, Cam16))]
#[derive(Clone, Copy, Debug, WithAlpha)]
#[palette(palette_internal, component = "T")]
pub struct Cam16<T> {
/// The [lightness](https://en.wikipedia.org/wiki/Lightness) (J) of the color.
#[doc(alias = "J")]
Expand All @@ -53,6 +61,82 @@ pub struct Cam16<T> {
pub saturation: T,
}

impl<T> Cam16<T> {
/// Derive CIE CAM16 attributes for the provided color, under the provided
/// viewing conditions.
#[inline]
pub fn from_xyz<WpParam>(
color: Xyz<WpParam::StaticWp, T>,
parameters: impl Into<BakedParameters<WpParam, T>>,
) -> Self
where
WpParam: WhitePointParameter<T>,
T: Real + Arithmetics + Powf + Sqrt + Abs + Signum + Trigonometry + RealAngle + Clone,
{
super::math::xyz_to_cam16(color.with_white_point(), parameters.into().inner)
}

/// Construct an XYZ color that matches these CIE CAM16 attributes, under
/// the provided viewing conditions.
///
/// This assumes that all of the correlated attributes are consistent, as
/// only some of them are actually used. You may want to use
/// [`PartialCam16`] for more control over which set of attributes that
/// should be.
#[inline]
pub fn into_xyz<WpParam>(
self,
parameters: impl Into<BakedParameters<WpParam, T>>,
) -> Xyz<WpParam::StaticWp, T>
where
WpParam: WhitePointParameter<T>,
T: Real
+ One
+ Zero
+ Sqrt
+ Powf
+ Abs
+ Signum
+ Arithmetics
+ Trigonometry
+ RealAngle
+ SignedAngle
+ PartialCmp
+ Clone,
T::Mask: LazySelect<Xyz<white_point::Any, T>>,
{
super::math::cam16_to_xyz(self.into(), parameters.into().inner).with_white_point()
}

/// Reconstruct a full set of CIE CAM16 attributes, using the original viewing conditions.
#[inline]
pub fn from_partial<WpParam, L, C>(
partial: PartialCam16<T, L, C>,
parameters: impl Into<BakedParameters<WpParam, T>>,
) -> Self
where
WpParam: WhitePointParameter<T>,
T: Real + Zero + Arithmetics + Sqrt + PartialCmp + Clone,
T::Mask: LazySelect<T> + Clone,
L: Cam16Luminance<T>,
C: Cam16Chromaticity<T>,
{
partial.into_full(parameters)
}

/// Create a partial set of CIE CAM16 attributes.
///
/// It's also possible to use `PartialCam16::from` or `Cam16::into`.
#[inline]
pub fn into_partial<L, C>(self) -> PartialCam16<T, L, C>
where
L: Cam16Luminance<T>,
C: Cam16Chromaticity<T>,
{
PartialCam16::from_full(self)
}
}

impl<T> crate::Clamp for Cam16<T>
where
T: Clamp + Zero,
Expand Down Expand Up @@ -88,27 +172,10 @@ impl_eq_hue!(
[lightness, chroma, brightness, colorfulness, saturation]
);

impl<T> FromColorUnclamped<Cam16<T>> for Cam16<T> {
fn from_color_unclamped(val: Cam16<T>) -> Self {
val
}
}

impl<Wp, T> FromColorUnclamped<Xyz<Wp, T>> for Cam16<T>
where
Wp: WhitePoint<T>,
Xyz<Wp, T>: IntoCam16<StaticWp<Wp>, T>,
BakedParameters<StaticWp<Wp>, T>: Default,
{
fn from_color_unclamped(val: Xyz<Wp, T>) -> Self {
val.into_cam16(BakedParameters::default())
}
}

#[cfg(test)]
mod test {
use crate::{
cam16::{ChromaticityType, LuminanceType, PartialCam16},
cam16::{ChromaticityType, LuminanceType, Parameters, PartialCam16},
convert::{FromColorUnclamped, IntoColorUnclamped},
Srgb,
};
Expand All @@ -118,8 +185,9 @@ mod test {
macro_rules! assert_cam16_to_rgb {
($cam16:expr, $rgb:expr, $($params:tt)*) => {
let cam16 = $cam16;
let parameters = Parameters::TEST_DEFAULTS;

let rgb: Srgb<f64> = cam16.into_color_unclamped();
let rgb: Srgb<f64> = cam16.into_xyz(parameters).into_color_unclamped();
assert_relative_eq!(rgb, $rgb, $($params)*);

let chromaticities = [
Expand All @@ -140,7 +208,7 @@ mod test {
luminance,
};
assert_relative_eq!(
Srgb::<f64>::from_color_unclamped(dbg!(partial)),
Srgb::<f64>::from_color_unclamped(dbg!(partial).into_xyz(parameters)),
$rgb,
$($params)*
);
Expand All @@ -152,7 +220,8 @@ mod test {
#[test]
fn example_blue() {
// Uses the example color from https://observablehq.com/@jrus/cam16
let mut cam16: Cam16<f64> = Srgb::from(0x5588cc).into_linear().into_color_unclamped();
let xyz = Srgb::from(0x5588cc).into_linear().into_color_unclamped();
let mut cam16: Cam16<f64> = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS);
cam16.hue = cam16.hue.into_positive_degrees().into();

assert_relative_eq!(
Expand All @@ -178,7 +247,8 @@ mod test {
#[test]
fn black() {
// Checks against the output from https://observablehq.com/@jrus/cam16
let mut cam16: Cam16<f64> = Srgb::from(0x000000).into_linear().into_color_unclamped();
let xyz = Srgb::from(0x000000).into_linear().into_color_unclamped();
let mut cam16: Cam16<f64> = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS);
cam16.hue = cam16.hue.into_positive_degrees().into();

assert_relative_eq!(
Expand All @@ -204,7 +274,8 @@ mod test {
#[test]
fn white() {
// Checks against the output from https://observablehq.com/@jrus/cam16
let mut cam16: Cam16<f64> = Srgb::from(0xffffff).into_linear().into_color_unclamped();
let xyz = Srgb::from(0xffffff).into_linear().into_color_unclamped();
let mut cam16: Cam16<f64> = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS);
cam16.hue = cam16.hue.into_positive_degrees().into();

assert_relative_eq!(
Expand All @@ -230,7 +301,8 @@ mod test {
#[test]
fn red() {
// Checks against the output from https://observablehq.com/@jrus/cam16
let mut cam16: Cam16<f64> = Srgb::from(0xff0000).into_linear().into_color_unclamped();
let xyz = Srgb::from(0xff0000).into_linear().into_color_unclamped();
let mut cam16: Cam16<f64> = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS);
cam16.hue = cam16.hue.into_positive_degrees().into();

assert_relative_eq!(
Expand All @@ -252,7 +324,8 @@ mod test {
#[test]
fn green() {
// Checks against the output from https://observablehq.com/@jrus/cam16
let mut cam16: Cam16<f64> = Srgb::from(0x00ff00).into_linear().into_color_unclamped();
let xyz = Srgb::from(0x00ff00).into_linear().into_color_unclamped();
let mut cam16: Cam16<f64> = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS);
cam16.hue = cam16.hue.into_positive_degrees().into();

assert_relative_eq!(
Expand All @@ -278,7 +351,8 @@ mod test {
#[test]
fn blue() {
// Checks against the output from https://observablehq.com/@jrus/cam16
let mut cam16: Cam16<f64> = Srgb::from(0x0000ff).into_linear().into_color_unclamped();
let xyz = Srgb::from(0x0000ff).into_linear().into_color_unclamped();
let mut cam16: Cam16<f64> = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS);
cam16.hue = cam16.hue.into_positive_degrees().into();

assert_relative_eq!(
Expand Down
Loading

0 comments on commit 404ac2e

Please sign in to comment.