From 78acbca89bb89365fe86ccc717208705833a4da4 Mon Sep 17 00:00:00 2001 From: Erik Hedvall Date: Sun, 16 Jul 2023 21:52:20 +0200 Subject: [PATCH] Implement CAM16 --- .github/workflows/ci.yml | 4 +- CONTRIBUTING.md | 194 ++++- benchmarks/benches/cie.rs | 12 +- no_std_test/Cargo.toml | 3 +- palette/src/cam16.rs | 152 ++++ palette/src/cam16/full.rs | 778 +++++++++++++++++ palette/src/cam16/math.rs | 563 +++++++++++++ palette/src/cam16/math/chromaticity.rs | 96 +++ palette/src/cam16/math/luminance.rs | 57 ++ palette/src/cam16/parameters.rs | 300 +++++++ palette/src/cam16/partial.rs | 793 ++++++++++++++++++ palette/src/cam16/ucs_jab.rs | 369 ++++++++ palette/src/cam16/ucs_jmh.rs | 451 ++++++++++ palette/src/color_difference.rs | 2 +- palette/src/hues.rs | 6 + palette/src/lab.rs | 15 +- palette/src/lib.rs | 1 + palette/src/macros/casting.rs | 44 +- palette/src/macros/clamp.rs | 3 + palette/src/macros/convert.rs | 2 +- palette/src/macros/equality.rs | 4 +- palette/src/num.rs | 32 + palette/src/num/libm.rs | 36 + palette/src/num/wide.rs | 14 + palette/src/serde/alpha_deserializer.rs | 10 +- palette/src/stimulus.rs | 8 +- palette/src/xyz.rs | 65 +- palette_derive/Cargo.toml | 3 +- palette_derive/src/alpha/with_alpha.rs | 21 +- palette_derive/src/cast/array_cast.rs | 26 +- palette_derive/src/color_types.rs | 670 +++++++++++++++ .../src/convert/from_color_unclamped.rs | 245 +++--- palette_derive/src/convert/mod.rs | 2 +- palette_derive/src/convert/util.rs | 189 ++--- palette_derive/src/lib.rs | 25 +- palette_derive/src/meta/field_attributes.rs | 23 +- palette_derive/src/meta/mod.rs | 33 +- .../src/meta/type_item_attributes.rs | 200 ++--- palette_derive/src/util.rs | 8 - 39 files changed, 4983 insertions(+), 476 deletions(-) create mode 100644 palette/src/cam16.rs create mode 100644 palette/src/cam16/full.rs create mode 100644 palette/src/cam16/math.rs create mode 100644 palette/src/cam16/math/chromaticity.rs create mode 100644 palette/src/cam16/math/luminance.rs create mode 100644 palette/src/cam16/parameters.rs create mode 100644 palette/src/cam16/partial.rs create mode 100644 palette/src/cam16/ucs_jab.rs create mode 100644 palette/src/cam16/ucs_jmh.rs create mode 100644 palette_derive/src/color_types.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc73f0650..b20727575 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,8 @@ jobs: run: cargo clippy -v -p palette --no-default-features --features "std find-crate" - name: Default check run: cargo clippy -v -p palette + - name: Check all features + run: cargo clippy -v -p palette --all-features - name: Test all features run: cargo test -v -p palette --all-features - name: Test each feature @@ -80,7 +82,7 @@ jobs: with: targets: thumbv6m-none-eabi - name: "Build with #[no_std]" - run: cargo build -v -p no_std_test --features nightly --target thumbv6m-none-eabi + run: cargo build -v -p no_std_test --all-features --target thumbv6m-none-eabi miri: name: Miri tests runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e6f46c15..07f8e6c8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Have a shiny new feature in store? Remember to explain it thoroughly and provide ## Testing -Every pull request is automatically tested with continuous integration to deny warnings and any missing documentation. It's a good idea to run your local tests with `RUSTFLAGS="-D warnings" cargo test` and also to run `cargo check` and `cargo build` with the compiler flag prepended. This will make sure that there are no warnings or missing documentation, all for the benefit of the user. +Every pull request is automatically tested with continuous integration to deny warnings and any missing documentation. The test suite will thoroughly test each feature separately, but it's a good idea to have ran your local tests with `RUSTFLAGS="-D warnings" cargo test -p palette -p integration_tests --all-features` and also to run `cargo clippy` before submitting your changes. This will make sure that there are no warnings or missing documentation, all for the benefit of the user. Visual Studio Code users can make use of the settings in the `.vscode` folder. They set the default check command and Cargo features, among other things. There are a number of programs in the `examples` directory that demonstrate applications of the library in more "real" code. The output of these should be checked to see if they are affected by changes made. @@ -38,6 +38,8 @@ mod test { } ``` +Pull requests track the test coverage, but it's not a hard requirement for acceptance. More of a reminder of missing test cases. + ### Regression Tests Each time a bug is fixed, a test of some sort (most likely a unit test) should be added to check that the reported bug has been fixed in the reported use case. This is to prevent the bug from reappearing in the future. The test case may, of course, be expanded to check for more than just the reported case. @@ -59,8 +61,7 @@ but rather like this: * Add the missing saturation validation in Hsv * Make Color implement the Mix trait -Notice how they are written as if they were instructions. Try not to write -them in past tense. +Notice how they are written as if they were instructions. They are usually not to written in past tense. \* Fixup commits are any commits that fix mistakes in other commits within the same pull request. Squashing them into the "original" commits makes the history easier to follow. @@ -74,31 +75,13 @@ Pull requests that close issues need to mention it in the description. A closed Pull requests that break backwards compatibility should say so in the end of the description, to make sure it's easy to find. -Here is an example PR: - ->## Translate the library to British English -> ->The whole library is translated to the one and only Queen's English. -> ->### Closed Issues -> ->* This closes #123, by changing `Color` to `Colour`. -> ->### Breaking Change -> ->This changes the name of a number of identifiers to their British spelling. - -That's about it, depending on the size of the contribution. +You will see a template when opening a pull request. Just uncomment the parts you need and remove the rest. [closing_commits]: https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword ## Code Style -The code style is whatever `rustfmt` produces. Running `rustfmt` or `cargo fmt` before committing, or while editing, is therefore strongly recommended. - -### Long Lines - -`rustfmt` will typically take care of line wrapping, but in cases where it can't, the recommended line length is somewhere around 80 to 120 characters. Try to prioritize readability and break up any complex expressions that complicate formatting. +The code style is whatever `rustfmt` produces. Running `rustfmt` or `cargo fmt` before committing, or while editing, is therefore strongly recommended. `rustfmt` will typically take care of line wrapping, but in cases where it can't, the recommended line length is somewhere around 80 to 120 characters. Try to prioritize readability and break up any complex expressions that complicate formatting. ### Documentation @@ -107,3 +90,168 @@ There are lints in place to make documentation a requirement. Remember to keep b Documentation comments are usually capped to 80 characters for source readability. This can be done automatically in some editors like SublimeText's Alt+Q, or via plugins like [Rewrap][rewrap] for Visual Studio Code. Some editors allow for visual rulers to indicate an 80 character width. [rewrap]: https://marketplace.visualstudio.com/items?itemName=stkb.rewrap + +## Adding a Color Type + +Color types have grown in size and complexity since this library was first created. It's usually easiest to look at an existing color type that's similar to the new one, and implement the same traits and methods. Here's a set of guidelines for how to implement a color type and what's recommended to add. + +### Naming + +Try to use the color space's typical name, but also follow Rust's naming convention. For example RGB becomes `Rgb`, with the first letter of the acronym capitalized. xyY, however, became `Yxy` to avoid capitalizing the x and keeping th Y capitalized. The name should also be globally unique, if possible. For example `Okhsl`, rather than just `Hsl` that would collide with the more common, RGB based HSL. Type names should be clear, but reasonably short. + +Component/channel names should be spelled out, if possible, since these names can prioritize clear text over brevity. Such as `red` instead of just `r`. In some cases, such as in XYZ, there are no "full names". + +### The Type + +Most color types are parametric over their component type and a meta type. The meta type may be a white point or some sort of standard. Some color types, such as `Xyz` has a meta type for convenience, even though it's white point agnostic. The meta parameter should be wrapped in `PhantomData`. The properties should be in the same order as the type name suggests. For example, if the type name is `Abc`, the order is `a`, `b`, then `c`. + +Color types are also `#[repr(C)]` or `#[repr(transparent)]`, so they can be cast to arrays. More on this later. + +An example of a color without a hue may look like this: + +```rust +#[repr(C)] +struct MyColor { + a: T, + b: T, + c: T, + white_point: PhantomData, +} +``` + +An example of a color with a hue may look like this: + +```rust +#[repr(C)] +struct MyColor { + hue: MyHue, + c: T, + l: T, + white_point: PhantomData, +} +``` + +The hue type (`MyHue` in the example) should be added in the `hues.rs` module, if the color needs its own hue. This is not necessary if it's based on an already existing definition of hue. + +### Constructors + +The set of constructors (`new` methods) differs depending on the type of color space. The input values should be in the same order as the type name suggests. For example, if the type name is `Abc`, the order is `a`, `b`, then `c`. + +Colors without a hue: + +* `pub const fn new(a: T, b: T, c: T) -> Self` - The main constructor. +* `pub fn from_components((a, b, c): (T, T, T)) -> Self` - Constructs the type from a tuple. This can just call `new` internally. + +Colors with a hue: + +* `pub fn new>(hue: H, c: T, l: T) -> Self` - The main constructor, which converts hue values to the hue type. This cannot be `const`, due to the lack of support for `const` traits. +* `pub const fn new_const(hue: MyHue, c: T, l: T) -> Self` - An extra `const` constructor, which takes the hue as an already wrapped value. +* `pub fn from_components>>((hue, c, l): (H, T, T)) -> Self` - Constructs the type from a tuple and converts the hue. This can just call `new` internally. + +### Other Common Methods + +* `pub fn into_components(self) -> (T, T, T)` or `pub fn into_components(self) -> (MyHue, T, T)` - The opposite of `from_components`. The output values should be in the same order as the input values are when `from_components` is called. +* `pub fn min_a() -> T` and `pub fn max_a() -> T` - Helper methods for getting the typical minimum and maximum of each component. Some types don't have this defined. + +### Standard Traits + +The standard library provides a number of useful traits that makes the types easier to work with. Some of them can be derived, but they may need to be implemented manually if the color type has a meta type. The derive macro would otherwise limit the meta type as well: + +```rust +#[derive(Clone)] // Will require `Wp` to be Clone! +#[repr(C)] +struct MyColor { + a: T, + b: T, + c: T, + white_point: PhantomData, +} +``` + +There are also macros for some traits, since a some of the require a lot of repeating code. + +Recommended standard traits for all color types: + +* `Clone` and `Copy` - Using `impl_copy_clone!`. May be derived if there's no meta type. +* `Debug` - Fine to derive. +* `PartialEq` and `Eq` - Using `impl_eq!` or `impl_eq_hue!`. +* `Default` - Don't derive if there's a meta type. "Default" and "black" are currently conflated, so no macro available. See [#324]. +* `Add` - Using `impl_color_add!`. +* `Sub` - Using `impl_color_sub!`. + +Additional traits for colors without hue: + +* `Mul` - Using `impl_color_mul!`. +* `Div` - Using `impl_color_div!`. + +Colors that are usually packed with different component orderings (such as RGB): + +* `From>` and vice versa. + +Colors with one single component (such as gray/luma): + +* `AsRef`, `AsMut`, `From`, `From<&T>`, `From<&mut T>`, and vise versa - Conversions to and from the bare component value. See `Luma` for reference. + +Colors with a hexadecimal representation: + +* `LowerHex` - Exclude the `#` or similar sigils. +* `UpperHex` - Exclude the `#` or similar sigils. + +[#324]: https://github.com/Ogeon/palette/issues/324 + +### `palette` Traits + +Many of the traits in `palette` are implemented using macros. The recommendation is currently to look at a similar color type and copy the macros for it. Some recommended traits will still require manual implementation: + +All color types: + +* `ArrayCast` - Derived. +* `FromColorUnclamped` - See _Color Conversion_ below. +* `WithAlpha` - Derived. +* `HasBoolMask`. + +For stimulus colors, such as RGB and XYZ: + +* `StimulusColor`. + +Colors with one single component (such as gray/luma): + +* `UintCast`. + +### Third Party Traits + +Types from `palette` implement traits from some third party crates. Some of them are covered by macros, while some are implemented manually: + +* `approx` - Using `impl_eq!` or `impl_eq_hue!`. +* `rand` - Using one of `impl_rand_traits_cartesian!`, `impl_rand_traits_cylinder!`, `impl_rand_traits_hsv_cone!`, `impl_rand_traits_hsl_bicone!`, or `impl_rand_traits_hwb_cone!`, depending on which shape the volume of typically valid colors resembles. +* `bytemuck` - The `Zeroable` and `Pod` traits are implemented manually for types that support them. + +### Color Conversion + +The central trait for color conversion is `FromColorUnclamped`. This should be derived for the color type to implement all combinations of conversions. The number of conversion combinations grows exponentially, so you don't want to do it by hand. Some manual conversions are also necessary. + +#### In The `palette_derive` Crate + +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. + +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 + +Derive `FromColorUnclamped` and add a `#[palette(palette_internal)]` (and more parameters) attribute should be added. The `palette_internal` parameter makes the derive macro find the types and modules in `crate::`. + +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: + +* Implement `FromColorUnclamped for MyType`, usually as a unit conversion. This is not blanket implemented, to allow the case when it's not a unit conversion. +* Implement `FromColorUnclamped for MyType` for converting from the "parent type" this color type is connected to in `PREFERRED_CONVERSION_SOURCE`. The parent type need a `FromColorUnclamped>` implementation, too. Also, make sure to mention it in `skip_derives`. + +#### Enabling `FromColor` And `TryFromColor` + +The `FromColor` and `TryFromColor` (as well as their `Into` counterparts) are blanket implemented for types that implement `Clamp` and `IsWithinBounds`, respectively, using `impl_clamp!` and `impl_is_within_bounds!`. These traits limit the values to the typical ranges for the color space. For example, `Rgb` has its components limited to `0.0..=1.0` if they are `f32` or `f64`. Implementing these traits will also make the color type implement `FromColor` and `TryFromColor`. They are also generally good to add. diff --git a/benchmarks/benches/cie.rs b/benchmarks/benches/cie.rs index b8bd31fe5..e6ecf3fe6 100644 --- a/benchmarks/benches/cie.rs +++ b/benchmarks/benches/cie.rs @@ -1,6 +1,6 @@ use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Criterion}; use palette::{ - color_difference::{Ciede2000, DeltaE}, + color_difference::{Ciede2000, DeltaE, ImprovedDeltaE}, convert::FromColorUnclamped, }; use palette::{Lab, Lch, Xyz, Yxy}; @@ -164,6 +164,16 @@ fn cie_delta_e(c: &mut Criterion) { }) }); + group.bench_with_input("Lab improved delta E", &lab, |b, lab| { + b.iter(|| { + for &lhs in lab { + for &rhs in lab.iter().rev() { + black_box(lhs.improved_delta_e(rhs)); + } + } + }) + }); + group.bench_with_input("Lab CIEDE2000", &lab, |b, lab| { b.iter(|| { for &lhs in lab { diff --git a/no_std_test/Cargo.toml b/no_std_test/Cargo.toml index 9a89d4dd6..b7f474309 100644 --- a/no_std_test/Cargo.toml +++ b/no_std_test/Cargo.toml @@ -15,6 +15,8 @@ bench = false [features] nightly = [] +# Avoids getting these features included in other packages in the same workspace. +all_features = ["palette/libm", "palette/named_from_str"] [dependencies.libc] version = "0.2" @@ -23,4 +25,3 @@ default-features = false [dependencies.palette] path = "../palette" default-features = false -features = ["libm", "named_from_str"] diff --git a/palette/src/cam16.rs b/palette/src/cam16.rs new file mode 100644 index 000000000..36ac1e237 --- /dev/null +++ b/palette/src/cam16.rs @@ -0,0 +1,152 @@ +//! Types for the CIE CAM16 color appearance model. +//! +//! CIE CAM16 is a color appearance model that tries to predict the appearance +//! of a color under certain viewing conditions, as specified via the +//! [`Parameters`] type. The [`Cam16`] type has descriptions for the CAM16 +//! attributes. The [Color appearance model page on Wikipedia][wikipedia_cam] +//! has some history and background as well. +//! +//! # Converting Between XYZ and CAM16 +//! +//! The CIE CAM16 implementation in Palette has the [`Cam16`] type and its +//! partial variants on one side of the boundary, and [`Xyz`](crate::Xyz) on the +//! other. Going between `Xyz` and `Cam16` requires the viewing conditions to be +//! specified as [`Parameters`]. +//! +//! ``` +//! use palette::{ +//! Srgb, FromColor, IntoColor, +//! cam16::{Cam16, Parameters}, +//! }; +//! +//! // Customize these according to the viewing conditions: +//! let mut example_parameters = Parameters::default_static_wp(40.0); +//! +//! // CAM16 from sRGB, or most other color spaces: +//! let rgb = Srgb::new(0.3f32, 0.8, 0.1); +//! let cam16_from_rgb = Cam16::from_xyz(rgb.into_color(), example_parameters); +//! +//! // sRGB from CAM16, using lightness, chroma and hue by default: +//! let rgb_from_cam16 = Srgb::from_color(cam16_from_rgb.into_xyz(example_parameters)); +//! ``` +//! +//! For more control over the attributes to use when converting from CAM16, one +//! of the partial CAM16 types can be used: +//! +//! * [`Cam16Jch`](crate::cam16::Cam16Jch): lightness and chroma. +//! * [`Cam16Jmh`](crate::cam16::Cam16Jmh): lightness and colorfulness. +//! * [`Cam16Jsh`](crate::cam16::Cam16Jsh): lightness and saturation. +//! * [`Cam16Qch`](crate::cam16::Cam16Qch): brightness and chroma. +//! * [`Cam16Qmh`](crate::cam16::Cam16Qmh): brightness and colorfulness. +//! * [`Cam16Qsh`](crate::cam16::Cam16Qsh): brightness and saturation. +//! +//! Generic traits and functions can make use of the [`IntoCam16Unclamped`], +//! [`FromCam16Unclamped`], [`Cam16IntoUnclamped`], and [`Cam16FromUnclamped`] +//! traits. They are similar to the traits from the [`convert`][crate::convert] +//! module and help abstracting away most of the implementation details. +//! +//! # The CAM16-UCS Color Space +//! +//! CIE CAM16 specifies a visually uniform color space that can be used for +//! color manipulation. It's represented by the [`Cam16UcsJmh`] and +//! [`Cam16UcsJab`] types, similar to [`Lch`][crate::Lch] and +//! [`Lab`][crate::Lab]. +//! +//! ``` +//! use palette::{ +//! Srgb, FromColor, IntoColor, +//! cam16::{Cam16Jmh, Parameters, Cam16UcsJmh}, +//! }; +//! +//! // Customize these according to the viewing conditions: +//! let mut example_parameters = Parameters::default_static_wp(40.0); +//! +//! // CAM16-UCS from sRGB, or most other color spaces: +//! let rgb = Srgb::new(0.3f32, 0.8, 0.1); +//! let cam16 = Cam16Jmh::from_xyz(rgb.into_color(), example_parameters); +//! let mut ucs_from_rgb = Cam16UcsJmh::from_color(cam16); +//! +//! // Shift the hue by 120 degrees in CAM16-UCS: +//! ucs_from_rgb.hue += 120.0; +//! +//! // Convert back to sRGB under the same viewing conditions: +//! let rgb = Srgb::from_color(Cam16Jmh::from_color(ucs_from_rgb).into_xyz(example_parameters)); +//! ``` +//! +//! [wikipedia_cam]: https://en.wikipedia.org/wiki/Color_appearance_model + +pub use full::*; +pub use parameters::*; +pub use partial::*; +pub use ucs_jab::{Cam16UcsJab, Cam16UcsJaba, Iter as Cam16UcsJabIter}; +pub use ucs_jmh::{Cam16UcsJmh, Cam16UcsJmha, Iter as Cam16UcsJmhIter}; + +#[cfg(feature = "random")] +pub use ucs_jab::UniformCam16UcsJab; +#[cfg(feature = "random")] +pub use ucs_jmh::UniformCam16UcsJmh; + +mod full; +pub(crate) mod math; +mod parameters; +mod partial; +mod ucs_jab; +mod ucs_jmh; + +/// A trait for converting into a CAM16 color type from `C` without clamping. +pub trait Cam16FromUnclamped { + /// The number type that's used in `parameters` when converting. + type Scalar; + + /// Converts `color` into `Self`, using the provided parameters. + fn cam16_from_unclamped(color: C, parameters: BakedParameters) -> Self; +} + +/// A trait for converting into a CAM16 color type `C` without clamping. +pub trait IntoCam16Unclamped { + /// The number type that's used in `parameters` when converting. + type Scalar; + + /// Converts `self` into `C`, using the provided parameters. + fn into_cam16_unclamped(self, parameters: BakedParameters) -> C; +} + +impl IntoCam16Unclamped for U +where + T: Cam16FromUnclamped, +{ + type Scalar = T::Scalar; + + fn into_cam16_unclamped(self, parameters: BakedParameters) -> T { + T::cam16_from_unclamped(self, parameters) + } +} + +/// A trait for converting from a CAM16 color type `C` without clamping. +pub trait FromCam16Unclamped { + /// The number type that's used in `parameters` when converting. + type Scalar; + + /// Converts `cam16` into `Self`, using the provided parameters. + fn from_cam16_unclamped(cam16: C, parameters: BakedParameters) -> Self; +} + +/// A trait for converting from a CAM16 color type into `C` without clamping. +pub trait Cam16IntoUnclamped { + /// The number type that's used in `parameters` when converting. + type Scalar; + + /// Converts `self` into `C`, using the provided parameters. + fn cam16_into_unclamped(self, parameters: BakedParameters) -> C; +} + +impl Cam16IntoUnclamped for U +where + T: FromCam16Unclamped, +{ + type Scalar = T::Scalar; + + fn cam16_into_unclamped(self, parameters: BakedParameters) -> T { + T::from_cam16_unclamped(self, parameters) + } +} diff --git a/palette/src/cam16/full.rs b/palette/src/cam16/full.rs new file mode 100644 index 000000000..b78caee67 --- /dev/null +++ b/palette/src/cam16/full.rs @@ -0,0 +1,778 @@ +use crate::{ + angle::RealAngle, + bool_mask::{HasBoolMask, LazySelect}, + hues::Cam16Hue, + num::{Abs, Arithmetics, FromScalar, PartialCmp, Powf, Real, Signum, Sqrt, Trigonometry, Zero}, + Alpha, GetHue, Xyz, +}; + +use super::{ + BakedParameters, Cam16FromUnclamped, Cam16IntoUnclamped, Cam16Jch, Cam16Jmh, Cam16Jsh, + Cam16Qch, Cam16Qmh, Cam16Qsh, FromCam16Unclamped, IntoCam16Unclamped, WhitePointParameter, +}; + +/// CIE CAM16 with an alpha component. +/// +/// See the [`Cam16a` implementation in `Alpha`](crate::Alpha#Cam16a). +pub type Cam16a = Alpha, T>; + +/// 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 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 used when converting _from_ CAM16, since they are +/// correlated and derived from each other. This library also provides partial +/// versions of this struct, to make it easier to correctly specify a minimum +/// attribute set. +/// +/// The full list of partial CAM16 variants is: +/// +/// * [`Cam16Jch`](crate::cam16::Cam16Jch): lightness and chroma. +/// * [`Cam16Jmh`](crate::cam16::Cam16Jmh): lightness and colorfulness. +/// * [`Cam16Jsh`](crate::cam16::Cam16Jsh): lightness and saturation. +/// * [`Cam16Qch`](crate::cam16::Cam16Qch): brightness and chroma. +/// * [`Cam16Qmh`](crate::cam16::Cam16Qmh): brightness and colorfulness. +/// * [`Cam16Qsh`](crate::cam16::Cam16Qsh): brightness and saturation. +/// +/// # CAM16-UCS +/// +/// While CIE CAM16 is a model of color appearance, it's not necessarily +/// suitable as a color space. Instead, there is the CAM16-UCS (CAM16 uniform +/// color space), that's based off of the lightness, colorfulness and hue +/// attributes. This colorspace is represented by the +/// [`Cam16UcsJmh`][crate::cam16::Cam16UcsJmh] and +/// [`Cam16UcsJab`][crate::cam16::Cam16UcsJab] types. +/// +/// # Creating a Value +/// +/// A `Cam16` value would typically come from another color space, or one of the +/// partial sets of CAM16 attributes. All of which require known viewing +/// conditions. +/// +/// ``` +/// use palette::{ +/// Srgb, FromColor, IntoColor, +/// cam16::{Cam16, Parameters, Cam16Jmh, Cam16UcsJmh}, +/// }; +/// +/// // Customize these according to the viewing conditions: +/// let mut example_parameters = Parameters::default_static_wp(40.0); +/// +/// // CAM16 from sRGB, or most other color spaces: +/// let rgb = Srgb::new(0.3f32, 0.8, 0.1); +/// let cam16_from_rgb = Cam16::from_xyz(rgb.into_color(), example_parameters); +/// +/// // Full CAM16 from a partial set (any partial set can be used): +/// let partial = Cam16Jmh::new(50.0f32, 80.0, 120.0); +/// let cam16_from_partial = partial.into_full(example_parameters); +/// +/// // Full CAM16 from CAM16-UCS J'M'h': +/// let ucs = Cam16UcsJmh::new(50.0f32, 80.0, 120.0); +/// let cam16_from_ucs = Cam16Jmh::from_color(ucs).into_full(example_parameters); +/// ``` +#[derive(Clone, Copy, Debug, WithAlpha, Default)] +#[palette(palette_internal, component = "T")] +#[repr(C)] +pub struct Cam16 { + /// The [lightness](https://cie.co.at/eilvterm/17-22-063) (J) of the + /// color. + /// + /// It's a perception of the color's luminance, but not linear to it, and is + /// relative to the reference white. The lightness of black is `0.0` and the + /// lightness of white is `100.0`. + /// + /// Lightness behaves similarly to L\* in [`Lch`][crate::Lch] or lightness + /// in [`Hsl`][crate::Hsl]. + /// + /// See also . + #[doc(alias = "J")] + pub lightness: T, + + /// The [chroma](https://cie.co.at/eilvterm/17-22-074) (C) of + /// the color. + /// + /// It's how chromatic the color appears in comparison with a grey color of + /// the same lightness. Changing the perceived chroma doesn't change the + /// perceived lightness, and vice versa. + /// + /// Chroma behaves similarly to chroma in [`Lch`][crate::Lch] or saturation + /// in [`Hsl`][crate::Hsl]. + /// + /// See also . + #[doc(alias = "C")] + pub chroma: T, + + /// The [hue](https://cie.co.at/eilvterm/17-22-067) (h) of the color. + /// + /// The color's position around a color circle, in degrees. + /// + /// See also . + #[doc(alias = "h")] + pub hue: Cam16Hue, + + /// The [brightness](https://cie.co.at/eilvterm/17-22-059) (Q) of the + /// color. + /// + /// It's the perception of how much light appears to shine from an object. + /// As opposed to `lightness`, this is not in comparison to a reference + /// white, but in more absolute terms. Lightness and brightness area also + /// not linearly correlated in CAM16. + /// + /// Brightness behaves similarly to value in [`Hsv`][crate::Hsv]. + /// + /// See also . + #[doc(alias = "Q")] + pub brightness: T, + + /// The [colorfulness](https://cie.co.at/eilvterm/17-22-072) (M) of the + /// color. + /// + /// It's a perception of how chromatic the color is and usually increases + /// with luminance, unless the brightness is very high. + /// + /// See also . + #[doc(alias = "M")] + pub colorfulness: T, + + /// The [saturation](https://cie.co.at/eilvterm/17-22-073) + /// (s) of the color. + /// + /// It's the colorfulness of a color in proportion to its own brightness. + /// The perceived saturation should stay the same when the perceived + /// brightness changes, and vice versa. + /// + /// Saturation behaves similarly to saturation in [`Hsv`][crate::Hsv]. + /// + /// See also . + #[doc(alias = "s")] + pub saturation: T, +} + +impl Cam16 { + /// Derive CIE CAM16 attributes for the provided color, under the provided + /// viewing conditions. + /// + /// ``` + /// use palette::{Srgb, IntoColor, cam16::{Cam16, Parameters}}; + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let rgb = Srgb::new(0.3f32, 0.8, 0.1); + /// let cam16 = Cam16::from_xyz(rgb.into_color(), example_parameters); + /// ``` + /// + /// It's also possible to "pre-bake" the parameters, to avoid recalculate + /// some of the derived values when converting multiple color value. + /// + /// ``` + /// use palette::{Srgb, IntoColor, cam16::{Cam16, Parameters}}; + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let baked_parameters = example_parameters.bake(); + /// + /// let rgb = Srgb::new(0.3f32, 0.8, 0.1); + /// let cam16 = Cam16::from_xyz(rgb.into_color(), baked_parameters); + /// ``` + #[inline] + pub fn from_xyz( + color: Xyz, + parameters: impl Into>, + ) -> Self + where + Xyz: IntoCam16Unclamped, + T: FromScalar, + WpParam: WhitePointParameter, + { + color.into_cam16_unclamped(parameters.into()) + } + + /// 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 one of the + /// partial CAM16 representations for more control over which set of + /// attributes that should be. + ///

+ /// + /// ``` + /// use palette::{Srgb, FromColor, cam16::{Cam16, Parameters}}; + /// # fn get_cam16_value() -> Cam16 {Cam16::default()} + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let cam16: Cam16 = get_cam16_value(); + /// let rgb = Srgb::from_color(cam16.into_xyz(example_parameters)); + /// ``` + /// + /// It's also possible to "pre-bake" the parameters, to avoid recalculate + /// some of the derived values when converting multiple color value. + /// + /// ``` + /// use palette::{Srgb, FromColor, cam16::{Cam16, Parameters}}; + /// # fn get_cam16_value() -> Cam16 {Cam16::default()} + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let baked_parameters = example_parameters.bake(); + /// + /// let cam16: Cam16 = get_cam16_value(); + /// let rgb = Srgb::from_color(cam16.into_xyz(baked_parameters)); + /// ``` + #[inline] + pub fn into_xyz( + self, + parameters: impl Into>, + ) -> Xyz + where + Self: Cam16IntoUnclamped, Scalar = T::Scalar>, + WpParam: WhitePointParameter, + T: FromScalar, + { + self.cam16_into_unclamped(parameters.into()) + } +} + +///[`Cam16a`](crate::cam16::Cam16a) implementations. +impl Alpha, A> { + /// Derive CIE CAM16 attributes with transparency for the provided color, + /// under the provided viewing conditions. + /// + /// ``` + /// use palette::{Srgba, IntoColor, cam16::{Cam16a, Parameters}}; + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let rgba = Srgba::new(0.3f32, 0.8, 0.1, 0.9); + /// let cam16a = Cam16a::from_xyz(rgba.into_color(), example_parameters); + /// ``` + /// + /// It's also possible to "pre-bake" the parameters, to avoid recalculate + /// some of the derived values when converting multiple color value. + /// + /// ``` + /// use palette::{Srgba, IntoColor, cam16::{Cam16a, Parameters}}; + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let baked_parameters = example_parameters.bake(); + /// + /// let rgba = Srgba::new(0.3f32, 0.8, 0.1, 0.9); + /// let cam16a = Cam16a::from_xyz(rgba.into_color(), baked_parameters); + /// ``` + #[inline] + pub fn from_xyz( + color: Alpha, A>, + parameters: impl Into>, + ) -> Self + where + Xyz: IntoCam16Unclamped, Scalar = T::Scalar>, + T: FromScalar, + WpParam: WhitePointParameter, + { + let Alpha { color, alpha } = color; + + Alpha { + color: Cam16::from_xyz(color, parameters), + alpha, + } + } + + /// Construct an XYZ color with transparency, 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 one of the + /// partial CAM16 representations for more control over which set of + /// attributes that should be. + ///

+ /// + /// ``` + /// use palette::{Srgba, FromColor, cam16::{Cam16a, Parameters}}; + /// # fn get_cam16a_value() -> Cam16a {Cam16a::default()} + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let cam16a = get_cam16a_value(); + /// let rgba = Srgba::from_color(cam16a.into_xyz(example_parameters)); + /// ``` + /// + /// It's also possible to "pre-bake" the parameters, to avoid recalculate + /// some of the derived values when converting multiple color value. + /// + /// ``` + /// use palette::{Srgba, FromColor, cam16::{Cam16a, Parameters}}; + /// # fn get_cam16a_value() -> Cam16a {Cam16a::default()} + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let baked_parameters = example_parameters.bake(); + /// + /// let cam16a = get_cam16a_value(); + /// let rgba = Srgba::from_color(cam16a.into_xyz(baked_parameters)); + /// ``` + #[inline] + pub fn into_xyz( + self, + parameters: impl Into>, + ) -> Alpha, A> + where + Cam16: Cam16IntoUnclamped, Scalar = T::Scalar>, + WpParam: WhitePointParameter, + T: FromScalar, + { + let Alpha { color, alpha } = self; + + Alpha { + color: color.into_xyz(parameters), + alpha, + } + } +} + +impl Cam16FromUnclamped> for Cam16 +where + WpParam: WhitePointParameter, + T: Real + + FromScalar + + Arithmetics + + Powf + + Sqrt + + Abs + + Signum + + Trigonometry + + RealAngle + + Clone, + T::Scalar: Clone, +{ + type Scalar = T::Scalar; + + fn cam16_from_unclamped( + color: Xyz, + parameters: BakedParameters, + ) -> Self { + super::math::xyz_to_cam16(color.with_white_point(), parameters.inner) + } +} + +macro_rules! impl_from_cam16_partial { + ($($name: ident),+) => { + $( + impl Cam16FromUnclamped> for Cam16 + where + WpParam: WhitePointParameter, + T: Real + FromScalar + Zero + Arithmetics + Sqrt + PartialCmp + Clone, + T::Mask: LazySelect + Clone, + T::Scalar: Clone + { + type Scalar = T::Scalar; + + fn cam16_from_unclamped( + cam16: $name, + parameters: crate::cam16::BakedParameters, + ) -> Self { + let ( + luminance, + chromaticity, + hue, + ) = cam16.into_dynamic(); + + let (lightness, brightness) = luminance.into_cam16(parameters.clone()); + let (chroma, colorfulness, saturation) = + chromaticity.into_cam16(lightness.clone(), parameters); + + Cam16 { + lightness, + chroma, + hue, + brightness, + colorfulness, + saturation, + } + } + } + + impl FromCam16Unclamped> for Cam16 + where + Self: Cam16FromUnclamped>, + { + type Scalar = >>::Scalar; + + fn from_cam16_unclamped( + cam16: $name, + parameters: crate::cam16::BakedParameters, + ) -> Self { + Self::cam16_from_unclamped(cam16, parameters) + } + } + )+ + }; +} + +impl_from_cam16_partial!(Cam16Jmh, Cam16Jch, Cam16Jsh, Cam16Qmh, Cam16Qch, Cam16Qsh); + +impl GetHue for Cam16 +where + T: Clone, +{ + type Hue = Cam16Hue; + + fn get_hue(&self) -> Cam16Hue { + self.hue.clone() + } +} + +impl HasBoolMask for Cam16 +where + T: HasBoolMask, +{ + type Mask = T::Mask; +} + +// Macro implementations + +impl_is_within_bounds! { + Cam16 { + lightness => [T::zero(), None], + chroma => [T::zero(), None], + brightness => [T::zero(), None], + colorfulness => [T::zero(), None], + saturation => [T::zero(), None] + } + where T: Zero +} +impl_clamp! { + Cam16 { + lightness => [T::zero()], + chroma => [T::zero()], + brightness => [T::zero()], + colorfulness => [T::zero()], + saturation => [T::zero()] + } + other {hue} + where T: Zero +} + +impl_eq_hue!( + Cam16, + Cam16Hue, + [lightness, chroma, brightness, colorfulness, saturation] +); +impl_simd_array_conversion_hue!( + Cam16, + [lightness, chroma, brightness, colorfulness, saturation] +); + +// Unit test + +#[cfg(test)] +#[cfg(feature = "approx")] +mod test { + use crate::{ + cam16::{ + math::{chromaticity::ChromaticityType, luminance::LuminanceType}, + BakedParameters, Cam16Jch, Parameters, + }, + convert::{FromColorUnclamped, IntoColorUnclamped}, + Srgb, + }; + + use super::Cam16; + + macro_rules! assert_cam16_to_rgb { + ($cam16:expr, $rgb:expr, $($params:tt)*) => { + let cam16 = $cam16; + let parameters = BakedParameters::from(Parameters::TEST_DEFAULTS); + + let rgb: Srgb = cam16.into_xyz(parameters).into_color_unclamped(); + assert_relative_eq!(rgb, $rgb, $($params)*); + + let chromaticities = [ + ChromaticityType::Chroma(cam16.chroma), + ChromaticityType::Colorfulness(cam16.colorfulness), + ChromaticityType::Saturation(cam16.saturation), + ]; + let luminances = [ + LuminanceType::Lightness(cam16.lightness), + LuminanceType::Brightness(cam16.brightness), + ]; + + for luminance in luminances { + for chromaticity in chromaticities { + let partial = ( + luminance, + chromaticity, + cam16.hue, + ); + + let xyz = crate::cam16::math::cam16_to_xyz(partial, parameters.inner).with_white_point(); + + assert_relative_eq!( + Srgb::::from_color_unclamped(xyz), + $rgb, + $($params)* + ); + } + } + }; + } + + #[test] + fn converts_with_jch() { + let parameters = Parameters::TEST_DEFAULTS.bake(); + let xyz = Srgb::from(0x5588cc).into_linear().into_color_unclamped(); + let mut cam16: Cam16 = Cam16::from_xyz(xyz, parameters); + let cam16jch = Cam16Jch::from_full(cam16); + + // Zero the other attributes so they produce errors if they are used. + cam16.brightness = 0.0; + cam16.colorfulness = 0.0; + cam16.saturation = 0.0; + + assert_eq!(cam16.into_xyz(parameters), cam16jch.into_xyz(parameters)); + } + + #[test] + fn example_blue() { + // Uses the example color from https://observablehq.com/@jrus/cam16 + let xyz = Srgb::from(0x5588cc).into_linear().into_color_unclamped(); + let mut cam16: Cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS); + cam16.hue = cam16.hue.into_positive_degrees().into(); + + assert_relative_eq!( + cam16, + Cam16 { + lightness: 45.544264720360346, + chroma: 45.07001048293764, + hue: 259.225345298129.into(), + brightness: 132.96974182692045, + colorfulness: 39.4130607870103, + saturation: 54.4432031413259, + }, + epsilon = 0.01 + ); + + assert_cam16_to_rgb!( + cam16, + Srgb::from(0x5588cc).into_format(), + epsilon = 0.0000001 + ); + } + + #[test] + fn black() { + // Checks against the output from https://observablehq.com/@jrus/cam16 + let xyz = Srgb::from(0x000000).into_linear().into_color_unclamped(); + let mut cam16: Cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS); + cam16.hue = cam16.hue.into_positive_degrees().into(); + + assert_relative_eq!( + cam16, + Cam16 { + lightness: 0.0, + chroma: 0.0, + hue: 0.0.into(), + brightness: 0.0, + colorfulness: 0.0, + saturation: 0.0, + }, + epsilon = 0.01 + ); + + assert_cam16_to_rgb!( + cam16, + Srgb::from(0x000000).into_format(), + epsilon = 0.0000001 + ); + } + + #[test] + fn white() { + // Checks against the output from https://observablehq.com/@jrus/cam16 + let xyz = Srgb::from(0xffffff).into_linear().into_color_unclamped(); + let mut cam16: Cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS); + cam16.hue = cam16.hue.into_positive_degrees().into(); + + assert_relative_eq!( + cam16, + Cam16 { + lightness: 99.99955537650459, + chroma: 2.1815254387079435, + hue: 209.49854407518228.into(), + brightness: 197.03120459014184, + colorfulness: 1.9077118865271965, + saturation: 9.839859256901553, + }, + epsilon = 0.1 + ); + + assert_cam16_to_rgb!( + cam16, + Srgb::from(0xffffff).into_format(), + epsilon = 0.0000001 + ); + } + + #[test] + fn red() { + // Checks against the output from https://observablehq.com/@jrus/cam16 + let xyz = Srgb::from(0xff0000).into_linear().into_color_unclamped(); + let mut cam16: Cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS); + cam16.hue = cam16.hue.into_positive_degrees().into(); + + assert_relative_eq!( + cam16, + Cam16 { + lightness: 46.23623443823762, + chroma: 113.27879472174797, + hue: 27.412485587695937.into(), + brightness: 133.9760614641257, + colorfulness: 99.06063864657237, + saturation: 85.98782392745971, + }, + epsilon = 0.01 + ); + + assert_cam16_to_rgb!(cam16, Srgb::from(0xff0000).into_format(), epsilon = 0.00001); + } + + #[test] + fn green() { + // Checks against the output from https://observablehq.com/@jrus/cam16 + let xyz = Srgb::from(0x00ff00).into_linear().into_color_unclamped(); + let mut cam16: Cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS); + cam16.hue = cam16.hue.into_positive_degrees().into(); + + assert_relative_eq!( + cam16, + Cam16 { + lightness: 79.23121430933533, + chroma: 107.77869525794452, + hue: 141.93451307926003.into(), + brightness: 175.38164288466993, + colorfulness: 94.25088262080988, + saturation: 73.30787758114869, + }, + epsilon = 0.01 + ); + + assert_cam16_to_rgb!( + cam16, + Srgb::from(0x00ff00).into_format(), + epsilon = 0.000001 + ); + } + + #[test] + fn blue() { + // Checks against the output from https://observablehq.com/@jrus/cam16 + let xyz = Srgb::from(0x0000ff).into_linear().into_color_unclamped(); + let mut cam16: Cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS); + cam16.hue = cam16.hue.into_positive_degrees().into(); + + assert_relative_eq!( + cam16, + Cam16 { + lightness: 25.22701796474445, + chroma: 86.59618504567312, + hue: 282.81848901862566.into(), + brightness: 98.96210767195342, + colorfulness: 75.72708922311855, + saturation: 87.47645277637828, + }, + epsilon = 0.01 + ); + + assert_cam16_to_rgb!( + cam16, + Srgb::from(0x0000ff).into_format(), + epsilon = 0.000001 + ); + } + + #[cfg(feature = "wide")] + #[test] + fn simd() { + let white_srgb = Srgb::from(0xffffff).into_format(); + let white_cam16 = Cam16 { + lightness: 99.99955537650459, + chroma: 2.1815254387079435, + hue: 209.49854407518228.into(), + brightness: 197.03120459014184, + colorfulness: 1.9077118865271965, + saturation: 9.839859256901553, + }; + + let red_srgb = Srgb::from(0xff0000).into_format(); + let red_cam16 = Cam16 { + lightness: 46.23623443823762, + chroma: 113.27879472174797, + hue: 27.412485587695937.into(), + brightness: 133.9760614641257, + colorfulness: 99.06063864657237, + saturation: 85.98782392745971, + }; + + let green_srgb = Srgb::from(0x00ff00).into_format(); + let green_cam16 = Cam16 { + lightness: 79.23121430933533, + chroma: 107.77869525794452, + hue: 141.93451307926003.into(), + brightness: 175.38164288466993, + colorfulness: 94.25088262080988, + saturation: 73.30787758114869, + }; + + let blue_srgb = Srgb::from(0x0000ff).into_format(); + let blue_cam16 = Cam16 { + lightness: 25.22701796474445, + chroma: 86.59618504567312, + hue: 282.81848901862566.into(), + brightness: 98.96210767195342, + colorfulness: 75.72708922311855, + saturation: 87.47645277637828, + }; + + let srgb = Srgb::::from([white_srgb, red_srgb, green_srgb, blue_srgb]); + let xyz = srgb.into_linear().into_color_unclamped(); + let mut cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS); + cam16.hue = cam16.hue.into_positive_degrees().into(); + + assert_relative_eq!( + &<[Cam16<_>; 4]>::from(cam16)[..], + &[white_cam16, red_cam16, green_cam16, blue_cam16][..], + epsilon = 0.1 + ); + + let srgb = Srgb::from_color_unclamped(cam16.into_xyz(Parameters::TEST_DEFAULTS)); + + assert_relative_eq!( + &<[Srgb<_>; 4]>::from(srgb)[..], + &[white_srgb, red_srgb, green_srgb, blue_srgb][..], + epsilon = 0.00001 + ); + } +} diff --git a/palette/src/cam16/math.rs b/palette/src/cam16/math.rs new file mode 100644 index 000000000..ff06bdd0b --- /dev/null +++ b/palette/src/cam16/math.rs @@ -0,0 +1,563 @@ +use core::{ + marker::PhantomData, + ops::{Div, Mul}, +}; + +use crate::{ + angle::{RealAngle, SignedAngle}, + bool_mask::{LazySelect, Select}, + clamp, + hues::Cam16Hue, + num::{ + Abs, Arithmetics, Clamp, Exp, FromScalar, One, PartialCmp, Powf, Real, Signum, Sqrt, + Trigonometry, Zero, + }, + white_point, + xyz::Xyz, +}; + +use super::{Cam16, Parameters}; + +use self::{chromaticity::ChromaticityType, luminance::LuminanceType}; + +pub(crate) mod chromaticity; +pub(crate) mod luminance; + +// This module is originally based on these sources: +// - https://observablehq.com/@jrus/cam16 +// - "Comprehensive color solutions: CAM16, CAT16, and CAM16-UCS" by Li C, Li Z, +// Wang Z, et al. +// (https://www.researchgate.net/publication/318152296_Comprehensive_color_solutions_CAM16_CAT16_and_CAM16-UCS) +// - "Algorithmic improvements for the CIECAM02 and CAM16 color appearance +// models" by Nico Schlömer (https://arxiv.org/pdf/1802.06067.pdf) +// - https://rawpedia.rawtherapee.com/CIECAM02. +// - "Usage Guidelines for CIECAM97s" by Nathan Moroney +// (https://www.imaging.org/common/uploaded%20files/pdfs/Papers/2000/PICS-0-81/1611.pdf) +// - "CIECAM02 and Its Recent Developments" by Ming Ronnier Luo and Changjun Li +// (https://cielab.xyz/pdf/CIECAM02_and_Its_Recent_Developments.pdf) +// - https://en.wikipedia.org/wiki/CIECAM02 + +pub(crate) fn xyz_to_cam16( + xyz: Xyz, + parameters: DependentParameters, +) -> Cam16 +where + T: Real + + FromScalar + + Arithmetics + + Powf + + Sqrt + + Abs + + Signum + + Trigonometry + + RealAngle + + Clone, + T::Scalar: Clone, +{ + let xyz = xyz.with_white_point() * T::from_f64(100.0); // The reference uses 0.0 to 100.0 instead of 0.0 to 1.0. + let d_rgb = map3(parameters.d_rgb.clone(), T::from_scalar); + + let [r_a, g_a, b_a] = map3(mul3(m16(xyz), d_rgb), |component| { + parameters.adapt.run(component) + }); + let a = r_a.clone() + (T::from_f64(-12.0) * &g_a + &b_a) / T::from_f64(11.0); // redness-greenness + let b = (r_a.clone() + &g_a - T::from_f64(2.0) * &b_a) / T::from_f64(9.0); // yellowness-blueness + let h_rad = b.clone().atan2(a.clone()); // hue in radians + let h = Cam16Hue::from_radians(h_rad.clone()); // hue in degrees + let e_t = T::from_f64(0.25) * (T::cos(h_rad + T::from_f64(2.0)) + T::from_f64(3.8)); + let capital_a = T::from_scalar(parameters.n_bb) + * (T::from_f64(2.0) * &r_a + &g_a + T::from_f64(0.05) * &b_a); + let j_root = (capital_a / T::from_scalar(parameters.a_w.clone())).powf( + T::from_f64(0.5) * T::from_scalar(parameters.c.clone()) * T::from_scalar(parameters.z), + ); + + let j = calculate_lightness(j_root.clone()); // lightness + let q = calculate_brightness( + j_root.clone(), + T::from_scalar(parameters.c.clone()), + T::from_scalar(parameters.a_w.clone()), + T::from_scalar(parameters.f_l_4.clone()), + ); // brightness + + let t = T::from_f64(5e4) / T::from_f64(13.0) + * T::from_scalar(parameters.n_c) + * T::from_scalar(parameters.n_cb) + * e_t + * (a.clone() * a + b.clone() * b).sqrt() + / (r_a + g_a + T::from_f64(1.05) * b_a + T::from_f64(0.305)); + let alpha = t.powf(T::from_f64(0.9)) + * (T::from_f64(1.64) - T::from_f64(0.29).powf(T::from_scalar(parameters.n))) + .powf(T::from_f64(0.73)); + + let c = calculate_chroma(j_root, alpha.clone()); // chroma + let m = calculate_colorfulness(T::from_scalar(parameters.f_l_4), c.clone()); // colorfulness + let s = calculate_saturation( + T::from_scalar(parameters.c), + T::from_scalar(parameters.a_w), + alpha, + ); // saturation + + Cam16 { + lightness: j, + chroma: c, + hue: h, + brightness: q, + colorfulness: m, + saturation: s, + } +} + +fn calculate_lightness(j_root: T) -> T +where + T: Real + Arithmetics, +{ + T::from_f64(100.0) * &j_root * j_root +} + +fn calculate_brightness(j_root: T, param_c: T, param_a_w: T, param_f_l_4: T) -> T +where + T: Real + Arithmetics, +{ + T::from_f64(4.0) / param_c * j_root * (T::from_f64(4.0) + param_a_w) * param_f_l_4 +} + +#[inline] +pub(super) fn calculate_chroma(j_root: T, alpha: T) -> T +where + T: Mul, +{ + j_root * alpha +} + +#[inline] +pub(super) fn calculate_colorfulness(param_f_l_4: T, chroma: T) -> T +where + T: Mul, +{ + param_f_l_4 * chroma +} + +#[inline] +pub(super) fn calculate_saturation(param_c: T, param_a_w: T, alpha: T) -> T +where + T: Real + Arithmetics + Sqrt, +{ + T::from_f64(50.0) * (param_c * alpha / (param_a_w + T::from_f64(4.0))).sqrt() +} + +#[inline] +pub(crate) fn cam16_to_xyz( + cam16: (LuminanceType, ChromaticityType, Cam16Hue), + parameters: DependentParameters, +) -> Xyz +where + T: Real + + FromScalar + + One + + Zero + + Sqrt + + Powf + + Abs + + Signum + + Arithmetics + + Trigonometry + + RealAngle + + SignedAngle + + PartialCmp + + Clone, + T::Mask: LazySelect + Clone, + T::Scalar: Clone, +{ + // Weird naming, but we just want to know if it's black or not here. + let is_black = match &cam16.0 { + LuminanceType::Lightness(lightness) => lightness.eq(&T::zero()), + LuminanceType::Brightness(brightness) => brightness.eq(&T::zero()), + }; + + let xyz = non_black_cam16_to_xyz(cam16, parameters); + Xyz { + x: is_black.clone().select(T::zero(), xyz.x), + y: is_black.clone().select(T::zero(), xyz.y), + z: is_black.select(T::zero(), xyz.z), + white_point: PhantomData, + } +} + +// Assumes that lightness has been checked to be non-zero in `cam16_to_xyz`. +fn non_black_cam16_to_xyz( + cam16: (LuminanceType, ChromaticityType, Cam16Hue), + parameters: DependentParameters, +) -> Xyz +where + T: Real + + FromScalar + + One + + Sqrt + + Powf + + Abs + + Signum + + Arithmetics + + Trigonometry + + RealAngle + + SignedAngle + + Clone, + T::Scalar: Clone, +{ + let h_rad = cam16.2.into_radians(); + let (sin_h, cos_h) = h_rad.clone().sin_cos(); + let j_root = match cam16.0 { + LuminanceType::Lightness(j) => lightness_to_j_root(j), + LuminanceType::Brightness(q) => brightness_to_j_root( + q, + T::from_scalar(parameters.c.clone()), + T::from_scalar(parameters.a_w.clone()), + T::from_scalar(parameters.f_l_4.clone()), + ), + }; + let alpha = match cam16.1 { + ChromaticityType::Chroma(c) => c / &j_root, + ChromaticityType::Colorfulness(m) => { + colorfulness_to_chroma(m, T::from_scalar(parameters.f_l_4)) / &j_root + } + ChromaticityType::Saturation(s) => saturation_to_alpha( + s, + T::from_scalar(parameters.c.clone()), + T::from_scalar(parameters.a_w.clone()), + ), + }; + let t = (alpha + * (T::from_f64(1.64) - T::from_f64(0.29).powf(T::from_scalar(parameters.n))) + .powf(T::from_f64(-0.73))) + .powf(T::from_f64(10.0) / T::from_f64(9.0)); + let e_t = T::from_f64(0.25) * ((h_rad + T::from_f64(2.0)).cos() + T::from_f64(3.8)); + let capital_a = T::from_scalar(parameters.a_w) + * j_root + .powf(T::from_f64(2.0) / T::from_scalar(parameters.c) / T::from_scalar(parameters.z)); + let p_1 = T::from_f64(5e4) / T::from_f64(13.0) + * T::from_scalar(parameters.n_c) + * T::from_scalar(parameters.n_cb) + * e_t; + let p_2 = capital_a / T::from_scalar(parameters.n_bb); + let r = T::from_f64(23.0) * (T::from_f64(0.305) + &p_2) * &t + / (T::from_f64(23.0) * p_1 + + t * (T::from_f64(11.0) * &cos_h + T::from_f64(108.0) * &sin_h)); + let a = cos_h * &r; + let b = sin_h * r; + let denom = T::one() / T::from_f64(1403.0); + let rgb_c = [ + (T::from_f64(460.0) * &p_2 + T::from_f64(451.0) * &a + T::from_f64(288.0) * &b) * &denom, + (T::from_f64(460.0) * &p_2 - T::from_f64(891.0) * &a - T::from_f64(261.0) * &b) * &denom, + (T::from_f64(460.0) * p_2 - T::from_f64(220.0) * a - T::from_f64(6300.0) * b) * &denom, + ]; + + let unadapt = parameters.unadapt; + let rgb_c = map3(rgb_c, |component| unadapt.run(component)); + let d_rgb_inv = map3(parameters.d_rgb_inv, T::from_scalar); + + m16_inv(mul3(rgb_c, d_rgb_inv)) / T::from_f64(100.0) // The reference uses 0.0 to 100.0 instead of 0.0 to 1.0. +} + +pub(super) fn prepare_parameters( + parameters: Parameters, T>, +) -> DependentParameters +where + T: Real + + FromScalar + + One + + Zero + + Clamp + + PartialCmp + + Arithmetics + + Powf + + Sqrt + + Exp + + Abs + + Signum + + Clone, + T::Mask: LazySelect, +{ + // Compute dependent parameters. + let xyz_w = parameters.white_point * T::from_f64(100.0); // The reference uses 0.0 to 100.0 instead of 0.0 to 1.0. + let l_a = parameters.adapting_luminance; + let y_b = parameters.background_luminance * T::from_f64(100.0); // The reference uses 0.0 to 100.0 instead of 0.0 to 1.0. + let y_w = xyz_w.y.clone(); + let surround = parameters.surround.into_percent() * T::from_f64(0.1); + let c = lazy_select! { + if surround.gt_eq(&T::one()) => lerp( + T::from_f64(0.59), + T::from_f64(0.69), + surround.clone() - T::one(), + ), + else => lerp(T::from_f64(0.525), T::from_f64(0.59), surround.clone()) + }; + let f = lazy_select! { + if c.gt_eq(&T::from_f64(0.59)) => lerp( + T::from_f64(0.9), + T::one(), + (c.clone() - T::from_f64(0.59)) / T::from_f64(0.1)), + else => lerp( + T::from_f64(0.8), + T::from_f64(0.9), + (c.clone() - T::from_f64(0.525)) / T::from_f64(0.065) + ) + }; + let n_c = f.clone(); + let k = T::one() / (T::from_f64(5.0) * &l_a + T::one()); + let f_l = { + // Luminance adaptation factor + let k4 = k.clone() * &k * &k * k; + let k4_inv = T::one() - &k4; + let a_third = T::one() / T::from_f64(3.0); + + k4 * &l_a + T::from_f64(0.1) * &k4_inv * k4_inv * (T::from_f64(5.0) * &l_a).powf(a_third) + }; + let f_l_4 = f_l.clone().powf(T::from_f64(0.25)); + let n = y_b / &y_w; + let z = T::from_f64(1.48) + n.clone().sqrt(); // Lightness non-linearity exponent (modified by `c`). + let n_bb = T::from_f64(0.725) * n.clone().powf(T::from_f64(-0.2)); // Chromatic induction factors + let n_cb = n_bb.clone(); + // Illuminant discounting (adaptation). Fully adapted = 1 + let d = match parameters.discounting { + super::Discounting::Auto => { + // The default D function. + f * (T::one() + - T::one() / T::from_f64(3.6) + * Exp::exp((-l_a - T::from_f64(42.0)) / T::from_f64(92.0))) + } + super::Discounting::Custom(degree) => degree, + }; + + let d = clamp(d, T::zero(), T::one()); + + let rgb_w = m16(xyz_w); // Cone responses of the white point + let d_rgb = map3(rgb_w.clone(), |c_w| { + lerp(T::one(), y_w.clone() / c_w, d.clone()) + }); + let d_rgb_inv = map3(d_rgb.clone(), |d_c| T::one() / d_c); + let rgb_cw = mul3(rgb_w, d_rgb.clone()); + + let adapt = Adapt { f_l: f_l.clone() }; + + let exponent = T::one() / T::from_f64(0.42); + let unadapt = Unadapt { + constant: T::from_f64(100.0) / f_l * T::from_f64(27.13).powf(exponent.clone()), + exponent, + }; + + let [rgb_aw1, rgb_aw2, rgb_aw3] = map3(rgb_cw, |component| adapt.run(component)); + let a_w = n_bb.clone() * (T::from_f64(2.0) * rgb_aw1 + rgb_aw2 + T::from_f64(0.05) * rgb_aw3); + + DependentParameters { + d_rgb, + d_rgb_inv, + n, + n_bb, + n_c, + n_cb, + a_w, + c, + z, + f_l_4, + adapt, + unadapt, + } +} + +#[inline] +pub(super) fn lightness_to_brightness( + lightness: T, + param_c: T, + param_a_w: T, + param_f_l_4: T, +) -> T +where + T: Real + Arithmetics + Sqrt, +{ + let j_root = lightness_to_j_root(lightness); + calculate_brightness(j_root, param_c, param_a_w, param_f_l_4) +} + +#[inline] +pub(super) fn brightness_to_lightness( + brightness: T, + param_c: T, + param_a_w: T, + param_f_l_4: T, +) -> T +where + T: Real + Arithmetics, +{ + let j_root = brightness_to_j_root(brightness, param_c, param_a_w, param_f_l_4); + calculate_lightness(j_root) +} + +#[inline] +pub(super) fn chroma_to_colorfulness(chroma: T, param_f_l_4: T) -> T +where + T: Mul, +{ + param_f_l_4 * chroma +} + +#[inline] +pub(super) fn chroma_to_saturation(chroma: T, lightness: T, param_c: T, param_a_w: T) -> T +where + T: Real + Arithmetics + Sqrt + Clone, +{ + let j_root = lightness_to_j_root(lightness); + let alpha = chroma / &j_root; + + calculate_saturation(param_c, param_a_w, alpha) +} + +#[inline] +pub(super) fn colorfulness_to_chroma(colorfulness: T, param_f_l_4: T) -> T +where + T: Div, +{ + colorfulness / param_f_l_4 +} + +#[inline] +pub(super) fn saturation_to_chroma(saturation: T, lightness: T, param_c: T, param_a_w: T) -> T +where + T: Real + Arithmetics + Sqrt, +{ + let j_root = lightness_to_j_root(lightness); + let alpha = saturation_to_alpha(saturation, param_c, param_a_w); + + calculate_chroma(j_root, alpha) +} + +#[inline] +fn lightness_to_j_root(lightness: T) -> T +where + T: Real + Mul + Sqrt, +{ + lightness.sqrt() * T::from_f64(0.1) +} + +#[inline] +fn brightness_to_j_root(brightness: T, param_c: T, param_a_w: T, param_f_l_4: T) -> T +where + T: Real + Arithmetics, +{ + T::from_f64(0.25) * param_c * brightness / ((T::from_f64(4.0) + param_a_w) * param_f_l_4) +} + +#[inline] +fn saturation_to_alpha(saturation: T, param_c: T, param_a_w: T) -> T +where + T: Real + Arithmetics, +{ + T::from_f64(0.0004) * &saturation * saturation * (T::from_f64(4.0) + param_a_w) / param_c +} + +#[derive(Clone, Copy)] +pub(crate) struct DependentParameters { + d_rgb: [T; 3], + d_rgb_inv: [T; 3], + n: T, + n_bb: T, + n_c: T, + n_cb: T, + pub(super) a_w: T, + pub(super) c: T, + z: T, + pub(super) f_l_4: T, + adapt: Adapt, + unadapt: Unadapt, +} + +#[derive(Clone, Copy)] +struct Adapt { + f_l: T, +} + +impl Adapt { + fn run(&self, component: V) -> V + where + V: Real + FromScalar + Abs + Signum + Powf + Arithmetics + Clone, + T: Clone, + { + let x = (V::from_scalar(self.f_l.clone()) * component.clone().abs() * V::from_f64(0.01)) + .powf(V::from_f64(0.42)); + component.signum() * V::from_f64(400.0) * &x / (x + V::from_f64(27.13)) + } +} + +#[derive(Clone, Copy)] +struct Unadapt { + constant: T, + exponent: T, +} + +impl Unadapt { + fn run(&self, component: V) -> V + where + V: Real + FromScalar + Abs + Signum + Powf + Arithmetics + Clone, + T: Clone, + { + let c_abs = component.clone().abs(); + component.signum() + * V::from_scalar(self.constant.clone()) + * (c_abs.clone() / (V::from_f64(400.0) - c_abs)) + .powf(V::from_scalar(self.exponent.clone())) + } +} + +fn lerp(from: T, to: T, factor: T) -> T +where + T: One + Arithmetics, +{ + (T::one() - &factor) * from + factor * to +} + +fn m16(xyz: Xyz) -> [T; 3] +where + T: Real + Arithmetics, +{ + let Xyz { x, y, z, .. } = xyz; + + #[rustfmt::skip] + let rgb = [ + T::from_f64( 0.401288) * &x + T::from_f64(0.650173) * &y - T::from_f64(0.051461) * &z, + T::from_f64(-0.250268) * &x + T::from_f64(1.204414) * &y + T::from_f64(0.045854) * &z, + T::from_f64(-0.002079) * x + T::from_f64(0.048952) * y + T::from_f64(0.953127) * z, + ]; + + rgb +} + +fn m16_inv(rgb: [T; 3]) -> Xyz +where + T: Real + Arithmetics, +{ + let [r, g, b] = rgb; + + #[rustfmt::skip] + #[allow(clippy::excessive_precision)] // Clippy didn't like the e+0 + let xyz = Xyz { + x: T::from_f64( 1.862067855087233e+0) * &r - T::from_f64(1.011254630531685e+0) * &g + T::from_f64(1.491867754444518e-1) * &b, + y: T::from_f64( 3.875265432361372e-1) * &r + T::from_f64(6.214474419314753e-1) * &g - T::from_f64(8.973985167612518e-3) * &b, + z: T::from_f64(-1.584149884933386e-2) * r - T::from_f64(3.412293802851557e-2) * g + T::from_f64(1.049964436877850e+0) * b, + white_point: PhantomData + }; + + xyz +} + +fn map3(array: [T; 3], mut map: impl FnMut(T) -> U) -> [U; 3] { + let [a1, a2, a3] = array; + [map(a1), map(a2), map(a3)] +} + +fn mul3(lhs: [T; 3], rhs: [T; 3]) -> [T; 3] +where + T: Mul, +{ + let [l1, l2, l3] = lhs; + let [r1, r2, r3] = rhs; + + [l1 * r1, l2 * r2, l3 * r3] +} diff --git a/palette/src/cam16/math/chromaticity.rs b/palette/src/cam16/math/chromaticity.rs new file mode 100644 index 000000000..de392b587 --- /dev/null +++ b/palette/src/cam16/math/chromaticity.rs @@ -0,0 +1,96 @@ +use crate::{ + bool_mask::{LazySelect, Select}, + cam16::{ + math::{self, DependentParameters}, + BakedParameters, + }, + num::{Arithmetics, FromScalar, PartialCmp, Real, Sqrt, Zero}, +}; + +/// One the apparent chromatic intensity metrics of CAM16. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum ChromaticityType { + /// The [chroma](https://en.wikipedia.org/wiki/Colorfulness#Chroma) (C) of a + /// color. + Chroma(T), + + /// The [colorfulness](https://en.wikipedia.org/wiki/Colorfulness) (M) of a + /// color. + Colorfulness(T), + + /// The [saturation](https://en.wikipedia.org/wiki/Colorfulness#Saturation) + /// (s) of a color. + Saturation(T), +} + +impl ChromaticityType { + pub(crate) fn into_cam16( + self, + lightness: T, + parameters: BakedParameters, + ) -> (T, T, T) + where + T: Real + FromScalar + Zero + Arithmetics + Sqrt + PartialCmp + Clone, + T::Mask: LazySelect + Clone, + { + let DependentParameters { c, a_w, f_l_4, .. } = parameters.inner; + let is_black = lightness.eq(&T::zero()); + + match self { + ChromaticityType::Chroma(chroma) => { + let colorfulness = lazy_select! { + if is_black.clone() => T::zero(), + else => math::chroma_to_colorfulness(chroma.clone(), T::from_scalar(f_l_4)) + }; + let saturation = lazy_select! { + if is_black.clone() => T::zero(), + else => math::chroma_to_saturation( + chroma.clone(), + lightness, + T::from_scalar(c), + T::from_scalar(a_w), + ) + }; + let chroma = is_black.select(T::zero(), chroma); + + (chroma, colorfulness, saturation) + } + ChromaticityType::Colorfulness(colorfulness) => { + let chroma = lazy_select! { + if is_black.clone() => T::zero(), + else => math::colorfulness_to_chroma(colorfulness.clone(), T::from_scalar(f_l_4)) + }; + let saturation = lazy_select! { + if is_black.clone() => T::zero(), + else => math::chroma_to_saturation( + chroma.clone(), + lightness, + T::from_scalar(c), + T::from_scalar(a_w), + ) + }; + let colorfulness = is_black.select(T::zero(), colorfulness); + + (chroma, colorfulness, saturation) + } + ChromaticityType::Saturation(saturation) => { + let chroma = lazy_select! { + if is_black.clone() => T::zero(), + else => math::saturation_to_chroma( + saturation.clone(), + lightness, + T::from_scalar(c), + T::from_scalar(a_w), + ) + }; + let colorfulness = lazy_select! { + if is_black.clone() => T::zero(), + else => math::chroma_to_colorfulness(chroma.clone(), T::from_scalar(f_l_4)) + }; + let saturation = is_black.select(T::zero(), saturation); + + (chroma, colorfulness, saturation) + } + } + } +} diff --git a/palette/src/cam16/math/luminance.rs b/palette/src/cam16/math/luminance.rs new file mode 100644 index 000000000..815e6cab6 --- /dev/null +++ b/palette/src/cam16/math/luminance.rs @@ -0,0 +1,57 @@ +use crate::{ + bool_mask::LazySelect, + cam16::BakedParameters, + num::{Arithmetics, FromScalar, PartialCmp, Real, Sqrt, Zero}, +}; +/// One the apparent luminance metrics of CAM16. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[non_exhaustive] +pub(crate) enum LuminanceType { + /// The [lightness](https://en.wikipedia.org/wiki/Lightness) (J) of a color. + Lightness(T), + + /// The [brightness](https://en.wikipedia.org/wiki/Brightness) (Q) of a + /// color. + Brightness(T), +} + +impl LuminanceType { + pub(crate) fn into_cam16(self, parameters: BakedParameters) -> (T, T) + where + T: Real + FromScalar + Zero + Arithmetics + Sqrt + PartialCmp + Clone, + T::Mask: LazySelect + Clone, + { + let parameters = parameters.inner; + + match self { + LuminanceType::Lightness(lightness) => { + let is_black = lightness.eq(&T::zero()); + let brightness = lazy_select! { + if is_black => T::zero(), + else => crate::cam16::math::lightness_to_brightness( + lightness.clone(), + T::from_scalar(parameters.c), + T::from_scalar(parameters.a_w), + T::from_scalar(parameters.f_l_4), + ) + }; + + (lightness, brightness) + } + LuminanceType::Brightness(brightness) => { + let is_black = brightness.eq(&T::zero()); + let lightness = lazy_select! { + if is_black => T::zero(), + else => crate::cam16::math::brightness_to_lightness( + brightness.clone(), + T::from_scalar(parameters.c), + T::from_scalar(parameters.a_w), + T::from_scalar(parameters.f_l_4), + ) + }; + + (lightness, brightness) + } + } + } +} diff --git a/palette/src/cam16/parameters.rs b/palette/src/cam16/parameters.rs new file mode 100644 index 000000000..9c81cfe9b --- /dev/null +++ b/palette/src/cam16/parameters.rs @@ -0,0 +1,300 @@ +use core::marker::PhantomData; + +use crate::{ + bool_mask::LazySelect, + num::{ + Abs, Arithmetics, Clamp, Exp, FromScalar, One, PartialCmp, Powf, Real, Signum, Sqrt, Zero, + }, + white_point::{self, WhitePoint}, + Xyz, +}; + +/// Parameters for CAM16 that describe the viewing conditions. +/// +/// These parameters describe the viewing conditions for a more accurate color +/// appearance metric. The CAM16 attributes and derived values are only really +/// comparable if they were calculated with the same parameters. The parameters +/// are, however, too dynamic to all be part of the type parameters of +/// [`Cam16`][super::Cam16]. +/// +/// The default values are mostly a "blank slate", with a couple of educated +/// guesses. Be sure to at least customize the luminances according to the +/// expected environment: +/// +/// ``` +/// use palette::{Srgb, Xyz, IntoColor, cam16::{Parameters, Surround, Cam16}}; +/// +/// // 40 nits, 50% background luminance and a dim surrounding: +/// let mut example_parameters = Parameters::default_static_wp(40.0); +/// example_parameters.background_luminance = 0.5; +/// example_parameters.surround = Surround::Dim; +/// +/// let example_color_xyz = Srgb::from(0x5588cc).into_linear().into_color(); +/// let cam16: Cam16 = Cam16::from_xyz(example_color_xyz, example_parameters); +/// ``` +/// +/// See also Moroney (2000) [Usage Guidelines for CIECAM97s][moroney_2000] for +/// more information and advice on how to customize these parameters. +/// +/// [moroney_2000]: +/// https://www.imaging.org/common/uploaded%20files/pdfs/Papers/2000/PICS-0-81/1611.pdf +#[derive(Clone, Copy)] +#[non_exhaustive] +pub struct Parameters { + /// White point of the test illuminant, *Xw* *Yw* + /// *Zw*. *Yw* should be normalized to 1.0. + /// + /// Defaults to `Wp` when it implements [`WhitePoint`]. It can also be set + /// to a custom value if `Wp` results in the wrong white point. + pub white_point: WpParam, + + /// The average luminance of the environment (test adapting field) + /// (*LA*) in *cd/m2* (nits). + /// + /// Under a “gray world” assumption this is 20% of the luminance of the + /// reference white. + pub adapting_luminance: T, + + /// The luminance factor of the background (*Yb*), on a scale + /// from `0.0` to `1.0` (relative to *Yw* = 1.0). + /// + /// Defaults to `0.2`, medium grey. + pub background_luminance: T, + + /// A description of the peripheral area, with a value from 0% to 20%. Any + /// value outside that range will be clamped to 0% or 20%. It has presets + /// for "dark", "dim" and "average". + /// + /// Defaults to "average" (20%). + pub surround: Surround, + + /// The degree of discounting of (or adaptation to) the reference + /// illuminant. Defaults to `Auto`, making the degree of discounting depend + /// on the other parameters, but can be customized if necessary. + pub discounting: Discounting, +} + +impl Parameters +where + WpParam: WhitePointParameter, +{ + fn into_any_white_point(self) -> Parameters, T> { + Parameters { + white_point: self.white_point.into_xyz(), + adapting_luminance: self.adapting_luminance, + background_luminance: self.background_luminance, + surround: self.surround, + discounting: self.discounting, + } + } +} + +impl Parameters, T> { + /// Creates a new set of parameters with a static white point and their + /// default values set. + /// + /// These parameters may need to be further customized according to the + /// viewing conditions. + #[inline] + pub fn default_static_wp(adapting_luminance: T) -> Self + where + T: Real, + { + Self { + white_point: StaticWp(PhantomData), + adapting_luminance, + background_luminance: T::from_f64(0.2), + surround: Surround::Average, + discounting: Discounting::Auto, + } + } +} + +impl Parameters, T> { + /// Creates a new set of parameters with a dynamic white point and their + /// default values set. + /// + /// These parameters may need to be further customized according to the + /// viewing conditions. + #[inline] + pub fn default_dynamic_wp(white_point: Xyz, adapting_luminance: T) -> Self + where + T: Real, + { + Self { + white_point, + adapting_luminance, + background_luminance: T::from_f64(0.2), + surround: Surround::Average, + discounting: Discounting::Auto, + } + } +} + +impl Parameters { + /// Pre-bakes the parameters to avoid repeating parts of the calculaitons + /// when converting to and from CAM16. + pub fn bake(self) -> BakedParameters + where + BakedParameters: From, + { + self.into() + } +} + +#[cfg(all(test, feature = "approx"))] +impl Parameters, f64> { + /// Only used in unit tests and corresponds to the defaults from https://observablehq.com/@jrus/cam16. + pub(crate) const TEST_DEFAULTS: Self = Self { + white_point: StaticWp(PhantomData), + adapting_luminance: 40.0f64, + background_luminance: 0.2f64, // 20 / 100, since our XYZ is in the range from 0.0 to 1.0 + surround: Surround::Average, + discounting: Discounting::Auto, + }; +} + +/// Pre-calculated variables for CAM16, that only depend on the viewing +/// conditions. +/// +/// Derived from [`Parameters`], the `BakedParameters` can help reducing the +/// amount of repeated work required for converting multiple colors. +pub struct BakedParameters { + pub(crate) inner: super::math::DependentParameters, + white_point: PhantomData, +} + +impl Clone for BakedParameters +where + T: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + white_point: PhantomData, + } + } +} + +impl Copy for BakedParameters where T: Copy {} + +impl From> for BakedParameters +where + WpParam: WhitePointParameter, + T: Real + + FromScalar + + One + + Zero + + Clamp + + PartialCmp + + Arithmetics + + Powf + + Sqrt + + Exp + + Abs + + Signum + + Clone, + T::Mask: LazySelect, +{ + fn from(value: Parameters) -> Self { + Self { + inner: super::math::prepare_parameters(value.into_any_white_point()), + white_point: PhantomData, + } + } +} + +/// A description of the peripheral area. +#[derive(Clone, Copy)] +#[non_exhaustive] +pub enum Surround { + /// Represents a dark room, such as a movie theatre. Corresponds to a + /// surround value of 0%. + Dark, + + /// Represents a dimly lit room with a bright TV or monitor. Corresponds to + /// a surround value of 10%. + Dim, + + /// Represents a surface color, such as a print on a 20% reflective, + /// uniformly lit background surface. Corresponds to a surround value of + /// 20%. + Average, + + /// Any custom value from 0% to 20%. Any value outside that range will be + /// clamped to either `0.0` or `20.0`. + Percent(T), +} + +impl Surround { + pub(crate) fn into_percent(self) -> T + where + T: Real + Clamp, + { + match self { + Surround::Dark => T::from_f64(0.0), + Surround::Dim => T::from_f64(10.0), + Surround::Average => T::from_f64(20.0), + Surround::Percent(value) => value.clamp(T::from_f64(0.0), T::from_f64(20.0)), + } + } +} + +/// The degree of discounting of (or adaptation to) the illuminant. +/// +/// See also: . +#[derive(Clone, Copy)] +#[non_exhaustive] +pub enum Discounting { + /// Uses luminance levels and surround conditions to calculate the + /// discounting, using the original CIECAM16 *D* function. Ranges from + /// `0.65` to `1.0`. + Auto, + + /// A value between `0.0` and `1.0`, where `0.0` represents no adaptation, + /// and `1.0` represents that the observer's vision is fully adapted to the + /// illuminant. Values outside that range will be clamped. + Custom(T), +} + +/// A trait for types that can be used as white point parameters in +/// [`Parameters`]. +pub trait WhitePointParameter { + /// The static representation of this white point, or [`white_point::Any`] + /// if it's dynamic. + type StaticWp; + + /// Returns the XYZ value for this white point. + fn into_xyz(self) -> Xyz; +} + +impl WhitePointParameter for Xyz { + type StaticWp = white_point::Any; + + fn into_xyz(self) -> Xyz { + self + } +} + +/// Represents a static white point in [`Parameters`], as opposed to a dynamic +/// [`Xyz`] value. +pub struct StaticWp(PhantomData); + +impl WhitePointParameter for StaticWp +where + Wp: WhitePoint, +{ + type StaticWp = Wp; + + fn into_xyz(self) -> Xyz { + Wp::get_xyz() + } +} + +impl Clone for StaticWp { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for StaticWp {} diff --git a/palette/src/cam16/partial.rs b/palette/src/cam16/partial.rs new file mode 100644 index 000000000..f13a3ecd9 --- /dev/null +++ b/palette/src/cam16/partial.rs @@ -0,0 +1,793 @@ +use crate::{ + cam16::Cam16UcsJmh, + convert::FromColorUnclamped, + num::{Arithmetics, Exp, One, Real}, + Alpha, +}; + +macro_rules! make_partial_cam16 { + ( + $(#[$type_meta: meta])* + $module: ident :: $name: ident { + $(#[$luminance_meta: meta])+ + $luminance: ident : $luminance_ty: ident, + $(#[$chromaticity_meta: meta])+ + $chromaticity: ident : $chromaticity_ty: ident + } + ) => { + pub use $module::$name; + + #[doc = concat!("Partial CIE CAM16, with ", stringify!($luminance), " and ", stringify!($chromaticity), ", and helper types.")] + pub mod $module { + use crate::{ + bool_mask::HasBoolMask, + cam16::{BakedParameters, Cam16, WhitePointParameter, Cam16FromUnclamped, IntoCam16Unclamped, Cam16IntoUnclamped}, + convert::FromColorUnclamped, + hues::{Cam16Hue, Cam16HueIter}, + num::{FromScalar, Zero}, + Alpha, Xyz, + }; + + use crate::cam16::math::chromaticity::*; + use crate::cam16::math::luminance::*; + + #[doc = concat!("Partial CIE CAM16, with ", stringify!($luminance), " and ", stringify!($chromaticity), ".")] + /// + /// It contains enough information for converting CAM16 to other + /// color spaces. See [Cam16] for more details about CIE CAM16. + /// + /// The full list of partial CAM16 variants is: + /// + /// * [`Cam16Jch`](crate::cam16::Cam16Jch): lightness and chroma. + /// * [`Cam16Jmh`](crate::cam16::Cam16Jmh): lightness and + /// colorfulness. + /// * [`Cam16Jsh`](crate::cam16::Cam16Jsh): lightness and + /// saturation. + /// * [`Cam16Qch`](crate::cam16::Cam16Qch): brightness and chroma. + /// * [`Cam16Qmh`](crate::cam16::Cam16Qmh): brightness and + /// colorfulness. + /// * [`Cam16Qsh`](crate::cam16::Cam16Qsh): brightness and + /// saturation. + /// + /// # Creating a Value + /// + /// Any partial CAM16 set can be obtained from the full set of + /// attributes. It's also possible to convert directly to it, using + #[doc = concat!("[`from_xyz`][", stringify!($name), "::from_xyz],")] + /// or to create a new value by calling + #[doc = concat!("[`new`][", stringify!($name), "::new].")] + /// ``` + /// use palette::{ + /// Srgb, FromColor, IntoColor, hues::Cam16Hue, + #[doc = concat!(" cam16::{Cam16, Parameters, ", stringify!($name), "},")] + /// }; + /// + #[doc = concat!("let partial = ", stringify!($name), "::new(50.0f32, 80.0, 120.0);")] + /// + /// // There's also `new_const`: + #[doc = concat!("const PARTIAL: ", stringify!($name), " = ", stringify!($name), "::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(40.0); + /// + /// // Partial CAM16 from sRGB, or most other color spaces: + /// let rgb = Srgb::new(0.3f32, 0.8, 0.1); + #[doc = concat!("let partial_from_rgb = ", stringify!($name), "::from_xyz(rgb.into_color(), example_parameters);")] + /// + /// // Partial CAM16 from sRGB, via full CAM16: + /// let rgb = Srgb::new(0.3f32, 0.8, 0.1); + /// let cam16_from_rgb = Cam16::from_xyz(rgb.into_color(), example_parameters); + #[doc = concat!("let partial_from_full = ", stringify!($name), "::from(cam16_from_rgb);")] + /// + /// // 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( + palette_internal, + component = "T", + skip_derives(Cam16, $name) + )] + $(#[$type_meta])* + #[repr(C)] + pub struct $name { + $(#[$luminance_meta])+ + pub $luminance: T, + + $(#[$chromaticity_meta])+ + pub $chromaticity: T, + + /// The [hue](https://cie.co.at/eilvterm/17-22-067) (h) of the color. + /// + /// See [`Cam16::hue`][crate::cam16::Cam16::hue]. + #[palette(unsafe_same_layout_as = "T")] + pub hue: Cam16Hue, + } + + impl $name { + /// Create a partial CIE CAM16 color. + #[inline] + pub fn new($luminance: T, $chromaticity: T, hue: H) -> Self + where + H: Into>, + { + Self::new_const($luminance.into(), $chromaticity.into(), hue.into()) + } + + #[doc = concat!("Create a partial CIE CAM16 color. This is the same as `", stringify!($name), "::new`")] + /// without the generic hue type. It's temporary until `const fn` + /// supports traits. + #[inline] + pub const fn new_const($luminance: T, $chromaticity: T, hue: Cam16Hue) -> Self { + Self { + $luminance, + $chromaticity, + hue, + } + } + + #[doc = concat!("Convert to a `(", stringify!($luminance), ", ", stringify!($chromaticity), ", hue)` tuple.")] + #[inline] + pub fn into_components(self) -> (T, T, Cam16Hue) { + (self.$luminance, self.$chromaticity, self.hue) + } + + #[doc = concat!("Convert from a `(", stringify!($luminance), ", ", stringify!($chromaticity), ", hue)` tuple.")] + #[inline] + pub fn from_components(($luminance, $chromaticity, hue): (T, T, H)) -> Self + where + H: Into>, + { + Self::new($luminance, $chromaticity, hue) + } + + /// Derive partial CIE CAM16 attributes for the provided color, under the provided + /// viewing conditions. + /// + /// ``` + #[doc = concat!("use palette::{Srgb, IntoColor, cam16::{", stringify!($name), ", Parameters}};")] + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let rgb = Srgb::new(0.3f32, 0.8, 0.1); + #[doc = concat!("let partial = ", stringify!($name), "::from_xyz(rgb.into_color(), example_parameters);")] + /// ``` + /// + /// It's also possible to "pre-bake" the parameters, to avoid recalculate + /// some of the derived values when converting multiple color value. + /// + /// ``` + #[doc = concat!("use palette::{Srgb, IntoColor, cam16::{", stringify!($name), ", Parameters}};")] + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let baked_parameters = example_parameters.bake(); + /// + /// let rgb = Srgb::new(0.3f32, 0.8, 0.1); + #[doc = concat!("let partial = ", stringify!($name), "::from_xyz(rgb.into_color(), baked_parameters);")] + /// ``` + #[inline] + pub fn from_xyz( + color: Xyz, + parameters: impl Into>, + ) -> Self + where + Xyz: IntoCam16Unclamped, + T: FromScalar, + WpParam: WhitePointParameter, + { + color.into_cam16_unclamped(parameters.into()) + } + + /// Construct an XYZ color from these CIE CAM16 attributes, under the + /// provided viewing conditions. + /// + /// ``` + #[doc = concat!("use palette::{Srgb, FromColor, cam16::{", stringify!($name), ", Parameters}};")] + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + #[doc = concat!("let partial = ", stringify!($name), "::new(50.0f32, 80.0, 120.0);")] + /// let rgb = Srgb::from_color(partial.into_xyz(example_parameters)); + /// ``` + /// + /// It's also possible to "pre-bake" the parameters, to avoid recalculate + /// some of the derived values when converting multiple color value. + /// + /// ``` + #[doc = concat!("use palette::{Srgb, FromColor, cam16::{", stringify!($name), ", Parameters}};")] + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let baked_parameters = example_parameters.bake(); + /// + #[doc = concat!("let partial = ", stringify!($name), "::new(50.0f32, 80.0, 120.0);")] + /// let rgb = Srgb::from_color(partial.into_xyz(baked_parameters)); + /// ``` + #[inline] + pub fn into_xyz( + self, + parameters: impl Into>, + ) -> Xyz + where + Self: Cam16IntoUnclamped, Scalar = T::Scalar>, + WpParam: WhitePointParameter, + T: FromScalar, + { + self.cam16_into_unclamped(parameters.into()) + } + + /// Create a partial set of CIE CAM16 attributes. + /// + #[doc = concat!("It's also possible to use `", stringify!($name), "::from` or `Cam16::into`.")] + #[inline] + pub fn from_full(full: Cam16) -> Self { + Self { + hue: full.hue, + $chromaticity: full.$chromaticity, + $luminance: full.$luminance, + } + } + + /// Reconstruct a full set of CIE CAM16 attributes, using the original viewing conditions. + /// + /// ``` + #[doc = concat!("use palette::{Srgb, IntoColor, cam16::{Cam16, ", stringify!($name), ", Parameters}};")] + /// use approx::assert_relative_eq; + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// // Optional, but saves some work: + /// let baked_parameters = example_parameters.bake(); + /// + /// let rgb = Srgb::new(0.3f64, 0.8, 0.1); + /// let cam16 = Cam16::from_xyz(rgb.into_color(), baked_parameters); + #[doc = concat!("let partial = ", stringify!($name), "::from(cam16);")] + /// let reconstructed = partial.into_full(baked_parameters); + /// + /// assert_relative_eq!(cam16, reconstructed, epsilon = 0.0000000000001); + /// ``` + #[inline] + pub fn into_full(self, parameters: impl Into>) -> Cam16 + where + Self: IntoCam16Unclamped, Scalar = T::Scalar>, + T: FromScalar + { + self.into_cam16_unclamped(parameters.into()) + } + + // Turn the chromaticity and luminance into dynamically decided + // attributes, to help conversion to a full set of attributes. + #[inline(always)] + pub(crate) fn into_dynamic(self) -> (LuminanceType, ChromaticityType, Cam16Hue) { + ( + LuminanceType::$luminance_ty(self.$luminance), + ChromaticityType::$chromaticity_ty(self.$chromaticity), + self.hue, + ) + } + } + + #[doc = concat!(r#""[`"#, stringify!($name), "a`](crate::cam16::", stringify!($name), "a)")] + ///implementations. + impl Alpha<$name, A> { + /// Create a partial CIE CAM16 color with transparency. + #[inline] + pub fn new>>($luminance: T, $chromaticity: T, hue: H, alpha: A) -> Self{ + Self::new_const($luminance.into(), $chromaticity.into(), hue.into(), alpha) + } + + /// Create a partial CIE CAM16 color with transparency. This is the + #[doc = concat!("same as `", stringify!($name), "::new` without the generic hue type. It's temporary until")] + /// `const fn` supports traits. + #[inline] + pub const fn new_const($luminance: T, $chromaticity: T, hue: Cam16Hue, alpha: A) -> Self { + Alpha { + color: $name::new_const($luminance, $chromaticity, hue), + alpha, + } + } + + #[doc = concat!("Convert to a `(", stringify!($luminance), ", ", stringify!($chromaticity), ", hue, alpha)` tuple.")] + #[inline] + pub fn into_components(self) -> (T, T, Cam16Hue, A) { + ( + self.color.$luminance, + self.color.$chromaticity, + self.color.hue, + self.alpha, + ) + } + + #[doc = concat!("Convert from a `(", stringify!($luminance), ", ", stringify!($chromaticity), ", hue, alpha)` tuple.")] + #[inline] + pub fn from_components>>( + ($luminance, $chromaticity, hue, alpha): (T, T, H, A), + ) -> Self { + Self::new($luminance, $chromaticity, hue, alpha) + } + + /// Derive partial CIE CAM16 attributes with transparency, for the provided + /// color, under the provided viewing conditions. + /// + /// ``` + #[doc = concat!("use palette::{Srgba, IntoColor, cam16::{", stringify!($name), "a, Parameters}};")] + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let rgba = Srgba::new(0.3f32, 0.8, 0.1, 0.9); + #[doc = concat!("let partial = ", stringify!($name), "a::from_xyz(rgba.into_color(), example_parameters);")] + /// ``` + /// + /// It's also possible to "pre-bake" the parameters, to avoid recalculate + /// some of the derived values when converting multiple color value. + /// + /// ``` + #[doc = concat!("use palette::{Srgba, IntoColor, cam16::{", stringify!($name), "a, Parameters}};")] + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let baked_parameters = example_parameters.bake(); + /// + /// let rgba = Srgba::new(0.3f32, 0.8, 0.1, 0.9); + #[doc = concat!("let partial = ", stringify!($name), "a::from_xyz(rgba.into_color(), baked_parameters);")] + /// ``` + #[inline] + pub fn from_xyz( + color: Alpha, A>, + parameters: impl Into>, + ) -> Self + where + Xyz: IntoCam16Unclamped, Scalar = T::Scalar>, + T: FromScalar, + WpParam: WhitePointParameter, + { + let Alpha { color, alpha } = color; + + Alpha { + color: $name::from_xyz(color, parameters), + alpha, + } + } + + /// Construct an XYZ color with transparency, from these CIE CAM16 + /// attributes, under the provided viewing conditions. + /// + /// ``` + #[doc = concat!("use palette::{Srgba, FromColor, cam16::{", stringify!($name), "a, Parameters}};")] + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + #[doc = concat!("let partial = ", stringify!($name), "a::new(50.0f32, 80.0, 120.0, 0.9);")] + /// let rgba = Srgba::from_color(partial.into_xyz(example_parameters)); + /// ``` + /// + /// It's also possible to "pre-bake" the parameters, to avoid recalculate + /// some of the derived values when converting multiple color value. + /// + /// ``` + #[doc = concat!("use palette::{Srgba, FromColor, cam16::{", stringify!($name), "a, Parameters}};")] + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// let baked_parameters = example_parameters.bake(); + /// + #[doc = concat!("let partial = ", stringify!($name), "a::new(50.0f32, 80.0, 120.0, 0.9);")] + /// let rgba = Srgba::from_color(partial.into_xyz(baked_parameters)); + /// ``` + #[inline] + pub fn into_xyz( + self, + parameters: impl Into>, + ) -> Alpha, A> + where + $name: Cam16IntoUnclamped, Scalar = T::Scalar>, + WpParam: WhitePointParameter, + T: FromScalar, + { + let Alpha { color, alpha } = self; + + Alpha { + color: color.into_xyz(parameters), + alpha, + } + } + + /// Create a partial set of CIE CAM16 attributes with transparency. + /// + #[doc = concat!("It's also possible to use `", stringify!($name), "a::from` or `Cam16a::into`.")] + #[inline] + pub fn from_full(full: Alpha, A>) -> Self { + let Alpha { color, alpha } = full; + + Alpha { + color: $name::from_full(color), + alpha, + } + } + + /// Reconstruct a full set of CIE CAM16 attributes with transparency, using + /// the original viewing conditions. + /// + /// ``` + #[doc = concat!("use palette::{Srgba, IntoColor, cam16::{Cam16a, ", stringify!($name), "a, Parameters}};")] + /// use approx::assert_relative_eq; + /// + /// // Customize these according to the viewing conditions: + /// let mut example_parameters = Parameters::default_static_wp(40.0); + /// + /// // Optional, but saves some work: + /// let baked_parameters = example_parameters.bake(); + /// + /// let rgba = Srgba::new(0.3f64, 0.8, 0.1, 0.9); + /// let cam16a = Cam16a::from_xyz(rgba.into_color(), baked_parameters); + #[doc = concat!("let partial = ", stringify!($name), "a::from(cam16a);")] + /// let reconstructed = partial.into_full(baked_parameters); + /// + /// assert_relative_eq!(cam16a, reconstructed, epsilon = 0.0000000000001); + /// ``` + #[inline] + pub fn into_full( + self, + parameters: impl Into>, + ) -> Alpha, A> + where + $name: IntoCam16Unclamped, Scalar = T::Scalar>, + WpParam: WhitePointParameter, + T: FromScalar, + { + let Alpha { color, alpha } = self; + + Alpha { + color: color.into_full(parameters), + alpha, + } + } + } + + impl FromColorUnclamped for $name { + #[inline] + fn from_color_unclamped(val: Self) -> Self { + val + } + } + + impl FromColorUnclamped> for $name { + #[inline] + fn from_color_unclamped(val: Cam16) -> Self { + Self::from_full(val) + } + } + + impl Cam16FromUnclamped> for $name + where + Xyz: IntoCam16Unclamped>, + WpParam: WhitePointParameter, + { + type Scalar = as IntoCam16Unclamped>>::Scalar; + + fn cam16_from_unclamped(color: Xyz, parameters: BakedParameters) -> Self { + color.into_cam16_unclamped(parameters).into() + } + } + + impl From> for $name { + #[inline] + fn from(value: Cam16) -> Self { + Self::from_full(value) + } + } + + impl From, A>> for Alpha<$name, A> { + #[inline] + fn from(value: Alpha, A>) -> Self { + Self::from_full(value) + } + } + + impl HasBoolMask for $name + where + T: HasBoolMask, + { + type Mask = T::Mask; + } + + #[cfg(feature = "bytemuck")] + unsafe impl bytemuck::Zeroable for $name where T: bytemuck::Zeroable {} + + #[cfg(feature = "bytemuck")] + unsafe impl bytemuck::Pod for $name where T: bytemuck::Pod {} + + impl_reference_component_methods_hue!($name, [$luminance, $chromaticity]); + impl_struct_of_arrays_methods_hue!($name, [$luminance, $chromaticity]); + + impl_tuple_conversion_hue!($name as (T, T, H), Cam16Hue); + + impl_is_within_bounds! { + $name { + $luminance => [T::zero(), None], + $chromaticity => [T::zero(), None] + } + where T: Zero + } + impl_clamp! { + $name { + $luminance => [T::zero()], + $chromaticity => [T::zero()] + } + other {hue} + where T: Zero + } + + impl_mix_hue!($name {$luminance, $chromaticity}); + impl_hue_ops!($name, Cam16Hue); + + impl_color_add!($name, [$luminance, $chromaticity, hue]); + impl_color_sub!($name, [$luminance, $chromaticity, hue]); + + impl_array_casts!($name, [T; 3]); + impl_simd_array_conversion_hue!($name, [$luminance, $chromaticity]); + impl_struct_of_array_traits_hue!($name, Cam16HueIter, [$luminance, $chromaticity]); + + impl_eq_hue!($name, Cam16Hue, [$luminance, $chromaticity, hue]); + } + }; +} + +/// Partial CIE CAM16 with lightness, chroma, and an alpha component. +/// +/// See the [`Cam16Jcha` implementation in `Alpha`](crate::Alpha#Cam16Jcha). +pub type Cam16Jcha = Alpha, T>; +make_partial_cam16! { + cam16_jch::Cam16Jch { + /// The [lightness](https://cie.co.at/eilvterm/17-22-063) (J) of the + /// color. + /// + /// See [`Cam16::lightness`][crate::cam16::Cam16::lightness]. + lightness: Lightness, + + /// The [chroma](https://cie.co.at/eilvterm/17-22-074) (C) of the color. + /// + /// See [`Cam16::chroma`][crate::cam16::Cam16::chroma]. + chroma: Chroma + } +} + +/// Partial CIE CAM16 with lightness, colorfulness, and an alpha component. +/// +/// See the [`Cam16Jmha` implementation in `Alpha`](crate::Alpha#Cam16Jmha). +pub type Cam16Jmha = Alpha, T>; +make_partial_cam16! { + /// + /// `Cam16Jmh` can also convert from CAM16-UCS types, such as + /// [`Cam16UcsJmh`][crate::cam16::Cam16UcsJmh]. + /// + /// ``` + /// use palette::{Srgb, FromColor, cam16::{Cam16Jmh, Cam16UcsJmh}}; + /// + /// let ucs = Cam16UcsJmh::new(50.0f32, 80.0, 120.0); + /// let partial_from_ucs = Cam16Jmh::from_color(ucs); + /// ``` + #[palette(skip_derives(Cam16UcsJmh))] + cam16_jmh::Cam16Jmh { + /// The [lightness](https://cie.co.at/eilvterm/17-22-063) (J) of the + /// color. + /// + /// See [`Cam16::lightness`][crate::cam16::Cam16::lightness]. + lightness: Lightness, + + /// The [colorfulness](https://cie.co.at/eilvterm/17-22-072) (M) of the + /// color. + /// + /// See [`Cam16::colorfulness`][crate::cam16::Cam16::colorfulness]. + colorfulness: Colorfulness + } +} + +/// Partial CIE CAM16 with lightness, saturation, and an alpha component. +/// +/// See the [`Cam16Jsha` implementation in `Alpha`](crate::Alpha#Cam16Jsha). +pub type Cam16Jsha = Alpha, T>; +make_partial_cam16! { + cam16_jsh::Cam16Jsh { + /// The [lightness](https://cie.co.at/eilvterm/17-22-063) (J) of the + /// color. + /// + /// See [`Cam16::lightness`][crate::cam16::Cam16::lightness]. + lightness: Lightness, + + /// The [saturation](https://cie.co.at/eilvterm/17-22-073) (s) of the + /// color. + /// + /// See ['Cam16::saturation][crate::cam16::Cam16::saturation]. + saturation: Saturation + } +} + +/// Partial CIE CAM16 with brightness, chroma, and an alpha component. +/// +/// See the [`Cam16Qcha` implementation in `Alpha`](crate::Alpha#Cam16Qcha). +pub type Cam16Qcha = Alpha, T>; +make_partial_cam16! { + cam16_qch::Cam16Qch { + /// The [brightness](https://cie.co.at/eilvterm/17-22-059) (Q) of the + /// color. + /// + /// See [`Cam16::brightness`][crate::cam16::Cam16::brightness]. + brightness: Brightness, + + /// The [chroma](https://cie.co.at/eilvterm/17-22-074) (C) of the color. + /// + /// See [`Cam16::chroma`][crate::cam16::Cam16::chroma]. + chroma: Chroma + } +} + +/// Partial CIE CAM16 with brightness, colorfulness, and an alpha component. +/// +/// See the [`Cam16Qmha` implementation in `Alpha`](crate::Alpha#Cam16Qmha). +pub type Cam16Qmha = Alpha, T>; +make_partial_cam16! { + cam16_qmh::Cam16Qmh { + /// The [brightness](https://cie.co.at/eilvterm/17-22-059) (Q) of the + /// color. + /// + /// See [`Cam16::brightness`][crate::cam16::Cam16::brightness]. + brightness: Brightness, + + /// The [colorfulness](https://cie.co.at/eilvterm/17-22-072) (M) of the + /// color. + /// + /// See [`Cam16::colorfulness`][crate::cam16::Cam16::colorfulness]. + colorfulness: Colorfulness + } +} + +/// Partial CIE CAM16 with brightness, saturation, and an alpha component. +/// +/// See the [`Cam16Qsha` implementation in `Alpha`](crate::Alpha#Cam16Qsha). +pub type Cam16Qsha = Alpha, T>; +make_partial_cam16! { + cam16_qsh::Cam16Qsh { + /// The [brightness](https://cie.co.at/eilvterm/17-22-059) (Q) of the + /// color. + /// + /// See [`Cam16::brightness`][crate::cam16::Cam16::brightness]. + brightness: Brightness, + + /// The [saturation](https://cie.co.at/eilvterm/17-22-073) (s) of the + /// color. + /// + /// See ['Cam16::saturation][crate::cam16::Cam16::saturation]. + saturation: Saturation + } +} + +impl FromColorUnclamped> for Cam16Jmh +where + T: Real + One + Exp + Arithmetics + Clone, +{ + #[inline] + fn from_color_unclamped(val: Cam16UcsJmh) -> Self { + let colorfulness = + ((val.colorfulness * T::from_f64(0.0228)).exp() - T::one()) / T::from_f64(0.0228); + let lightness = + val.lightness.clone() / (T::from_f64(1.7) - T::from_f64(0.007) * val.lightness); + + Self { + hue: val.hue, + colorfulness, + lightness, + } + } +} + +#[cfg(test)] +#[cfg(feature = "approx")] +mod test { + use super::{Cam16Jch, Cam16Jmh, Cam16Jsh, Cam16Qch, Cam16Qmh, Cam16Qsh}; + use crate::{ + cam16::{Cam16, Parameters, StaticWp}, + convert::IntoColorUnclamped, + white_point::D65, + Srgb, + }; + + macro_rules! assert_partial_to_full { + ($cam16: expr) => {assert_partial_to_full!($cam16,)}; + ($cam16: expr, $($params:tt)*) => { + assert_relative_eq!( + Cam16Jch::from($cam16).into_full(Parameters::, _>::TEST_DEFAULTS), + $cam16, + $($params)* + ); + assert_relative_eq!( + Cam16Jmh::from($cam16).into_full(Parameters::, _>::TEST_DEFAULTS), + $cam16, + $($params)* + ); + assert_relative_eq!( + Cam16Jsh::from($cam16).into_full(Parameters::, _>::TEST_DEFAULTS), + $cam16, + $($params)* + ); + + assert_relative_eq!( + Cam16Qch::from($cam16).into_full(Parameters::, _>::TEST_DEFAULTS), + $cam16, + $($params)* + ); + assert_relative_eq!( + Cam16Qmh::from($cam16).into_full(Parameters::, _>::TEST_DEFAULTS), + $cam16, + $($params)* + ); + assert_relative_eq!( + Cam16Qsh::from($cam16).into_full(Parameters::, _>::TEST_DEFAULTS), + $cam16, + $($params)* + ); + }; + } + + #[test] + fn example_blue() { + // Uses the example color from https://observablehq.com/@jrus/cam16 + let xyz = Srgb::from(0x5588cc).into_linear().into_color_unclamped(); + let cam16: Cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS); + assert_partial_to_full!(cam16, epsilon = 0.0000000000001); + } + + #[test] + fn black() { + // Checks against the output from https://observablehq.com/@jrus/cam16 + let xyz = Srgb::from(0x000000).into_linear().into_color_unclamped(); + let cam16: Cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS); + assert_partial_to_full!(cam16); + } + + #[test] + fn white() { + // Checks against the output from https://observablehq.com/@jrus/cam16 + let xyz = Srgb::from(0xffffff).into_linear().into_color_unclamped(); + let cam16: Cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS); + assert_partial_to_full!(cam16, epsilon = 0.000000000000001); + } + + #[test] + fn red() { + // Checks against the output from https://observablehq.com/@jrus/cam16 + let xyz = Srgb::from(0xff0000).into_linear().into_color_unclamped(); + let cam16: Cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS); + assert_partial_to_full!(cam16, epsilon = 0.0000000000001); + } + + #[test] + fn green() { + // Checks against the output from https://observablehq.com/@jrus/cam16 + let xyz = Srgb::from(0x00ff00).into_linear().into_color_unclamped(); + let cam16: Cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS); + assert_partial_to_full!(cam16, epsilon = 0.0000000000001); + } + + #[test] + fn blue() { + // Checks against the output from https://observablehq.com/@jrus/cam16 + let xyz = Srgb::from(0x0000ff).into_linear().into_color_unclamped(); + let cam16: Cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS); + assert_partial_to_full!(cam16); + } +} diff --git a/palette/src/cam16/ucs_jab.rs b/palette/src/cam16/ucs_jab.rs new file mode 100644 index 000000000..4bbe0311b --- /dev/null +++ b/palette/src/cam16/ucs_jab.rs @@ -0,0 +1,369 @@ +use core::ops::Mul; + +use crate::{ + angle::RealAngle, + bool_mask::HasBoolMask, + color_difference::{DeltaE, EuclideanDistance, ImprovedDeltaE}, + convert::FromColorUnclamped, + num::{MinMax, Powf, Real, Sqrt, Trigonometry, Zero}, + Alpha, +}; + +use super::Cam16UcsJmh; + +/// Cartesian CAM16-UCS with an alpha component. +/// +/// See the [`Cam16UcsJaba` implementation in +/// `Alpha`](crate::Alpha#Cam16UcsJaba). +pub type Cam16UcsJaba = Alpha, T>; + +/// 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 = Cam16UcsJab::new(50.0, 80.0, -30.0); +/// +/// // Customize these according to the viewing conditions: +/// let mut example_parameters = Parameters::default_static_wp(40.0); +/// +/// // 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( + palette_internal, + component = "T", + skip_derives(Cam16UcsJmh, Cam16UcsJab) +)] +#[repr(C)] +pub struct Cam16UcsJab { + /// 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 Cam16UcsJab { + /// 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 `(J', a', b')` tuple. + pub fn into_components(self) -> (T, T, T) { + (self.lightness, self.a, self.b) + } + + /// Convert from a `(J', a', b')` tuple. + pub fn from_components((lightness, a, b): (T, T, T)) -> Self { + Self::new(lightness, a, b) + } +} + +impl Cam16UcsJab +where + T: Zero + Real, +{ + /// Return the `lightness` value minimum. + pub fn min_lightness() -> T { + T::zero() + } + + /// Return the `lightness` value maximum. + pub fn max_lightness() -> T { + T::from_f64(100.0) + } + + /// Return an `a` value minimum that includes the sRGB gamut. + /// + ///

+ /// This is entirely arbitrary and only for use in random generation. + /// Colorfulness doesn't have a well defined upper bound, which makes + /// a' unbounded. + ///

+ 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. + /// + ///

+ /// This is entirely arbitrary and only for use in random generation. + /// Colorfulness doesn't have a well defined upper bound, which makes + /// a' unbounded. + ///

+ 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. + /// + ///

+ /// This is entirely arbitrary and only for use in random generation. + /// Colorfulness doesn't have a well defined upper bound, which makes + /// b' unbounded. + ///

+ 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. + /// + ///

+ /// This is entirely arbitrary and only for use in random generation. + /// Colorfulness doesn't have a well defined upper bound, which makes + /// b' unbounded. + ///

+ pub fn max_srgb_b() -> T { + // Based on a plot from https://facelessuser.github.io/coloraide/colors/cam16_ucs/ + T::from_f64(50.0) + } +} + +///[`Cam16UcsJaba`](crate::cam16::Cam16UcsJaba) implementations. +impl Alpha, A> { + /// 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 `(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 `(J', a', b', a)` tuple. + pub fn from_components((lightness, a, b, alpha): (T, T, T, A)) -> Self { + Self::new(lightness, a, b, alpha) + } +} + +impl FromColorUnclamped> for Cam16UcsJab { + fn from_color_unclamped(val: Cam16UcsJab) -> Self { + val + } +} + +impl FromColorUnclamped> for Cam16UcsJab +where + T: RealAngle + Zero + Mul + Trigonometry + MinMax + Clone, +{ + fn from_color_unclamped(val: Cam16UcsJmh) -> Self { + let (a, b) = val.hue.into_cartesian(); + let colorfulness = val.colorfulness.max(T::zero()); + + Self { + lightness: val.lightness, + a: a * colorfulness.clone(), + b: b * colorfulness, + } + } +} + +impl DeltaE for Cam16UcsJab +where + Self: EuclideanDistance, + T: Sqrt, +{ + type Scalar = T; + + #[inline] + fn delta_e(self, other: Self) -> Self::Scalar { + self.distance(other) + } +} + +impl ImprovedDeltaE for Cam16UcsJab +where + Self: DeltaE + EuclideanDistance, + T: Real + Mul + Powf, +{ + #[inline] + fn improved_delta_e(self, other: Self) -> Self::Scalar { + // Coefficients from "Power functions improving the performance of + // color-difference formulas" by Huang et al. + // https://opg.optica.org/oe/fulltext.cfm?uri=oe-23-1-597&id=307643 + // + // The multiplication of 0.5 in the exponent makes it square root the + // squared distance. + T::from_f64(1.41) * self.distance_squared(other).powf(T::from_f64(0.63 * 0.5)) + } +} + +impl HasBoolMask for Cam16UcsJab +where + T: HasBoolMask, +{ + type Mask = T::Mask; +} + +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Zeroable for Cam16UcsJab where T: bytemuck::Zeroable {} + +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Pod for Cam16UcsJab where T: bytemuck::Pod {} + +// Macro implementations + +impl_reference_component_methods!(Cam16UcsJab, [lightness, a, b]); +impl_struct_of_arrays_methods!(Cam16UcsJab, [lightness, a, b]); + +impl_tuple_conversion!(Cam16UcsJab as (T, T, T)); + +impl_is_within_bounds! { + Cam16UcsJab { + lightness => [Self::min_lightness(), Self::max_lightness()] + } + where T: Real + Zero +} +impl_clamp! { + Cam16UcsJab { + lightness => [Self::min_lightness(), Self::max_lightness()] + } + other {a, b} + where T: Real + Zero +} + +impl_mix!(Cam16UcsJab); +impl_lighten!(Cam16UcsJab increase {lightness => [Self::min_lightness(), Self::max_lightness()]} other {a, b}); +impl_premultiply!(Cam16UcsJab { lightness, a, b }); +impl_euclidean_distance!(Cam16UcsJab { lightness, a, b }); +impl_hyab!(Cam16UcsJab { + lightness: lightness, + chroma1: a, + chroma2: b +}); +impl_lab_color_schemes!(Cam16UcsJab[lightness]); + +impl_color_add!(Cam16UcsJab, [lightness, a, b]); +impl_color_sub!(Cam16UcsJab, [lightness, a, b]); +impl_color_mul!(Cam16UcsJab, [lightness, a, b]); +impl_color_div!(Cam16UcsJab, [lightness, a, b]); + +impl_array_casts!(Cam16UcsJab, [T; 3]); +impl_simd_array_conversion!(Cam16UcsJab, [lightness, a, b]); +impl_struct_of_array_traits!(Cam16UcsJab, [lightness, a, b]); + +impl_eq!(Cam16UcsJab, [lightness, a, b]); + +impl_rand_traits_cartesian!( + UniformCam16UcsJab, + Cam16UcsJab { + lightness => [|x| x * Cam16UcsJab::::max_lightness()], + a => [|x| Cam16UcsJab::::min_srgb_a() + x * (Cam16UcsJab::::max_srgb_a() - Cam16UcsJab::::min_srgb_a())], + b => [|x| Cam16UcsJab::::min_srgb_b() + x * (Cam16UcsJab::::max_srgb_b() - Cam16UcsJab::::min_srgb_b())] + } + where T: Real + Zero + core::ops::Add + core::ops::Sub + core::ops::Mul +); + +// Unit test + +#[cfg(test)] +mod test { + #[cfg(feature = "approx")] + use crate::{cam16::Cam16Jmh, convert::FromColorUnclamped}; + + use super::Cam16UcsJab; + + #[test] + fn ranges() { + assert_ranges! { + Cam16UcsJab; + clamped { + lightness: 0.0 => 100.0 + } + clamped_min {} + unclamped { + a: -100.0 => 100.0, + b: -100.0 => 100.0 + } + } + } + + #[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); + + struct_of_arrays_tests!( + Cam16UcsJab[lightness, a, b], + super::Cam16UcsJaba::new(0.1f32, 0.2, 0.3, 0.4), + super::Cam16UcsJaba::new(0.2, 0.3, 0.4, 0.5), + super::Cam16UcsJaba::new(0.3, 0.4, 0.5, 0.6) + ); + + #[cfg(feature = "serializing")] + #[test] + fn serialize() { + let serialized = ::serde_json::to_string(&Cam16UcsJab::::new(0.3, 0.8, 0.1)).unwrap(); + + assert_eq!(serialized, r#"{"lightness":0.3,"a":0.8,"b":0.1}"#); + } + + #[cfg(feature = "serializing")] + #[test] + fn deserialize() { + let deserialized: Cam16UcsJab = + ::serde_json::from_str(r#"{"lightness":0.3,"a":0.8,"b":0.1}"#).unwrap(); + + assert_eq!(deserialized, Cam16UcsJab::new(0.3, 0.8, 0.1)); + } + + test_uniform_distribution! { + Cam16UcsJab { + lightness: (0.0, 100.0), + a: (-50.0, 50.0), + b: (-50.0, 50.0) + }, + min: Cam16UcsJab::new(0.0f32, -50.0, -50.0), + max: Cam16UcsJab::new(100.0, 50.0, 50.0) + } +} diff --git a/palette/src/cam16/ucs_jmh.rs b/palette/src/cam16/ucs_jmh.rs new file mode 100644 index 000000000..68a3c8cc6 --- /dev/null +++ b/palette/src/cam16/ucs_jmh.rs @@ -0,0 +1,451 @@ +use crate::{ + angle::RealAngle, + bool_mask::HasBoolMask, + color_difference::{DeltaE, ImprovedDeltaE}, + convert::{FromColorUnclamped, IntoColorUnclamped}, + hues::{Cam16Hue, Cam16HueIter}, + num::{Arithmetics, Hypot, Ln, One, Real, Trigonometry, Zero}, + Alpha, +}; + +use super::{Cam16Jmh, Cam16UcsJab}; + +/// Polar CAM16-UCS with an alpha component. +/// +/// See the [`Cam16UcsJmha` implementation in +/// `Alpha`](crate::Alpha#Cam16UcsJmha). +pub type Cam16UcsJmha = Alpha, 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 = 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(40.0); +/// +/// // 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( + palette_internal, + component = "T", + skip_derives(Cam16Jmh, Cam16UcsJmh, Cam16UcsJab) +)] +#[repr(C)] +pub struct Cam16UcsJmh { + /// 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 (M') of the color. + /// + /// It's derived from [`Cam16::colorfulness`][crate::cam16::Cam16::colorfulness]. + pub colorfulness: T, + + /// 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, +} + +impl Cam16UcsJmh { + /// Create a CAM16-UCS J' M' h' color. + pub fn new>>(lightness: T, colorfulness: T, hue: H) -> Self { + Self::new_const(lightness, colorfulness, hue.into()) + } + + /// Create a CAM16-UCS J' M' h' color. This is the same as + /// `Cam16UcsJmh::new` without the generic hue type. It's temporary until + /// `const fn` supports traits. + pub const fn new_const(lightness: T, colorfulness: T, hue: Cam16Hue) -> Self { + Self { + lightness, + colorfulness, + hue, + } + } + + /// Convert to a `(J', M', h')` tuple. + pub fn into_components(self) -> (T, T, Cam16Hue) { + (self.lightness, self.colorfulness, self.hue) + } + + /// Convert from a `(J', M', h')` tuple. + pub fn from_components>>( + (lightness, colorfulness, hue): (T, T, H), + ) -> Self { + Self::new(lightness, colorfulness, hue) + } +} + +impl Cam16UcsJmh +where + T: Zero + Real, +{ + /// Return the `lightness` value minimum. + pub fn min_lightness() -> T { + T::zero() + } + + /// Return the `lightness` value maximum. + pub fn max_lightness() -> T { + T::from_f64(100.0) + } + + /// Return the `colorfulness` value minimum. + pub fn min_colorfulness() -> T { + T::zero() + } + + /// Return a `colorfulness` value maximum that includes the sRGB gamut. + /// + ///

+ /// This is entirely arbitrary and only for use in `Lighten`, `Darken` and + /// random generation. Colorfulness doesn't have a well defined upper + /// bound. + ///

+ pub fn max_srgb_colorfulness() -> T { + // Based on a plot from https://facelessuser.github.io/coloraide/colors/cam16_ucs/ + T::from_f64(50.0) + } +} + +///[`Cam16UcsJmha`](crate::cam16::Cam16UcsJmha) implementations. +impl Alpha, A> { + /// Create a CAM16-UCS J' M' h' color with transparency. + pub fn new>>(lightness: T, colorfulness: T, hue: H, alpha: A) -> Self { + Self::new_const(lightness, colorfulness, hue.into(), alpha) + } + + /// Create a CAM16-UCS J' M' h' color with transparency. This is the same as + /// `Cam16UcsJmha::new` without the generic hue type. It's temporary until + /// `const fn` supports traits. + pub const fn new_const(lightness: T, colorfulness: T, hue: Cam16Hue, alpha: A) -> Self { + Self { + color: Cam16UcsJmh::new_const(lightness, colorfulness, hue), + alpha, + } + } + + /// Convert to a `(J', M', h', a)` tuple. + pub fn into_components(self) -> (T, T, Cam16Hue, A) { + ( + self.color.lightness, + self.color.colorfulness, + self.color.hue, + self.alpha, + ) + } + + /// Convert from a `(J', M', h', a)` tuple. + pub fn from_components>>( + (lightness, colorfulness, hue, alpha): (T, T, H, A), + ) -> Self { + Self::new(lightness, colorfulness, hue, alpha) + } +} + +impl FromColorUnclamped> for Cam16UcsJmh { + fn from_color_unclamped(val: Cam16UcsJmh) -> Self { + val + } +} + +impl FromColorUnclamped> for Cam16UcsJmh +where + T: Real + One + Ln + Arithmetics, +{ + fn from_color_unclamped(val: Cam16Jmh) -> Self { + let colorfulness = + (T::one() + T::from_f64(0.0228) * val.colorfulness).ln() / T::from_f64(0.0228); + let lightness = + T::from_f64(1.7) * &val.lightness / (T::one() + T::from_f64(0.007) * val.lightness); + + Cam16UcsJmh { + lightness, + colorfulness, + hue: val.hue, + } + } +} + +impl FromColorUnclamped> for Cam16UcsJmh +where + T: RealAngle + Hypot + Trigonometry + Arithmetics + Clone, +{ + fn from_color_unclamped(val: Cam16UcsJab) -> Self { + Self { + lightness: val.lightness, + colorfulness: val.a.clone().hypot(val.b.clone()), + hue: Cam16Hue::from_cartesian(val.a, val.b), + } + } +} + +impl DeltaE for Cam16UcsJmh +where + Cam16UcsJab: DeltaE + FromColorUnclamped, +{ + type Scalar = T; + + #[inline] + fn delta_e(self, other: Self) -> Self::Scalar { + // Jab and Jmh have the same delta E. + Cam16UcsJab::from_color_unclamped(self).delta_e(other.into_color_unclamped()) + } +} + +impl ImprovedDeltaE for Cam16UcsJmh +where + Cam16UcsJab: ImprovedDeltaE + FromColorUnclamped, +{ + #[inline] + fn improved_delta_e(self, other: Self) -> Self::Scalar { + // Jab and Jmh have the same delta E. + Cam16UcsJab::from_color_unclamped(self).improved_delta_e(other.into_color_unclamped()) + } +} + +impl HasBoolMask for Cam16UcsJmh +where + T: HasBoolMask, +{ + type Mask = T::Mask; +} + +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Zeroable for Cam16UcsJmh where T: bytemuck::Zeroable {} + +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Pod for Cam16UcsJmh where T: bytemuck::Pod {} + +// Macro implementations + +impl_reference_component_methods_hue!(Cam16UcsJmh, [lightness, colorfulness]); +impl_struct_of_arrays_methods_hue!(Cam16UcsJmh, [lightness, colorfulness]); +impl_tuple_conversion_hue!(Cam16UcsJmh as (T, T, H), Cam16Hue); + +impl_is_within_bounds! { + Cam16UcsJmh { + lightness => [Self::min_lightness(), Self::max_lightness()], + colorfulness => [Self::min_colorfulness(), None] + } + where T: Zero + Real +} +impl_clamp! { + Cam16UcsJmh { + lightness => [Self::min_lightness(), Self::max_lightness()], + colorfulness => [Self::min_colorfulness()] + } + other {hue} + where T: Zero + Real +} + +impl_mix_hue!(Cam16UcsJmh { + lightness, + colorfulness +}); +impl_lighten!(Cam16UcsJmh increase {lightness => [Self::min_lightness(), Self::max_lightness()]} other {hue, colorfulness}); +impl_saturate!(Cam16UcsJmh increase {colorfulness => [Self::min_colorfulness(), Self::max_srgb_colorfulness()]} other {hue, lightness}); +impl_hue_ops!(Cam16UcsJmh, Cam16Hue); + +impl_color_add!(Cam16UcsJmh, [lightness, colorfulness, hue]); +impl_color_sub!(Cam16UcsJmh, [lightness, colorfulness, hue]); + +impl_array_casts!(Cam16UcsJmh, [T; 3]); +impl_simd_array_conversion_hue!(Cam16UcsJmh, [lightness, colorfulness]); +impl_struct_of_array_traits_hue!(Cam16UcsJmh, Cam16HueIter, [lightness, colorfulness]); + +impl_eq_hue!(Cam16UcsJmh, Cam16Hue, [lightness, colorfulness, hue]); + +impl_rand_traits_cylinder!( + UniformCam16UcsJmh, + Cam16UcsJmh { + hue: UniformCam16Hue => Cam16Hue, + height: lightness => [|l: T| l * Cam16UcsJmh::::max_lightness()], + radius: colorfulness => [|c| c * Cam16UcsJmh::::max_srgb_colorfulness()] + } + where T: Real + Zero + core::ops::Mul, +); + +// Unit tests + +#[cfg(test)] +mod test { + use crate::{ + cam16::{Cam16Jmh, Cam16UcsJmh}, + convert::FromColorUnclamped, + }; + + #[cfg(feature = "approx")] + use crate::color_difference::DeltaE; + + #[cfg(all(feature = "approx", feature = "alloc"))] + use crate::{ + cam16::Cam16UcsJab, color_difference::ImprovedDeltaE, convert::IntoColorUnclamped, + }; + + #[test] + fn ranges() { + assert_ranges! { + Cam16UcsJmh; + clamped { + lightness: 0.0 => 100.0 + } + clamped_min { + colorfulness: 0.0 => 200.0 + } + unclamped { + hue: -360.0 => 360.0 + } + } + } + + #[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); + + #[test] + #[cfg(feature = "approx")] + fn delta_e_large_hue_diff() { + let lhs1 = Cam16UcsJmh::::new(50.0, 64.0, -730.0); + let rhs1 = Cam16UcsJmh::new(50.0, 64.0, 730.0); + + let lhs2 = Cam16UcsJmh::::new(50.0, 64.0, -10.0); + let rhs2 = Cam16UcsJmh::new(50.0, 64.0, 10.0); + + assert_relative_eq!( + lhs1.delta_e(rhs1), + lhs2.delta_e(rhs2), + epsilon = 0.0000000000001 + ); + } + + // Jab and Jmh have the same delta E. + #[test] + #[cfg(all(feature = "approx", feature = "alloc"))] + fn jab_delta_e_equality() { + let mut jab_colors: Vec> = alloc::vec::Vec::new(); + + for j_step in 0i8..5 { + for a_step in -2i8..3 { + for b_step in -2i8..3 { + jab_colors.push(Cam16UcsJab::new( + j_step as f64 * 25.0, + a_step as f64 * 60.0, + b_step as f64 * 60.0, + )) + } + } + } + + let jmh_colors: alloc::vec::Vec> = jab_colors.clone().into_color_unclamped(); + + for (&lhs_jab, &lhs_jmh) in jab_colors.iter().zip(&jmh_colors) { + for (&rhs_jab, &rhs_jmh) in jab_colors.iter().zip(&jmh_colors) { + let delta_e_jab = lhs_jab.delta_e(rhs_jab); + let delta_e_jmh = lhs_jmh.delta_e(rhs_jmh); + assert_relative_eq!(delta_e_jab, delta_e_jmh, epsilon = 0.0000000000001); + } + } + } + + // Jab and Jmh have the same delta E, so should also have the same improved + // delta E. + #[test] + #[cfg(all(feature = "approx", feature = "alloc"))] + fn jab_improved_delta_e_equality() { + let mut jab_colors: Vec> = alloc::vec::Vec::new(); + + for j_step in 0i8..5 { + for a_step in -2i8..3 { + for b_step in -2i8..3 { + jab_colors.push(Cam16UcsJab::new( + j_step as f64 * 25.0, + a_step as f64 * 60.0, + b_step as f64 * 60.0, + )) + } + } + } + + let jmh_colors: alloc::vec::Vec> = jab_colors.clone().into_color_unclamped(); + + for (&lhs_jab, &lhs_jmh) in jab_colors.iter().zip(&jmh_colors) { + for (&rhs_jab, &rhs_jmh) in jab_colors.iter().zip(&jmh_colors) { + let delta_e_jab = lhs_jab.improved_delta_e(rhs_jab); + let delta_e_jmh = lhs_jmh.improved_delta_e(rhs_jmh); + assert_relative_eq!(delta_e_jab, delta_e_jmh, epsilon = 0.0000000000001); + } + } + } + + struct_of_arrays_tests!( + Cam16UcsJmh[lightness, colorfulness, hue], + super::Cam16UcsJmha::new(0.1f32, 0.2, 0.3, 0.4), + super::Cam16UcsJmha::new(0.2, 0.3, 0.4, 0.5), + super::Cam16UcsJmha::new(0.3, 0.4, 0.5, 0.6) + ); + + #[cfg(feature = "serializing")] + #[test] + fn serialize() { + let serialized = ::serde_json::to_string(&Cam16UcsJmh::new(0.3, 0.8, 0.1)).unwrap(); + + assert_eq!( + serialized, + r#"{"lightness":0.3,"colorfulness":0.8,"hue":0.1}"# + ); + } + + #[cfg(feature = "serializing")] + #[test] + fn deserialize() { + let deserialized: Cam16UcsJmh = + ::serde_json::from_str(r#"{"lightness":0.3,"colorfulness":0.8,"hue":0.1}"#).unwrap(); + + assert_eq!(deserialized, Cam16UcsJmh::new(0.3, 0.8, 0.1)); + } + + test_uniform_distribution! { + Cam16UcsJmh as crate::cam16::Cam16UcsJab { + lightness: (0.0, 100.0), + a: (-30.0, 30.0), + b: (-30.0, 30.0), + }, + min: Cam16UcsJmh::new(0.0f32, 0.0, 0.0), + max: Cam16UcsJmh::new(100.0, 50.0, 360.0) + } +} diff --git a/palette/src/color_difference.rs b/palette/src/color_difference.rs index a200bf70b..96af3823f 100644 --- a/palette/src/color_difference.rs +++ b/palette/src/color_difference.rs @@ -21,7 +21,7 @@ //! | [`Ciede2000`] | High | High for small differences, lower for large differences | The de-facto standard, but requires complex calculations to compensate for increased errors in certain areas of the CIE L\*a\*b\* (CIELAB) space. //! | [`ImprovedCiede2000`] | High | High for small differences, lower for large differences | A general improvement of [`Ciede2000`], using a formula by Huang et al. //! | [`DeltaE`] | Usually low | Medium to high | The formula differs between color spaces and may not always be the best. Other formulas, such as [`Ciede2000`], may be preferred for some spaces. -//! | [`ImprovedDeltaE`] | Usually low | Medium to high | A general improvement of [`DeltaE`], using a formula by Huang et al. +//! | [`ImprovedDeltaE`] | Usually low to medium | Medium to high | A general improvement of [`DeltaE`], using a formula by Huang et al. //! | [`EuclideanDistance`] | Low | Medium to high for perceptually uniform spaces, otherwise low | Can be good enough for perceptually uniform spaces or as a "quick and dirty" check. //! | [`HyAb`] | Low | High accuracy for medium to large differences. Less accurate than CIEDE2000 for small differences, but still performs well and is much less computationally expensive. | Similar to Euclidean distance, but separates lightness and chroma more. Limited to Cartesian spaces with a lightness axis and a chroma plane. //! | [`Wcag21RelativeContrast`] | Low | Low and only compares lightness | Meant for checking contrasts in computer graphics (such as between text and background colors), assuming sRGB. Mostly useful as a hint or for checking WCAG 2.1 compliance, considering the criticism it has received. diff --git a/palette/src/hues.rs b/palette/src/hues.rs index 6505574b6..cfc17e318 100644 --- a/palette/src/hues.rs +++ b/palette/src/hues.rs @@ -799,6 +799,11 @@ make_hues! { /// /// It's measured in degrees. struct OklabHue; OklabHueIter + + /// A hue type for the CAM16 color appearance model. + /// + /// It's measured in degrees. + struct Cam16Hue; Cam16HueIter } macro_rules! impl_uniform { @@ -894,6 +899,7 @@ impl_uniform!(UniformLabHue, LabHue); impl_uniform!(UniformRgbHue, RgbHue); impl_uniform!(UniformLuvHue, LuvHue); impl_uniform!(UniformOklabHue, OklabHue); +impl_uniform!(UniformCam16Hue, Cam16Hue); #[cfg(test)] mod test { diff --git a/palette/src/lab.rs b/palette/src/lab.rs index 326852a4b..14cf6d746 100644 --- a/palette/src/lab.rs +++ b/palette/src/lab.rs @@ -194,13 +194,13 @@ where T: RealAngle + Zero + MinMax + Trigonometry + Mul + Clone, { fn from_color_unclamped(color: Lch) -> Self { - let (hue_sin, hue_cos) = color.hue.into_raw_radians().sin_cos(); + let (a, b) = color.hue.into_cartesian(); let chroma = color.chroma.max(T::zero()); Lab { l: color.l, - a: hue_cos * chroma.clone(), - b: hue_sin * chroma, + a: a * chroma.clone(), + b: b * chroma, white_point: PhantomData, } } @@ -259,15 +259,18 @@ where impl ImprovedDeltaE for Lab where - Self: DeltaE, - T: Real + Mul + Powf + Sqrt, + Self: DeltaE + EuclideanDistance, + T: Real + Mul + Powf, { #[inline] fn improved_delta_e(self, other: Self) -> Self::Scalar { // Coefficients from "Power functions improving the performance of // color-difference formulas" by Huang et al. // https://opg.optica.org/oe/fulltext.cfm?uri=oe-23-1-597&id=307643 - T::from_f64(1.26) * self.delta_e(other).powf(T::from_f64(0.55)) + // + // The multiplication of 0.5 in the exponent makes it square root the + // squared distance. + T::from_f64(1.26) * self.distance_squared(other).powf(T::from_f64(0.55 * 0.5)) } } diff --git a/palette/src/lib.rs b/palette/src/lib.rs index 43c91e8f9..4bce3c1fd 100644 --- a/palette/src/lib.rs +++ b/palette/src/lib.rs @@ -344,6 +344,7 @@ pub mod alpha; pub mod angle; pub mod blend; pub mod bool_mask; +pub mod cam16; pub mod cast; pub mod chromatic_adaptation; pub mod color_difference; diff --git a/palette/src/macros/casting.rs b/palette/src/macros/casting.rs index 268d853ce..81613033f 100644 --- a/palette/src/macros/casting.rs +++ b/palette/src/macros/casting.rs @@ -1,28 +1,28 @@ #[cfg(test)] macro_rules! raw_pixel_conversion_tests { - ($name: ident <$($ty_param: path),+> : $($component: ident),+) => { + ($name: ident <$($ty_param: path),*> : $($component: ident),+) => { #[test] fn convert_from_f32_array() { - raw_pixel_conversion_tests!(@float_array_test f32, $name<$($ty_param),+>: $($component),+); + raw_pixel_conversion_tests!(@float_array_test f32, $name<$($ty_param),*>: $($component),+); } #[test] fn convert_from_f64_array() { - raw_pixel_conversion_tests!(@float_array_test f64, $name<$($ty_param),+>: $($component),+); + raw_pixel_conversion_tests!(@float_array_test f64, $name<$($ty_param),*>: $($component),+); } #[test] fn convert_from_f32_slice() { - raw_pixel_conversion_tests!(@float_slice_test f32, $name<$($ty_param),+>: $($component),+); + raw_pixel_conversion_tests!(@float_slice_test f32, $name<$($ty_param),*>: $($component),+); } #[test] fn convert_from_f64_slice() { - raw_pixel_conversion_tests!(@float_slice_test f64, $name<$($ty_param),+>: $($component),+); + raw_pixel_conversion_tests!(@float_slice_test f64, $name<$($ty_param),*>: $($component),+); } }; - (@float_array_test $float: ty, $name: ident <$($ty_param: path),+> : $($component: ident),+) => { + (@float_array_test $float: ty, $name: ident <$($ty_param: path),*> : $($component: ident),+) => { use crate::cast::ArrayCast; use crate::Alpha; @@ -33,21 +33,21 @@ macro_rules! raw_pixel_conversion_tests { )+ let alpha = counter + 0.1; - let raw: <$name<$($ty_param,)+ $float> as ArrayCast>::Array = [$($component),+]; - let raw_plus_1: , $float> as ArrayCast>::Array = [ + let raw: <$name<$($ty_param,)* $float> as ArrayCast>::Array = [$($component),+]; + let raw_plus_1: , $float> as ArrayCast>::Array = [ $($component,)+ alpha ]; - let color: $name<$($ty_param,)+ $float> = crate::cast::from_array(raw); + let color: $name<$($ty_param,)* $float> = crate::cast::from_array(raw); - let color_alpha: Alpha<$name<$($ty_param,)+ $float>, $float> = crate::cast::from_array(raw_plus_1); + let color_alpha: Alpha<$name<$($ty_param,)* $float>, $float> = crate::cast::from_array(raw_plus_1); assert_eq!(color, $name::new($($component),+)); - assert_eq!(color_alpha, Alpha::<$name<$($ty_param,)+ $float>, $float>::new($($component,)+ alpha)); + assert_eq!(color_alpha, Alpha::<$name<$($ty_param,)* $float>, $float>::new($($component,)+ alpha)); }; - (@float_slice_test $float: ty, $name: ident <$($ty_param: path),+> : $($component: ident),+) => { + (@float_slice_test $float: ty, $name: ident <$($ty_param: path),*> : $($component: ident),+) => { use core::convert::{TryInto, TryFrom}; use crate::Alpha; @@ -68,39 +68,39 @@ macro_rules! raw_pixel_conversion_tests { alpha, extra ]; - let color: &$name<$($ty_param,)+ $float> = raw.try_into().unwrap(); - assert!(<&$name<$($ty_param,)+ $float>>::try_from(raw_plus_1).is_err()); + let color: &$name<$($ty_param,)* $float> = raw.try_into().unwrap(); + assert!(<&$name<$($ty_param,)* $float>>::try_from(raw_plus_1).is_err()); - let color_alpha: &Alpha<$name<$($ty_param,)+ $float>, $float> = raw_plus_1.try_into().unwrap(); - assert!(<&Alpha<$name<$($ty_param,)+ $float>, $float>>::try_from(raw_plus_2).is_err()); + let color_alpha: &Alpha<$name<$($ty_param,)* $float>, $float> = raw_plus_1.try_into().unwrap(); + assert!(<&Alpha<$name<$($ty_param,)* $float>, $float>>::try_from(raw_plus_2).is_err()); assert_eq!(color, &$name::new($($component),+)); - assert_eq!(color_alpha, &Alpha::<$name<$($ty_param,)+ $float>, $float>::new($($component,)+ alpha)); + assert_eq!(color_alpha, &Alpha::<$name<$($ty_param,)* $float>, $float>::new($($component,)+ alpha)); }; } #[cfg(test)] macro_rules! raw_pixel_conversion_fail_tests { - ($name: ident <$($ty_param: path),+> : $($component: ident),+) => { + ($name: ident <$($ty_param: path),*> : $($component: ident),+) => { #[test] #[should_panic(expected = "TryFromSliceError")] fn convert_from_short_f32_slice() { - raw_pixel_conversion_fail_tests!(@float_slice_test f32, $name<$($ty_param),+>); + raw_pixel_conversion_fail_tests!(@float_slice_test f32, $name<$($ty_param),*>); } #[test] #[should_panic(expected = "TryFromSliceError")] fn convert_from_short_f64_slice() { - raw_pixel_conversion_fail_tests!(@float_slice_test f64, $name<$($ty_param),+>); + raw_pixel_conversion_fail_tests!(@float_slice_test f64, $name<$($ty_param),*>); } }; - (@float_slice_test $float: ty, $name: ident <$($ty_param: path),+>) => { + (@float_slice_test $float: ty, $name: ident <$($ty_param: path),*>) => { use core::convert::TryInto; let raw: &[$float] = &[0.1]; - let _: &$name<$($ty_param,)+ $float> = raw.try_into().unwrap(); + let _: &$name<$($ty_param,)* $float> = raw.try_into().unwrap(); }; } diff --git a/palette/src/macros/clamp.rs b/palette/src/macros/clamp.rs index db8c93616..9483aa69e 100644 --- a/palette/src/macros/clamp.rs +++ b/palette/src/macros/clamp.rs @@ -221,6 +221,7 @@ macro_rules! assert_ranges { let $unclamped = (1..11).map(|i| from - (i as f64 / 10.0) * diff); )* + #[allow(clippy::needless_update)] for assert_ranges!(@make_tuple (), $($clamped,)+ $($clamped_min,)* $($unclamped,)* ) in repeat(()) $(.zip($clamped))+ $(.zip($clamped_min))* $(.zip($unclamped))* { let color: $ty<$($ty_params),+> = $ty { $($clamped: $clamped.into(),)+ @@ -268,6 +269,7 @@ macro_rules! assert_ranges { let $unclamped = (0..11).map(|i| from + (i as f64 / 10.0) * diff); )* + #[allow(clippy::needless_update)] for assert_ranges!(@make_tuple (), $($clamped,)+ $($clamped_min,)* $($unclamped,)* ) in repeat(()) $(.zip($clamped))+ $(.zip($clamped_min))* $(.zip($unclamped))* { let color: $ty<$($ty_params),+> = $ty { $($clamped: $clamped.into(),)+ @@ -308,6 +310,7 @@ macro_rules! assert_ranges { let $unclamped = (1..11).map(|i| to + (i as f64 / 10.0) * diff); )* + #[allow(clippy::needless_update)] for assert_ranges!(@make_tuple (), $($clamped,)+ $($clamped_min,)* $($unclamped,)* ) in repeat(()) $(.zip($clamped))+ $(.zip($clamped_min))* $(.zip($unclamped))* { let color: $ty<$($ty_params),+> = $ty { $($clamped: $clamped.into(),)+ diff --git a/palette/src/macros/convert.rs b/palette/src/macros/convert.rs index 2d315948f..80e343d22 100644 --- a/palette/src/macros/convert.rs +++ b/palette/src/macros/convert.rs @@ -6,7 +6,7 @@ macro_rules! test_convert_into_from_xyz { fn convert_from_xyz() { use crate::FromColor; - let _: $ty = <$ty>::from_color(crate::Xyz::default()); + let _: $ty = <$ty>::from_color(crate::Xyz::::default()); } #[test] diff --git a/palette/src/macros/equality.rs b/palette/src/macros/equality.rs index 5624319cd..234eacceb 100644 --- a/palette/src/macros/equality.rs +++ b/palette/src/macros/equality.rs @@ -1,8 +1,8 @@ macro_rules! impl_eq { - ( $self_ty: ident , [$element: ident]) => { + ( $self_ty: ident , [$element: tt]) => { impl_eq!($self_ty<>, [$element]); }; - ( $self_ty: ident < $($ty_param: ident),* > , [$element: ident]) => { + ( $self_ty: ident < $($ty_param: ident),* > , [$element: tt]) => { impl<$($ty_param,)* T> PartialEq for $self_ty<$($ty_param,)* T> where T: PartialEq, diff --git a/palette/src/num.rs b/palette/src/num.rs index 4338e8cb2..583f88122 100644 --- a/palette/src/num.rs +++ b/palette/src/num.rs @@ -306,6 +306,22 @@ pub trait SaturatingSub { fn saturating_sub(self, other: Rhs) -> Self::Output; } +/// Trait for getting a number that represents the sign of `self`. +pub trait Signum { + /// Returns a number that represents the sign of `self`. For floating point: + /// + /// * `1.0` if the number is positive, `+0.0` or `INFINITY` + /// * `-1.0` if the number is negative, `-0.0` or `NEG_INFINITY` + /// * NaN if the number is NaN + fn signum(self) -> Self; +} + +/// Trait for getting the natural logarithm of `self`. +pub trait Ln { + /// Returns the natural logarithm of `self`. + fn ln(self) -> Self; +} + macro_rules! impl_uint { ($($ty: ident),+) => { $( @@ -705,6 +721,22 @@ macro_rules! impl_float { (self * m) - s } } + + #[cfg(any(feature = "std", all(test, not(feature = "libm"))))] + impl Signum for $ty { + #[inline] + fn signum(self) -> Self { + $ty::signum(self) + } + } + + #[cfg(any(feature = "std", all(test, not(feature = "libm"))))] + impl Ln for $ty { + #[inline] + fn ln(self) -> Self { + $ty::ln(self) + } + } )+ }; } diff --git a/palette/src/num/libm.rs b/palette/src/num/libm.rs index c2da042ce..5637e6dec 100644 --- a/palette/src/num/libm.rs +++ b/palette/src/num/libm.rs @@ -253,3 +253,39 @@ impl MulAdd for f64 { ::libm::fma(self, m, a) } } + +impl Signum for f32 { + #[inline] + fn signum(self) -> Self { + if self.is_nan() { + Self::NAN + } else { + ::libm::copysignf(1.0, self) + } + } +} + +impl Signum for f64 { + #[inline] + fn signum(self) -> Self { + if self.is_nan() { + Self::NAN + } else { + ::libm::copysign(1.0, self) + } + } +} + +impl Ln for f32 { + #[inline] + fn ln(self) -> Self { + ::libm::logf(self) + } +} + +impl Ln for f64 { + #[inline] + fn ln(self) -> Self { + ::libm::log(self) + } +} diff --git a/palette/src/num/wide.rs b/palette/src/num/wide.rs index 69aa4eb14..550611855 100644 --- a/palette/src/num/wide.rs +++ b/palette/src/num/wide.rs @@ -298,6 +298,20 @@ macro_rules! impl_wide_float { $ty::mul_sub(self, m, s) } } + + impl Signum for $ty { + #[inline] + fn signum(self) -> Self { + self.is_nan().blend(Self::from($scalar::NAN), $ty::copysign(Self::from(1.0), self)) + } + } + + impl Ln for $ty { + #[inline] + fn ln(self) -> Self { + self.ln() + } + } )+ }; } diff --git a/palette/src/serde/alpha_deserializer.rs b/palette/src/serde/alpha_deserializer.rs index 1ebc48a17..92157f7fa 100644 --- a/palette/src/serde/alpha_deserializer.rs +++ b/palette/src/serde/alpha_deserializer.rs @@ -490,10 +490,12 @@ where { // We need the field count here to get the last tuple field. No field // count implies that we definitely expected a struct or a map. - let field_count = self.field_count.ok_or(serde::de::Error::invalid_type( - serde::de::Unexpected::Unsigned(v), - &"map key or struct field", - ))?; + let field_count = self.field_count.ok_or_else(|| { + serde::de::Error::invalid_type( + serde::de::Unexpected::Unsigned(v), + &"map key or struct field", + ) + })?; // Assume that it's the alpha value if it's after the expected number of // fields. Otherwise, pass on to the wrapped type's deserializer. diff --git a/palette/src/stimulus.rs b/palette/src/stimulus.rs index ca16919ea..85031b925 100644 --- a/palette/src/stimulus.rs +++ b/palette/src/stimulus.rs @@ -185,7 +185,7 @@ impl IntoStimulus for u8 { fn into_stimulus(self) -> f32 { let comp_u = u32::from(self) + C23; let comp_f = f32::from_bits(comp_u) - f32::from_bits(C23); - let max_u = u32::from(core::u8::MAX) + C23; + let max_u = u32::from(u8::MAX) + C23; let max_f = (f32::from_bits(max_u) - f32::from_bits(C23)).recip(); comp_f * max_f } @@ -197,7 +197,7 @@ impl IntoStimulus for u8 { fn into_stimulus(self) -> f64 { let comp_u = u64::from(self) + C52; let comp_f = f64::from_bits(comp_u) - f64::from_bits(C52); - let max_u = u64::from(core::u8::MAX) + C52; + let max_u = u64::from(u8::MAX) + C52; let max_f = (f64::from_bits(max_u) - f64::from_bits(C52)).recip(); comp_f * max_f } @@ -354,7 +354,7 @@ mod test { #[test] fn uint_to_float() { fn into_stimulus_old(n: u8) -> f32 { - let max = core::u8::MAX as f32; + let max = u8::MAX as f32; n as f32 / max } @@ -367,7 +367,7 @@ mod test { #[test] fn uint_to_double() { fn into_stimulus_old(n: u8) -> f64 { - let max = core::u8::MAX as f64; + let max = u8::MAX as f64; n as f64 / max } diff --git a/palette/src/xyz.rs b/palette/src/xyz.rs index 07f9c4c3d..3139aadd0 100644 --- a/palette/src/xyz.rs +++ b/palette/src/xyz.rs @@ -3,12 +3,20 @@ use core::{marker::PhantomData, ops::Mul}; use crate::{ + angle::{RealAngle, SignedAngle}, bool_mask::{HasBoolMask, LazySelect}, + cam16::{ + Cam16, Cam16IntoUnclamped, Cam16Jch, Cam16Jmh, Cam16Jsh, Cam16Qch, Cam16Qmh, Cam16Qsh, + FromCam16Unclamped, WhitePointParameter, + }, convert::{FromColorUnclamped, IntoColorUnclamped}, encoding::IntoLinear, luma::LumaStandard, matrix::{matrix_map, multiply_rgb_to_xyz, multiply_xyz, rgb_to_xyz_matrix}, - num::{Arithmetics, FromScalar, IsValidDivisor, One, PartialCmp, Powi, Real, Recip, Zero}, + num::{ + Abs, Arithmetics, FromScalar, IsValidDivisor, One, PartialCmp, Powf, Powi, Real, Recip, + Signum, Sqrt, Trigonometry, Zero, + }, oklab, rgb::{Primaries, Rgb, RgbSpace, RgbStandard}, stimulus::{Stimulus, StimulusColor}, @@ -318,6 +326,61 @@ where } } +impl FromCam16Unclamped> for Xyz +where + WpParam: WhitePointParameter, + T: FromScalar, + Cam16Jch: Cam16IntoUnclamped, +{ + type Scalar = T::Scalar; + + fn from_cam16_unclamped( + cam16: Cam16, + parameters: crate::cam16::BakedParameters, + ) -> Self { + Cam16Jch::from(cam16).cam16_into_unclamped(parameters) + } +} + +macro_rules! impl_from_cam16_partial { + ($($name: ident),+) => { + $( + impl FromCam16Unclamped> for Xyz + where + WpParam: WhitePointParameter, + T: Real + + FromScalar + + One + + Zero + + Sqrt + + Powf + + Abs + + Signum + + Arithmetics + + Trigonometry + + RealAngle + + SignedAngle + + PartialCmp + + Clone, + T::Mask: LazySelect + Clone, + T::Scalar: Clone, + { + type Scalar = T::Scalar; + + fn from_cam16_unclamped( + cam16: $name, + parameters: crate::cam16::BakedParameters, + ) -> Self { + crate::cam16::math::cam16_to_xyz(cam16.into_dynamic(), parameters.inner) + .with_white_point() + } + } + )+ + }; +} + +impl_from_cam16_partial!(Cam16Jmh, Cam16Jch, Cam16Jsh, Cam16Qmh, Cam16Qch, Cam16Qsh); + impl_tuple_conversion!(Xyz as (T, T, T)); impl_is_within_bounds! { diff --git a/palette_derive/Cargo.toml b/palette_derive/Cargo.toml index e41628208..bccb8fc87 100644 --- a/palette_derive/Cargo.toml +++ b/palette_derive/Cargo.toml @@ -28,6 +28,5 @@ syn = { version = "2.0.13", default-features = false, features = [ ] } quote = "^1.0" proc-macro2 = "^1.0" +by_address = "1.2.1" find-crate = { version = "0.6", optional = true } - -[features] diff --git a/palette_derive/src/alpha/with_alpha.rs b/palette_derive/src/alpha/with_alpha.rs index 116945926..17796b3df 100644 --- a/palette_derive/src/alpha/with_alpha.rs +++ b/palette_derive/src/alpha/with_alpha.rs @@ -22,10 +22,10 @@ pub fn derive(item: TokenStream) -> ::std::result::Result(attrs); - let fields_meta: FieldAttributes = if let syn::Data::Struct(struct_data) = data { - parse_field_attributes(struct_data.fields)? + let (fields_meta, field_errors) = if let syn::Data::Struct(struct_data) = data { + parse_field_attributes::(struct_data.fields) } else { return Err(vec![syn::Error::new( Span::call_site(), @@ -39,7 +39,20 @@ pub fn derive(item: TokenStream) -> ::std::result::Result std::result::Result(attrs); let mut number_of_channels = 0usize; let mut field_type: Option = None; - let (all_fields, fields_meta) = match data { + let (all_fields, fields_meta, field_errors) = match data { Data::Struct(struct_item) => { - let fields_meta: FieldAttributes = - meta::parse_field_attributes(struct_item.fields.clone())?; + let (fields_meta, field_errors) = + meta::parse_field_attributes::(struct_item.fields.clone()); let all_fields = match struct_item.fields { Fields::Named(fields) => fields.named, Fields::Unnamed(fields) => fields.unnamed, Fields::Unit => Default::default(), }; - (all_fields, fields_meta) + (all_fields, fields_meta, field_errors) } Data::Enum(_) => { return Err(vec![syn::Error::new( @@ -120,7 +120,21 @@ pub fn derive(tokens: TokenStream) -> std::result::Result std::result::Result> { diff --git a/palette_derive/src/color_types.rs b/palette_derive/src/color_types.rs new file mode 100644 index 000000000..a33d9f296 --- /dev/null +++ b/palette_derive/src/color_types.rs @@ -0,0 +1,670 @@ +use proc_macro2::{Span, TokenStream}; +use syn::{parse_quote, GenericParam, Generics, Ident, Type}; + +use crate::{ + convert::util::{InputUser, UsedInput, WhitePointSource}, + meta::TypeItemAttributes, + util, +}; + +pub(crate) struct ColorGroup { + pub(crate) root_type: ColorInfo, + pub(crate) colors: &'static [ColorType], +} + +impl ColorGroup { + pub(crate) fn check_availability(&self, name: &str) -> Result<(), ColorError> { + if name == self.root_type.name { + return Ok(()); + } + + for color in self.colors { + if name != color.info.name { + continue; + } + + return Ok(()); + } + + Err(ColorError::UnknownColor) + } + + pub(crate) fn color_names(&'static self) -> ColorNames { + ColorNames { + root_type: Some(&self.root_type), + colors: self.colors.iter(), + } + } + + pub(crate) fn find_type_by_name(&self, name: &str) -> Option<&ColorType> { + self.colors.iter().find(|color| color.info.name == name) + } + + pub(crate) fn find_by_name(&self, name: &str) -> Option<&ColorInfo> { + if self.root_type.name == name { + Some(&self.root_type) + } else { + self.find_type_by_name(name).map(|ty| &ty.info) + } + } +} + +pub(crate) struct ColorType { + pub(crate) info: ColorInfo, + pub(crate) infer_group: bool, + pub(crate) preferred_source: &'static str, +} + +type MetaTypeGeneratorFn = fn( + self_color: &ColorInfo, + meta_type_source: MetaTypeSource, + white_point: &Type, + used_input: &mut UsedInput, + user: InputUser, + meta: &TypeItemAttributes, +) -> syn::Result; + +pub(crate) struct ColorInfo { + pub(crate) name: &'static str, + pub(crate) module: Option<&'static str>, + pub(crate) default_white_point: InternalExternal>, + pub(crate) get_meta_type: Option, +} + +impl ColorInfo { + pub(crate) fn get_path(&self, internal: bool) -> TokenStream { + if let Some(module) = self.module { + util::path([module, self.name], internal) + } else { + util::path([self.name], internal) + } + } + + pub(crate) fn get_type( + &self, + meta_type_source: MetaTypeSource, + component: &Type, + white_point: &Type, + used_input: &mut UsedInput, + user: InputUser, + meta: &TypeItemAttributes, + ) -> syn::Result { + let meta_type: Option = self + .get_meta_type + .map(|get| get(self, meta_type_source, white_point, used_input, user, meta)) + .transpose()?; + + let color_path = self.get_path(meta.internal); + + if let Some(meta_type) = meta_type { + Ok(parse_quote!(#color_path::<#meta_type, #component>)) + } else { + Ok(parse_quote!(#color_path::<#component>)) + } + } + + pub(crate) fn get_default_white_point(&self, internal: bool) -> (Type, WhitePointSource) { + let path = if internal { + self.default_white_point.internal + } else { + self.default_white_point.external + }; + + path.map(|path| { + ( + util::path_type(path, internal), + WhitePointSource::ConcreteType, + ) + }) + .unwrap_or_else(|| (parse_quote!(_Wp), WhitePointSource::GeneratedGeneric)) + } +} + +pub(crate) struct InternalExternal { + pub(crate) internal: T, + pub(crate) external: T, +} + +pub(crate) struct ColorNames { + root_type: Option<&'static ColorInfo>, + colors: std::slice::Iter<'static, ColorType>, +} + +impl Iterator for ColorNames { + type Item = &'static ColorInfo; + + fn next(&mut self) -> Option { + if let Some(root_type) = self.root_type.take() { + return Some(root_type); + } + + self.colors.next().map(|color| &color.info) + } +} + +/// These are the disjoint networks of possible conversions. It's possible to +/// convert directly to and from each color within each group, while converting +/// between the groups requires additional runtime data. +pub(crate) static COLOR_GROUPS: &[&ColorGroup] = &[ + &XYZ_COLORS, + &CAM16_JCH_COLORS, + &CAM16_JMH_COLORS, + &CAM16_JSH_COLORS, + &CAM16_QCH_COLORS, + &CAM16_QMH_COLORS, + &CAM16_QSH_COLORS, +]; + +// The XYZ color group is where most colors should be. All of these have some +// connection to `Xyz`. + +pub(crate) static XYZ_COLORS: ColorGroup = ColorGroup { + root_type: ColorInfo { + name: "Xyz", + module: None, + default_white_point: InternalExternal { + internal: None, + external: Some(&["white_point", "D65"]), + }, + get_meta_type: Some(get_white_point), + }, + colors: &[ + ColorType { + info: ColorInfo { + name: "Rgb", + module: Some("rgb"), + default_white_point: InternalExternal { + internal: None, + external: Some(&["white_point", "D65"]), + }, + get_meta_type: Some(get_rgb_standard), + }, + infer_group: true, + preferred_source: "Xyz", + }, + ColorType { + info: ColorInfo { + name: "Luma", + module: Some("luma"), + default_white_point: InternalExternal { + internal: None, + external: Some(&["white_point", "D65"]), + }, + get_meta_type: Some(get_luma_standard), + }, + infer_group: true, + preferred_source: "Xyz", + }, + ColorType { + info: ColorInfo { + name: "Hsl", + module: None, + default_white_point: InternalExternal { + internal: None, + external: Some(&["white_point", "D65"]), + }, + get_meta_type: Some(get_rgb_standard), + }, + infer_group: true, + preferred_source: "Rgb", + }, + ColorType { + info: ColorInfo { + name: "Hsluv", + module: None, + default_white_point: InternalExternal { + internal: None, + external: Some(&["white_point", "D65"]), + }, + get_meta_type: Some(get_white_point), + }, + infer_group: true, + preferred_source: "Lchuv", + }, + ColorType { + info: ColorInfo { + name: "Hsv", + module: None, + default_white_point: InternalExternal { + internal: None, + external: Some(&["white_point", "D65"]), + }, + get_meta_type: Some(get_rgb_standard), + }, + infer_group: true, + preferred_source: "Rgb", + }, + ColorType { + info: ColorInfo { + name: "Hwb", + module: None, + default_white_point: InternalExternal { + internal: None, + external: Some(&["white_point", "D65"]), + }, + get_meta_type: Some(get_rgb_standard), + }, + infer_group: true, + preferred_source: "Hsv", + }, + ColorType { + info: ColorInfo { + name: "Lab", + module: None, + default_white_point: InternalExternal { + internal: None, + external: Some(&["white_point", "D65"]), + }, + get_meta_type: Some(get_white_point), + }, + infer_group: true, + preferred_source: "Xyz", + }, + ColorType { + info: ColorInfo { + name: "Lch", + module: None, + default_white_point: InternalExternal { + internal: None, + external: Some(&["white_point", "D65"]), + }, + get_meta_type: Some(get_white_point), + }, + infer_group: true, + preferred_source: "Lab", + }, + ColorType { + info: ColorInfo { + name: "Lchuv", + module: None, + default_white_point: InternalExternal { + internal: None, + external: Some(&["white_point", "D65"]), + }, + get_meta_type: Some(get_white_point), + }, + infer_group: true, + preferred_source: "Luv", + }, + ColorType { + info: ColorInfo { + name: "Luv", + module: None, + default_white_point: InternalExternal { + internal: None, + external: Some(&["white_point", "D65"]), + }, + get_meta_type: Some(get_white_point), + }, + infer_group: true, + preferred_source: "Xyz", + }, + ColorType { + info: ColorInfo { + name: "Oklab", + module: None, + default_white_point: InternalExternal { + internal: Some(&["white_point", "D65"]), + external: Some(&["white_point", "D65"]), + }, + get_meta_type: None, + }, + infer_group: true, + preferred_source: "Xyz", + }, + ColorType { + info: ColorInfo { + name: "Oklch", + module: None, + default_white_point: InternalExternal { + internal: Some(&["white_point", "D65"]), + external: Some(&["white_point", "D65"]), + }, + get_meta_type: None, + }, + infer_group: true, + preferred_source: "Oklab", + }, + ColorType { + info: ColorInfo { + name: "Okhsl", + module: None, + default_white_point: InternalExternal { + internal: Some(&["white_point", "D65"]), + external: Some(&["white_point", "D65"]), + }, + get_meta_type: None, + }, + infer_group: true, + preferred_source: "Oklab", + }, + ColorType { + info: ColorInfo { + name: "Okhsv", + module: None, + default_white_point: InternalExternal { + internal: Some(&["white_point", "D65"]), + external: Some(&["white_point", "D65"]), + }, + get_meta_type: None, + }, + infer_group: true, + preferred_source: "Oklab", + }, + ColorType { + info: ColorInfo { + name: "Okhwb", + module: None, + default_white_point: InternalExternal { + internal: Some(&["white_point", "D65"]), + external: Some(&["white_point", "D65"]), + }, + get_meta_type: None, + }, + infer_group: true, + preferred_source: "Okhsv", + }, + ColorType { + info: ColorInfo { + name: "Yxy", + module: None, + default_white_point: InternalExternal { + internal: None, + external: Some(&["white_point", "D65"]), + }, + get_meta_type: Some(get_white_point), + }, + infer_group: true, + preferred_source: "Xyz", + }, + ], +}; + +// The CAM16 groups are a bit special, since they require information about the +// viewing conditions to convert between each other. + +static CAM16_JCH_COLORS: ColorGroup = ColorGroup { + root_type: ColorInfo { + name: "Cam16Jch", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + colors: &[ColorType { + info: ColorInfo { + name: "Cam16", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + infer_group: false, // For generating connections only from `Cam16`, but not to it + preferred_source: "Cam16Jch", + }], +}; + +static CAM16_JMH_COLORS: ColorGroup = ColorGroup { + root_type: ColorInfo { + name: "Cam16Jmh", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + colors: &[ + ColorType { + info: ColorInfo { + name: "Cam16", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + infer_group: false, // For generating connections only from `Cam16`, but not to it + preferred_source: "Cam16Jmh", + }, + // CAM16 UCS + ColorType { + info: ColorInfo { + name: "Cam16UcsJmh", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + infer_group: true, + preferred_source: "Cam16Jmh", + }, + ColorType { + info: ColorInfo { + name: "Cam16UcsJab", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + infer_group: true, + preferred_source: "Cam16UcsJmh", + }, + ], +}; + +static CAM16_JSH_COLORS: ColorGroup = ColorGroup { + root_type: ColorInfo { + name: "Cam16Jsh", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + colors: &[ColorType { + info: ColorInfo { + name: "Cam16", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + infer_group: false, // For generating connections only from `Cam16`, but not to it + preferred_source: "Cam16Jsh", + }], +}; + +static CAM16_QCH_COLORS: ColorGroup = ColorGroup { + root_type: ColorInfo { + name: "Cam16Qch", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + colors: &[ColorType { + info: ColorInfo { + name: "Cam16", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + infer_group: false, // For generating connections only from `Cam16`, but not to it + preferred_source: "Cam16Qch", + }], +}; + +static CAM16_QMH_COLORS: ColorGroup = ColorGroup { + root_type: ColorInfo { + name: "Cam16Qmh", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + colors: &[ColorType { + info: ColorInfo { + name: "Cam16", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + infer_group: false, // For generating connections only from `Cam16`, but not to it + preferred_source: "Cam16Qmh", + }], +}; + +static CAM16_QSH_COLORS: ColorGroup = ColorGroup { + root_type: ColorInfo { + name: "Cam16Qsh", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + colors: &[ColorType { + info: ColorInfo { + name: "Cam16", + module: Some("cam16"), + default_white_point: InternalExternal { + internal: None, + external: None, + }, + get_meta_type: None, + }, + infer_group: false, // For generating connections only from `Cam16`, but not to it + preferred_source: "Cam16Qsh", + }], +}; + +pub(crate) enum ColorError { + UnknownColor, +} + +fn get_rgb_standard( + self_color: &ColorInfo, + meta_type_source: MetaTypeSource, + white_point: &Type, + used_input: &mut UsedInput, + user: InputUser, + meta: &TypeItemAttributes, +) -> syn::Result { + if let Some(rgb_standard) = &meta.rgb_standard { + Ok(rgb_standard.clone()) + } else { + match meta_type_source { + MetaTypeSource::Generics(generics) => { + used_input.white_point.set_used(user); + + let rgb_standard_path = util::path(["rgb", "RgbStandard"], meta.internal); + let rgb_space_path = util::path(["rgb", "RgbSpace"], meta.internal); + + generics.params.push(GenericParam::Type( + Ident::new("_S", Span::call_site()).into(), + )); + let where_clause = generics.make_where_clause(); + + where_clause + .predicates + .push(parse_quote!(_S: #rgb_standard_path)); + where_clause + .predicates + .push(parse_quote!(_S::Space: #rgb_space_path)); + + Ok(parse_quote!(_S)) + } + MetaTypeSource::OtherColor(other_color) => { + match other_color.name { + "Rgb" | "Hsl" | "Hsv" | "Hwb" => Ok(parse_quote!(_S)), + _ => Err(syn::parse::Error::new( + Span::call_site(), + format!( + "could not determine which RGB standard to use when converting to and from `{}` via `{}`", + other_color.name, + self_color.name + ), + )), + } + } + } + } +} + +fn get_luma_standard( + _self_color: &ColorInfo, + meta_type_source: MetaTypeSource, + white_point: &Type, + used_input: &mut UsedInput, + user: InputUser, + meta: &TypeItemAttributes, +) -> syn::Result { + if let Some(luma_standard) = meta.luma_standard.as_ref() { + return Ok(luma_standard.clone()); + } + + used_input.white_point.set_used(user); + + match meta_type_source { + MetaTypeSource::Generics(generics) => { + let luma_standard_path = util::path(["luma", "LumaStandard"], meta.internal); + + generics.params.push(GenericParam::Type( + Ident::new("_S", Span::call_site()).into(), + )); + + generics + .make_where_clause() + .predicates + .push(parse_quote!(_S: #luma_standard_path)); + + Ok(parse_quote!(_S)) + } + MetaTypeSource::OtherColor(_) => { + let linear_path = util::path(["encoding", "Linear"], meta.internal); + + Ok(parse_quote!(#linear_path<#white_point>)) + } + } +} + +fn get_white_point( + _self_color: &ColorInfo, + _meta_type_source: MetaTypeSource, + white_point: &Type, + used_input: &mut UsedInput, + user: InputUser, + _meta: &TypeItemAttributes, +) -> syn::Result { + used_input.white_point.set_used(user); + Ok(white_point.clone()) +} + +pub(crate) enum MetaTypeSource<'a> { + OtherColor(&'a ColorInfo), + Generics(&'a mut Generics), +} diff --git a/palette_derive/src/convert/from_color_unclamped.rs b/palette_derive/src/convert/from_color_unclamped.rs index f5ff2eb53..bbf42e484 100644 --- a/palette_derive/src/convert/from_color_unclamped.rs +++ b/palette_derive/src/convert/from_color_unclamped.rs @@ -1,18 +1,17 @@ -use std::collections::HashSet; - use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::quote; use syn::{parse_quote, DeriveInput, Generics, Ident, Result, Type}; -use crate::convert::util::WhitePointSource; -use crate::meta::{ - parse_field_attributes, parse_namespaced_attributes, FieldAttributes, IdentOrIndex, - TypeItemAttributes, +use crate::{ + color_types::{ColorInfo, MetaTypeSource, XYZ_COLORS}, + convert::util::{InputUser, WhitePointSource}, + meta::{ + parse_field_attributes, parse_namespaced_attributes, FieldAttributes, IdentOrIndex, + TypeItemAttributes, + }, + util, }; -use crate::util; - -use crate::COLOR_TYPES; use super::util::{component_type, find_nearest_color, get_convert_color_type, white_point_type}; @@ -25,10 +24,10 @@ pub fn derive(item: TokenStream) -> ::std::result::Result(attrs); - let fields_meta: FieldAttributes = if let syn::Data::Struct(struct_data) = data { - parse_field_attributes(struct_data.fields)? + let (fields_meta, field_errors) = if let syn::Data::Struct(struct_data) = data { + parse_field_attributes::(struct_data.fields) } else { return Err(vec![syn::Error::new( Span::call_site(), @@ -37,7 +36,7 @@ pub fn derive(item: TokenStream) -> ::std::result::Result ::std::result::Result ::std::result::Result, component: &Type, - white_point: &Type, + white_point: Option<(Type, WhitePointSource)>, meta: &TypeItemAttributes, generics: &Generics, - white_point_source: Option, -) -> Result> { - let included_colors = COLOR_TYPES.iter().filter(|&&color| !skip.contains(color)); - let linear_path = util::path(["encoding", "Linear"], meta.internal); +) -> (Vec, Vec) { + let included_colors = meta + .color_groups + .iter() + .flat_map(|group| group.color_names()) + .filter(|&color| !meta.skip_derives.contains(color.name)); let mut parameters = Vec::new(); + let mut errors = Vec::new(); - for &color_name in included_colors { - let nearest_color_name = find_nearest_color(color_name, skip)?; - - let mut generics = generics.clone(); - - let (color_ty, mut used_input) = get_convert_color_type( - color_name, - white_point, + for color in included_colors { + let impl_params = prepare_from_impl_for_pair( + color, component, - meta.rgb_standard.as_ref(), - meta.luma_standard.as_ref(), - &mut generics, - meta.internal, + white_point.clone(), + meta, + generics.clone(), ); - let nearest_color_path = util::color_path(nearest_color_name, meta.internal); - let target_color_rgb_standard = match color_name { - "Rgb" | "Hsl" | "Hsv" | "Hwb" => Some(parse_quote!(_S)), - _ => None, - }; - - let nearest_color_ty: Type = match nearest_color_name { - "Rgb" | "Hsl" | "Hsv" | "Hwb" => { - let rgb_standard = meta.rgb_standard - .clone() - .or(target_color_rgb_standard) - .ok_or_else(|| { - syn::parse::Error::new( - Span::call_site(), - format!( - "could not determine which RGB standard to use when converting to and from `{}` via `{}`", - color_name, - nearest_color_name - ), - ) - })?; - - parse_quote!(#nearest_color_path::<#rgb_standard, #component>) - } - "Luma" => { - if let Some(luma_standard) = meta.luma_standard.as_ref() { - parse_quote!(#nearest_color_path::<#luma_standard, #component>) - } else { - used_input.white_point = true; - parse_quote!(#nearest_color_path::<#linear_path<#white_point>, #component>) - } + match impl_params { + Ok(Some(impl_params)) => parameters.push(impl_params), + Ok(None) => {} + Err(error) => errors.push(error), + } + } + + (parameters, errors) +} + +fn prepare_from_impl_for_pair( + color: &ColorInfo, + component: &Type, + white_point: Option<(Type, WhitePointSource)>, + meta: &TypeItemAttributes, + mut generics: Generics, +) -> Result> { + let nearest_color = find_nearest_color(color, meta)?; + + // Figures out which white point the target type prefers, unless it's specified in `white_point`. + let (white_point, white_point_source) = if let Some((white_point, source)) = white_point { + (white_point, source) + } else { + color.get_default_white_point(meta.internal) + }; + + let (color_ty, mut used_input) = + get_convert_color_type(color, &white_point, component, meta, &mut generics)?; + + let nearest_color_ty = nearest_color.get_type( + MetaTypeSource::OtherColor(color), + component, + &white_point, + &mut used_input, + InputUser::Nearest, + meta, + )?; + + // Skip implementing the trait where it wouldn't be able to constrain the + // white point. This is only happening when certain optional features are + // enabled. + if used_input.white_point.is_unconstrained() + && matches!(white_point_source, WhitePointSource::GeneratedGeneric) + { + return Ok(None); + } + + if used_input.white_point.is_used() { + match white_point_source { + WhitePointSource::WhitePoint => { + let white_point_path = util::path(["white_point", "WhitePoint"], meta.internal); + generics + .make_where_clause() + .predicates + .push(parse_quote!(#white_point: #white_point_path<#component>)) } - "Oklab" | "Oklch" | "Okhsv" | "Okhsl" | "Okhwb" => { - parse_quote!(#nearest_color_path::<#component>) + WhitePointSource::RgbStandard => { + let rgb_standard_path = util::path(["rgb", "RgbStandard"], meta.internal); + let rgb_standard = meta.rgb_standard.as_ref(); + generics + .make_where_clause() + .predicates + .push(parse_quote!(#rgb_standard: #rgb_standard_path)); } - _ => { - used_input.white_point = true; - parse_quote!(#nearest_color_path::<#white_point, #component>) + WhitePointSource::LumaStandard => { + let luma_standard_path = util::path(["luma", "LumaStandard"], meta.internal); + let luma_standard = meta.luma_standard.as_ref(); + generics + .make_where_clause() + .predicates + .push(parse_quote!(#luma_standard: #luma_standard_path)); } - }; - - if used_input.white_point { - match white_point_source { - Some(WhitePointSource::WhitePoint) => { - let white_point_path = util::path(["white_point", "WhitePoint"], meta.internal); - generics - .make_where_clause() - .predicates - .push(parse_quote!(#white_point: #white_point_path<#component>)) - } - Some(WhitePointSource::RgbStandard) => { - let rgb_standard_path = util::path(["rgb", "RgbStandard"], meta.internal); - let rgb_standard = meta.rgb_standard.as_ref(); - generics - .make_where_clause() - .predicates - .push(parse_quote!(#rgb_standard: #rgb_standard_path)); - } - Some(WhitePointSource::LumaStandard) => { - let luma_standard_path = util::path(["luma", "LumaStandard"], meta.internal); - let luma_standard = meta.luma_standard.as_ref(); - generics - .make_where_clause() - .predicates - .push(parse_quote!(#luma_standard: #luma_standard_path)); - } - None => {} + WhitePointSource::ConcreteType => {} + WhitePointSource::GeneratedGeneric => { + generics.params.push(parse_quote!(_Wp)); } } - - parameters.push(FromImplParameters { - generics, - color_ty, - nearest_color_ty, - }); } - Ok(parameters) + Ok(Some(FromImplParameters { + generics, + color_ty, + nearest_color_ty, + })) } struct FromImplParameters { diff --git a/palette_derive/src/convert/mod.rs b/palette_derive/src/convert/mod.rs index de6c01cb4..0b88a7e56 100644 --- a/palette_derive/src/convert/mod.rs +++ b/palette_derive/src/convert/mod.rs @@ -1,4 +1,4 @@ pub use self::from_color_unclamped::derive as derive_from_color_unclamped; mod from_color_unclamped; -mod util; +pub mod util; diff --git a/palette_derive/src/convert/util.rs b/palette_derive/src/convert/util.rs index 4422e27bc..bd98b5dd0 100644 --- a/palette_derive/src/convert/util.rs +++ b/palette_derive/src/convert/util.rs @@ -1,27 +1,29 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use proc_macro2::Span; -use syn::spanned::Spanned; -use syn::{parse_quote, GenericParam, Generics, Ident, Result, Type}; +use syn::{parse_quote, Generics, Result, Type}; -use crate::util; -use crate::{COLOR_TYPES, PREFERRED_CONVERSION_SOURCE}; +use crate::{ + color_types::{ColorInfo, MetaTypeSource}, + meta::TypeItemAttributes, + util, +}; pub fn white_point_type( white_point: Option<&Type>, rgb_standard: Option<&Type>, luma_standard: Option<&Type>, internal: bool, -) -> (Type, Option) { +) -> Option<(Type, WhitePointSource)> { white_point - .map(|white_point| (white_point.clone(), Some(WhitePointSource::WhitePoint))) + .map(|white_point| (white_point.clone(), WhitePointSource::WhitePoint)) .or_else(|| { rgb_standard.map(|rgb_standard| { let rgb_standard_path = util::path(["rgb", "RgbStandard"], internal); let rgb_space_path = util::path(["rgb", "RgbSpace"], internal); ( parse_quote!(<<#rgb_standard as #rgb_standard_path>::Space as #rgb_space_path>::WhitePoint), - Some(WhitePointSource::RgbStandard), + WhitePointSource::RgbStandard, ) }) }) @@ -30,116 +32,49 @@ pub fn white_point_type( let luma_standard_path = util::path(["luma", "LumaStandard"], internal); ( parse_quote!(<#luma_standard as #luma_standard_path>::WhitePoint), - Some(WhitePointSource::LumaStandard), + WhitePointSource::LumaStandard, ) }) }) - .unwrap_or_else(|| { - ( - util::path_type(&["white_point", "D65"], internal), - None, - ) - }) } pub fn component_type(component: Option) -> Type { component.unwrap_or_else(|| parse_quote!(f32)) } -pub fn get_convert_color_type( - color: &str, +pub(crate) fn get_convert_color_type( + color: &ColorInfo, white_point: &Type, component: &Type, - rgb_standard: Option<&Type>, - luma_standard: Option<&Type>, + meta: &TypeItemAttributes, generics: &mut Generics, - internal: bool, -) -> (Type, UsedInput) { - let color_path = util::color_path(color, internal); - - match color { - "Luma" => { - let luma_standard_path = util::path(["luma", "LumaStandard"], internal); - - if let Some(luma_standard) = luma_standard { - ( - parse_quote!(#color_path<#luma_standard, #component>), - UsedInput::default(), - ) - } else { - generics.params.push(GenericParam::Type( - Ident::new("_S", Span::call_site()).into(), - )); - - generics - .make_where_clause() - .predicates - .push(parse_quote!(_S: #luma_standard_path)); - ( - parse_quote!(#color_path<_S, #component>), - UsedInput { white_point: true }, - ) - } - } - "Rgb" | "Hsl" | "Hsv" | "Hwb" => { - let rgb_standard_path = util::path(["rgb", "RgbStandard"], internal); - let rgb_space_path = util::path(["rgb", "RgbSpace"], internal); - - if let Some(rgb_standard) = rgb_standard { - ( - parse_quote!(#color_path<#rgb_standard, #component>), - UsedInput::default(), - ) - } else { - generics.params.push(GenericParam::Type( - Ident::new("_S", Span::call_site()).into(), - )); - let where_clause = generics.make_where_clause(); - - where_clause - .predicates - .push(parse_quote!(_S: #rgb_standard_path)); - where_clause - .predicates - .push(parse_quote!(_S::Space: #rgb_space_path)); - - ( - parse_quote!(#color_path<_S, #component>), - UsedInput { white_point: true }, - ) - } - } - "Oklab" | "Oklch" | "Okhsv" | "Okhsl" | "Okhwb" => { - (parse_quote!(#color_path<#component>), UsedInput::default()) - } - _ => ( - parse_quote!(#color_path<#white_point, #component>), - UsedInput { white_point: true }, - ), - } +) -> syn::Result<(Type, UsedInput)> { + let mut used_input = UsedInput::default(); + let color_type = color.get_type( + MetaTypeSource::Generics(generics), + component, + white_point, + &mut used_input, + InputUser::Target, + meta, + )?; + + Ok((color_type, used_input)) } -pub fn find_nearest_color<'a>(color: &'a str, skip: &HashSet) -> Result<&'a str> { +pub(crate) fn find_nearest_color<'a>( + color: &'a ColorInfo, + meta: &TypeItemAttributes, +) -> Result<&'a ColorInfo> { let mut stack = vec![(color, 0)]; let mut found = None; let mut visited = HashMap::new(); // Make sure there is at least one valid color in the skip list - assert!(!skip.is_empty()); - for skipped_color in skip { - if !COLOR_TYPES - .iter() - .any(|valid_color| skipped_color == valid_color) - { - return Err(::syn::parse::Error::new( - color.span(), - format!("`{}` is not a valid color type", skipped_color), - )); - } - } + assert!(!meta.skip_derives.is_empty()); while let Some((color, distance)) = stack.pop() { - if skip.contains(color) { + if meta.skip_derives.contains(color.name) { if let Some((_, found_distance)) = found { if distance < found_distance { found = Some((color, distance)); @@ -151,25 +86,32 @@ pub fn find_nearest_color<'a>(color: &'a str, skip: &HashSet) -> Result< } } - if let Some(&previous_distance) = visited.get(color) { + if let Some(&previous_distance) = visited.get(color.name) { if previous_distance <= distance { continue; } } - visited.insert(color, distance); + visited.insert(color.name, distance); // Start by pushing the plan B routes... - for &(destination, source) in PREFERRED_CONVERSION_SOURCE { - if color == source { - stack.push((destination, distance + 1)); + for group in &meta.color_groups { + for candidate in group.colors { + if color.name == candidate.preferred_source { + stack.push((&candidate.info, distance + 1)); + } } } // ...then push the preferred routes. They will be popped first. - for &(destination, source) in PREFERRED_CONVERSION_SOURCE { - if color == destination { - stack.push((source, distance + 1)); + for group in &meta.color_groups { + for candidate in group.colors { + if color.name == candidate.info.name { + let preferred = group + .find_by_name(candidate.preferred_source) + .expect("preferred sources have to exist in the group"); + stack.push((preferred, distance + 1)); + } } } } @@ -178,10 +120,10 @@ pub fn find_nearest_color<'a>(color: &'a str, skip: &HashSet) -> Result< Ok(color) } else { Err(::syn::parse::Error::new( - color.span(), + Span::call_site(), format!( "none of the skipped colors can be used for converting from {}", - color + color.name ), )) } @@ -192,9 +134,40 @@ pub enum WhitePointSource { WhitePoint, RgbStandard, LumaStandard, + ConcreteType, + GeneratedGeneric, } #[derive(Debug, Default)] pub struct UsedInput { - pub white_point: bool, + pub white_point: InputUsage, +} + +#[derive(Debug, Default)] +pub struct InputUsage { + used_by_target: bool, + used_by_nearest: bool, +} + +impl InputUsage { + pub(crate) fn set_used(&mut self, user: InputUser) { + match user { + InputUser::Target => self.used_by_target = true, + InputUser::Nearest => self.used_by_nearest = true, + } + } + + pub(crate) fn is_used(&self) -> bool { + self.used_by_target || self.used_by_nearest + } + + pub(crate) fn is_unconstrained(&self) -> bool { + !self.used_by_target && self.used_by_nearest + } +} + +#[derive(Clone, Copy)] +pub enum InputUser { + Target, + Nearest, } diff --git a/palette_derive/src/lib.rs b/palette_derive/src/lib.rs index fdcb1d42a..0a901796c 100644 --- a/palette_derive/src/lib.rs +++ b/palette_derive/src/lib.rs @@ -33,34 +33,11 @@ macro_rules! syn_try { mod alpha; mod cast; +mod color_types; mod convert; mod meta; mod util; -const COLOR_TYPES: &[&str] = &[ - "Rgb", "Luma", "Hsl", "Hsluv", "Hsv", "Hwb", "Lab", "Lch", "Lchuv", "Luv", "Oklab", "Oklch", - "Okhwb", "Okhsl", "Okhsv", "Xyz", "Yxy", -]; - -const PREFERRED_CONVERSION_SOURCE: &[(&str, &str)] = &[ - ("Rgb", "Xyz"), - ("Luma", "Xyz"), - ("Hsl", "Rgb"), - ("Hsluv", "Lchuv"), - ("Hsv", "Rgb"), - ("Hwb", "Hsv"), - ("Lab", "Xyz"), - ("Lch", "Lab"), - ("Lchuv", "Luv"), - ("Luv", "Xyz"), - ("Oklab", "Xyz"), - ("Oklch", "Oklab"), - ("Okhsl", "Oklab"), - ("Okhsv", "Oklab"), - ("Okhwb", "Okhsv"), - ("Yxy", "Xyz"), -]; - #[proc_macro_derive(WithAlpha, attributes(palette))] pub fn derive_with_alpha(tokens: TokenStream) -> TokenStream { syn_try!(alpha::derive_with_alpha(tokens)) diff --git a/palette_derive/src/meta/field_attributes.rs b/palette_derive/src/meta/field_attributes.rs index 42f2bd59b..294619499 100644 --- a/palette_derive/src/meta/field_attributes.rs +++ b/palette_derive/src/meta/field_attributes.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet}; use syn::{spanned::Spanned, Expr, ExprLit}; -use syn::{Lit, Meta, MetaNameValue, Result, Type}; +use syn::{Lit, Meta, MetaNameValue, Type}; use super::{assert_path_meta, FieldAttributeArgumentParser, IdentOrIndex}; @@ -13,12 +13,17 @@ pub struct FieldAttributes { } impl FieldAttributeArgumentParser for FieldAttributes { - fn argument(&mut self, field_name: &IdentOrIndex, ty: &Type, argument: Meta) -> Result<()> { + fn argument( + &mut self, + field_name: &IdentOrIndex, + ty: &Type, + argument: Meta, + ) -> Result<(), Vec> { let argument_name = argument.path().get_ident().map(ToString::to_string); match argument_name.as_deref() { Some("alpha") => { - assert_path_meta(&argument)?; + assert_path_meta(&argument).map_err(|error| vec![error])?; self.alpha_property = Some((field_name.clone(), ty.clone())); } Some("unsafe_same_layout_as") => { @@ -31,25 +36,25 @@ impl FieldAttributeArgumentParser for FieldAttributes { .. }) = argument { - string.parse()? + string.parse().map_err(|error| vec![error])? } else { - return Err(::syn::parse::Error::new( + return Err(vec![::syn::parse::Error::new( argument.span(), "expected `unsafe_same_layout_as = \"SomeType\"`", - )); + )]); }; self.type_substitutes.insert(field_name.clone(), substitute); } Some("unsafe_zero_sized") => { - assert_path_meta(&argument)?; + assert_path_meta(&argument).map_err(|error| vec![error])?; self.zero_size_fields.insert(field_name.clone()); } _ => { - return Err(::syn::parse::Error::new( + return Err(vec![::syn::parse::Error::new( argument.span(), "unknown field attribute", - )); + )]); } } diff --git a/palette_derive/src/meta/mod.rs b/palette_derive/src/meta/mod.rs index 9f0fcc11c..2b0ce1257 100644 --- a/palette_derive/src/meta/mod.rs +++ b/palette_derive/src/meta/mod.rs @@ -15,7 +15,7 @@ mod type_item_attributes; pub fn parse_namespaced_attributes( attributes: Vec, -) -> ::std::result::Result> { +) -> (T, Vec<::syn::parse::Error>) { let mut result = T::default(); let mut errors = Vec::new(); @@ -53,8 +53,8 @@ pub fn parse_namespaced_attributes( match parse_result { Ok(meta) => { for argument in meta { - if let Err(error) = result.argument(argument) { - errors.push(error); + if let Err(new_error) = result.argument(argument) { + errors.extend(new_error); } } } @@ -62,16 +62,12 @@ pub fn parse_namespaced_attributes( } } - if errors.is_empty() { - Ok(result) - } else { - Err(errors) - } + (result, errors) } pub fn parse_field_attributes( fields: Fields, -) -> ::std::result::Result> { +) -> (T, Vec<::syn::parse::Error>) { let mut result = T::default(); let mut errors = Vec::new(); @@ -121,8 +117,8 @@ pub fn parse_field_attributes( match parse_result { Ok(meta) => { for argument in meta { - if let Err(error) = result.argument(&field_name, &ty, argument) { - errors.push(error); + if let Err(new_errors) = result.argument(&field_name, &ty, argument) { + errors.extend(new_errors); } } } @@ -130,11 +126,7 @@ pub fn parse_field_attributes( } } - if errors.is_empty() { - Ok(result) - } else { - Err(errors) - } + (result, errors) } pub fn assert_path_meta(meta: &Meta) -> Result<()> { @@ -208,9 +200,14 @@ impl ::quote::ToTokens for IdentOrIndex { } pub trait AttributeArgumentParser: Default { - fn argument(&mut self, argument: Meta) -> Result<()>; + fn argument(&mut self, argument: Meta) -> std::result::Result<(), Vec>; } pub trait FieldAttributeArgumentParser: Default { - fn argument(&mut self, field_name: &IdentOrIndex, ty: &Type, argument: Meta) -> Result<()>; + fn argument( + &mut self, + field_name: &IdentOrIndex, + ty: &Type, + argument: Meta, + ) -> std::result::Result<(), Vec>; } diff --git a/palette_derive/src/meta/type_item_attributes.rs b/palette_derive/src/meta/type_item_attributes.rs index 5a5137347..ef3c9a030 100644 --- a/palette_derive/src/meta/type_item_attributes.rs +++ b/palette_derive/src/meta/type_item_attributes.rs @@ -1,8 +1,11 @@ use std::collections::HashSet; +use by_address::ByAddress; use quote::quote; use syn::{punctuated::Punctuated, spanned::Spanned, token::Comma, Expr, ExprLit}; -use syn::{Ident, Lit, Meta, MetaNameValue, Result, Type}; +use syn::{Ident, Lit, Meta, MetaNameValue, Type}; + +use crate::color_types::{ColorGroup, COLOR_GROUPS}; use super::AttributeArgumentParser; @@ -15,163 +18,132 @@ pub struct TypeItemAttributes { pub white_point: Option, pub rgb_standard: Option, pub luma_standard: Option, + pub(crate) color_groups: HashSet>, } impl AttributeArgumentParser for TypeItemAttributes { - fn argument(&mut self, argument: Meta) -> Result<()> { + fn argument(&mut self, argument: Meta) -> Result<(), Vec> { let argument_name = argument.path().get_ident().map(ToString::to_string); match argument_name.as_deref() { Some("skip_derives") => { if let Meta::List(list) = argument { - let skipped = - list.parse_args_with(Punctuated::::parse_terminated)?; + let skipped = list + .parse_args_with(Punctuated::::parse_terminated) + .map_err(|error| vec![error])?; + + let mut errors = Vec::new(); + for skipped_color in skipped { + let color_name = skipped_color.to_string(); + self.skip_derives.insert(color_name.clone()); + + let color_group = COLOR_GROUPS + .iter() + .find(|group| group.check_availability(&color_name).is_ok()); + + let group = if let Some(&group) = color_group { + group + } else { + errors.push(syn::Error::new( + skipped_color.span(), + format!("`{}` is not a valid color type", skipped_color), + )); + continue; + }; + + let infer_group = group + .find_type_by_name(&color_name) + .map_or(true, |ty| ty.infer_group); - self.skip_derives - .extend(skipped.into_iter().map(|ident| ident.to_string())); + if infer_group { + self.color_groups.insert(group.into()); + } + } + + if !errors.is_empty() { + return Err(errors); + } } else { - return Err(::syn::parse::Error::new( + return Err(vec![syn::Error::new( argument.span(), "expected `skip_derives` to have a list of color type names, like `skip_derives(Xyz, Luma, Rgb)`", - )); + )]); } } Some("component") => { - if self.component.is_none() { - let result = if let Meta::NameValue(MetaNameValue { - value: - Expr::Lit(ExprLit { - lit: Lit::Str(ty), .. - }), - .. - }) = argument - { - self.component = Some(ty.parse()?); - Ok(()) - } else { - Err(argument.span()) - }; - - if let Err(span) = result { - let message = "expected `component` to be a type or type parameter in a string, like `component = \"T\"`"; - return Err(::syn::parse::Error::new(span, message)); - } - } else { - return Err(::syn::parse::Error::new( - argument.span(), - "`component` appears more than once", - )); - } + get_meta_type_argument(argument, &mut self.component)?; } Some("white_point") => { - if self.white_point.is_none() { - let result = if let Meta::NameValue(MetaNameValue { - value: - Expr::Lit(ExprLit { - lit: Lit::Str(ty), .. - }), - .. - }) = argument - { - self.white_point = Some(ty.parse()?); - Ok(()) - } else { - Err(argument.span()) - }; - - if let Err(span) = result { - let message = "expected `white_point` to be a type or type parameter in a string, like `white_point = \"T\"`"; - return Err(::syn::parse::Error::new(span, message)); - } - } else { - return Err(::syn::parse::Error::new( - argument.span(), - "`white_point` appears more than once", - )); - } + get_meta_type_argument(argument, &mut self.white_point)?; } Some("rgb_standard") => { - if self.rgb_standard.is_none() { - let result = if let Meta::NameValue(MetaNameValue { - value: - Expr::Lit(ExprLit { - lit: Lit::Str(ty), .. - }), - .. - }) = argument - { - self.rgb_standard = Some(ty.parse()?); - Ok(()) - } else { - Err(argument.span()) - }; - - if let Err(span) = result { - let message = "expected `rgb_standard` to be a type or type parameter in a string, like `rgb_standard = \"T\"`"; - return Err(::syn::parse::Error::new(span, message)); - } - } else { - return Err(::syn::parse::Error::new( - argument.span(), - "`rgb_standard` appears more than once", - )); - } + get_meta_type_argument(argument, &mut self.rgb_standard)?; } Some("luma_standard") => { - if self.luma_standard.is_none() { - let result = if let Meta::NameValue(MetaNameValue { - value: - Expr::Lit(ExprLit { - lit: Lit::Str(ty), .. - }), - .. - }) = argument - { - self.luma_standard = Some(ty.parse()?); - Ok(()) - } else { - Err(argument.span()) - }; - - if let Err(span) = result { - let message = "expected `luma_standard` to be a type or type parameter in a string, like `luma_standard = \"T\"`"; - return Err(::syn::parse::Error::new(span, message)); - } - } else { - return Err(::syn::parse::Error::new( - argument.span(), - "`luma_standard` appears more than once", - )); - } + get_meta_type_argument(argument, &mut self.luma_standard)?; } Some("palette_internal") => { if let Meta::Path(_) = argument { self.internal = true; } else { - return Err(::syn::parse::Error::new( + return Err(vec![syn::Error::new( argument.span(), "expected `palette_internal` to a literal without value", - )); + )]); } } Some("palette_internal_not_base_type") => { if let Meta::Path(_) = argument { self.internal_not_base_type = true; } else { - return Err(::syn::parse::Error::new( + return Err(vec![syn::Error::new( argument.span(), "expected `palette_internal` to a literal without value", - )); + )]); } } _ => { - return Err(::syn::parse::Error::new( + return Err(vec![syn::Error::new( argument.span(), format!("`{}` is not a known type item attribute", quote!(#argument)), - )); + )]); } } Ok(()) } } + +fn get_meta_type_argument( + argument: Meta, + attribute: &mut Option, +) -> Result<(), Vec> { + if attribute.is_none() { + let result = if let Meta::NameValue(MetaNameValue { + value: Expr::Lit(ExprLit { + lit: Lit::Str(ty), .. + }), + .. + }) = argument + { + *attribute = Some(ty.parse().map_err(|error| vec![error])?); + Ok(()) + } else { + Err((argument.span(), argument.path())) + }; + + if let Err((span, path)) = result { + let name = path.get_ident().unwrap(); + let message = format!("expected `{name}` to be a type or type parameter in a string, like `{name} = \"T\"`"); + Err(vec![syn::Error::new(span, message)]) + } else { + Ok(()) + } + } else { + let name = argument.path().get_ident().unwrap(); + Err(vec![syn::Error::new( + argument.span(), + format!("`{name}` appears more than once"), + )]) + } +} diff --git a/palette_derive/src/util.rs b/palette_derive/src/util.rs index 60ea8f62e..755b8701b 100644 --- a/palette_derive/src/util.rs +++ b/palette_derive/src/util.rs @@ -29,14 +29,6 @@ pub fn path_type(path: &[&str], internal: bool) -> Type { } } -pub fn color_path(color: &str, internal: bool) -> TokenStream { - match color { - "Luma" => path(["luma", "Luma"], internal), - "Rgb" => path(["rgb", "Rgb"], internal), - _ => path([color], internal), - } -} - #[cfg(feature = "find-crate")] fn find_crate_name() -> Ident { use find_crate::Error;