From b907852b26f8268ffd2e2fb45157ee8efa33e9cb Mon Sep 17 00:00:00 2001 From: Erik Hedvall Date: Wed, 31 Jul 2024 16:40:59 +0200 Subject: [PATCH] Add conversion matrix and `Convert{Once}` traits --- benchmarks/benches/cie.rs | 23 ++- benchmarks/benches/matrix.rs | 27 +-- benchmarks/benches/rgb.rs | 46 +++++- palette/src/cam16/full.rs | 88 ++-------- palette/src/cam16/parameters.rs | 119 ++++++++++++- palette/src/chromatic_adaptation.rs | 18 +- palette/src/convert.rs | 165 ++++++++++++++++-- palette/src/convert/matrix3.rs | 248 ++++++++++++++++++++++++++++ palette/src/lms/lms.rs | 27 ++- palette/src/matrix.rs | 112 +++---------- palette/src/oklab.rs | 13 +- palette/src/rgb/rgb.rs | 41 ++++- palette/src/xyz.rs | 128 +++++++------- 13 files changed, 746 insertions(+), 309 deletions(-) create mode 100644 palette/src/convert/matrix3.rs diff --git a/benchmarks/benches/cie.rs b/benchmarks/benches/cie.rs index 2292bbad0..48ca35d24 100644 --- a/benchmarks/benches/cie.rs +++ b/benchmarks/benches/cie.rs @@ -3,7 +3,7 @@ use std::path::Path; use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Criterion}; use palette::{ color_difference::{Ciede2000, DeltaE, ImprovedDeltaE}, - convert::FromColorUnclamped, + convert::{Convert, FromColorUnclamped}, }; use palette::{Lab, Lch, Xyz, Yxy}; @@ -24,7 +24,9 @@ use data_color_mine::{load_data, ColorMine}; fn cie_conversion(c: &mut Criterion) { let mut group = c.benchmark_group("Cie family"); - let mut colormine: Vec> = load_data(Some(Path::new("../integration_tests/tests/convert/data_color_mine.csv"))); + let mut colormine: Vec> = load_data(Some(Path::new( + "../integration_tests/tests/convert/data_color_mine.csv", + ))); colormine.truncate(colormine.len() - colormine.len() % 8); assert_eq!( colormine.len() % 8, @@ -57,6 +59,8 @@ fn cie_conversion(c: &mut Criterion) { .map(|x| Lch::from_color_unclamped(x.xyz)) .collect(); + let rgb_to_xyz_matrix = Xyz::matrix_from_rgb(); + group.bench_with_input("xyz to lab", &colormine, |b, colormine| { b.iter(|| { for c in colormine { @@ -96,6 +100,17 @@ fn cie_conversion(c: &mut Criterion) { }) }, ); + group.bench_with_input( + "linsrgb to xyz - Matrix3", + &(&colormine, rgb_to_xyz_matrix), + |b, &(colormine, matrix)| { + b.iter(|| { + for c in colormine { + black_box(matrix.convert(c.linear_rgb)); + } + }) + }, + ); group.bench_with_input("yxy to xyz", &colormine, |b, colormine| { b.iter(|| { for c in colormine { @@ -123,7 +138,9 @@ fn cie_conversion(c: &mut Criterion) { fn cie_delta_e(c: &mut Criterion) { let mut group = c.benchmark_group("Cie delta E"); - let colormine: Vec> = load_data(Some(Path::new("../integration_tests/tests/convert/data_color_mine.csv"))); + let colormine: Vec> = load_data(Some(Path::new( + "../integration_tests/tests/convert/data_color_mine.csv", + ))); let lab: Vec = colormine .iter() diff --git a/benchmarks/benches/matrix.rs b/benchmarks/benches/matrix.rs index 587869ffe..0ba98b475 100644 --- a/benchmarks/benches/matrix.rs +++ b/benchmarks/benches/matrix.rs @@ -1,36 +1,17 @@ use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Criterion}; use palette::encoding; -use palette::matrix::{ - matrix_inverse, multiply_3x3, multiply_rgb_to_xyz, multiply_xyz, multiply_xyz_to_rgb, - rgb_to_xyz_matrix, -}; -use palette::white_point::{WhitePoint, D65}; -use palette::{LinSrgb, Xyz}; +use palette::matrix::{matrix_inverse, multiply_3x3, rgb_to_xyz_matrix}; fn matrix(c: &mut Criterion) { let mut group = c.benchmark_group("Matrix functions"); - let inp1 = [0.1, 0.2, 0.3, 0.3, 0.2, 0.1, 0.2, 0.1, 0.3]; - let inp2 = Xyz::new(0.4, 0.6, 0.8); - let inp3 = [1.0f32, 2.0, 3.0, 3.0, 2.0, 1.0, 2.0, 1.0, 3.0]; - let inp4 = [4.0, 5.0, 6.0, 6.0, 5.0, 4.0, 4.0, 6.0, 5.0]; + let inp1 = [1.0f32, 2.0, 3.0, 3.0, 2.0, 1.0, 2.0, 1.0, 3.0]; + let inp2 = [4.0, 5.0, 6.0, 6.0, 5.0, 4.0, 4.0, 6.0, 5.0]; let inverse: [f32; 9] = [3.0, 0.0, 2.0, 2.0, 0.0, -2.0, 0.0, 1.0, 1.0]; - let color = LinSrgb::new(0.2f32, 0.8, 0.4); - let mat3 = rgb_to_xyz_matrix::(); - let wp: Xyz = D65::get_xyz().with_white_point(); - group.bench_function("multiply_xyz", |b| { - b.iter(|| multiply_xyz::<_>(black_box(inp1), black_box(inp2))) - }); - group.bench_function("multiply_xyz_to_rgb", |b| { - b.iter(|| multiply_xyz_to_rgb::(black_box(inp1), black_box(wp))) - }); - group.bench_function("multiply_rgb_to_xyz", |b| { - b.iter(|| multiply_rgb_to_xyz(black_box(mat3), black_box(color))) - }); group.bench_function("multiply_3x3", |b| { - b.iter(|| multiply_3x3(black_box(inp3), black_box(inp4))) + b.iter(|| multiply_3x3(black_box(inp1), black_box(inp2))) }); group.bench_with_input("matrix_inverse", &inverse, |b, inverse| { b.iter(|| matrix_inverse(*inverse)) diff --git a/benchmarks/benches/rgb.rs b/benchmarks/benches/rgb.rs index e335834a5..852b7168a 100644 --- a/benchmarks/benches/rgb.rs +++ b/benchmarks/benches/rgb.rs @@ -1,8 +1,12 @@ use std::path::Path; use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Criterion}; -use palette::convert::FromColorUnclamped; -use palette::encoding; +use palette::{ + convert::{Convert, FromColorUnclamped}, + white_point::D65, + Xyz, +}; +use palette::{encoding, lms::BradfordLms}; use palette::{Hsl, Hsv, Hwb, IntoColor, LinSrgb, Srgb}; type SrgbHsv = Hsv; @@ -41,7 +45,9 @@ use data_color_mine::{load_data, ColorMine}; fn rgb_conversion(c: &mut Criterion) { let mut group = c.benchmark_group("Rgb family"); - let mut colormine: Vec> = load_data(Some(Path::new("../integration_tests/tests/convert/data_color_mine.csv"))); + let mut colormine: Vec> = load_data(Some(Path::new( + "../integration_tests/tests/convert/data_color_mine.csv", + ))); colormine.truncate(colormine.len() - colormine.len() % 8); assert_eq!( colormine.len() % 8, @@ -74,6 +80,11 @@ fn rgb_conversion(c: &mut Criterion) { let linear_hsv: Vec = colormine.iter().map(|x| x.hsv.into_color()).collect(); let linear_hsl: Vec = colormine.iter().map(|x| x.hsl.into_color()).collect(); let linear_hwb: Vec = colormine.iter().map(|x| x.hwb.into_color()).collect(); + let bradford_lms: Vec> = + colormine.iter().map(|x| x.xyz.into_color()).collect(); + + let xyz_to_rgb_matrix = LinSrgb::matrix_from_xyz(); + let lms_to_rgb_matrix = Xyz::matrix_from_lms().then(LinSrgb::matrix_from_xyz()); group.bench_with_input("rgb to linsrgb", &colormine, |b, colormine| { b.iter(|| { @@ -175,6 +186,35 @@ fn rgb_conversion(c: &mut Criterion) { }) }, ); + group.bench_with_input( + "xyz to linsrgb - Matrix3", + &(&colormine, xyz_to_rgb_matrix), + |b, &(colormine, matrix)| { + b.iter(|| { + for c in colormine { + black_box(matrix.convert(c.xyz)); + } + }) + }, + ); + group.bench_with_input("lms to linsrgb", &bradford_lms, |b, bradford_lms| { + b.iter(|| { + for &c in bradford_lms { + black_box(LinSrgb::from_color_unclamped(c)); + } + }) + }); + group.bench_with_input( + "lms to linsrgb - Matrix3", + &(&bradford_lms, lms_to_rgb_matrix), + |b, &(bradford_lms, matrix)| { + b.iter(|| { + for &c in bradford_lms { + black_box(matrix.convert(c)); + } + }) + }, + ); group.bench_with_input("hsl to rgb", &colormine, |b, colormine| { b.iter(|| { for c in colormine { diff --git a/palette/src/cam16/full.rs b/palette/src/cam16/full.rs index b78caee67..65cffebf6 100644 --- a/palette/src/cam16/full.rs +++ b/palette/src/cam16/full.rs @@ -1,14 +1,14 @@ use crate::{ - angle::RealAngle, - bool_mask::{HasBoolMask, LazySelect}, + bool_mask::HasBoolMask, + convert::Convert, hues::Cam16Hue, - num::{Abs, Arithmetics, FromScalar, PartialCmp, Powf, Real, Signum, Sqrt, Trigonometry, Zero}, + num::{FromScalar, Zero}, Alpha, GetHue, Xyz, }; use super::{ - BakedParameters, Cam16FromUnclamped, Cam16IntoUnclamped, Cam16Jch, Cam16Jmh, Cam16Jsh, - Cam16Qch, Cam16Qmh, Cam16Qsh, FromCam16Unclamped, IntoCam16Unclamped, WhitePointParameter, + BakedParameters, Cam16FromUnclamped, Cam16IntoUnclamped, IntoCam16Unclamped, + WhitePointParameter, }; /// CIE CAM16 with an alpha component. @@ -353,87 +353,19 @@ impl Alpha, A> { } } -impl Cam16FromUnclamped> for Cam16 +impl Cam16FromUnclamped for Cam16 where + T: FromScalar, WpParam: WhitePointParameter, - T: Real - + FromScalar - + Arithmetics - + Powf - + Sqrt - + Abs - + Signum - + Trigonometry - + RealAngle - + Clone, - T::Scalar: Clone, + BakedParameters: Convert, { type Scalar = T::Scalar; - fn cam16_from_unclamped( - color: Xyz, - parameters: BakedParameters, - ) -> Self { - super::math::xyz_to_cam16(color.with_white_point(), parameters.inner) + fn cam16_from_unclamped(color: C, parameters: BakedParameters) -> Self { + parameters.convert(color) } } -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, diff --git a/palette/src/cam16/parameters.rs b/palette/src/cam16/parameters.rs index 9c81cfe9b..7a26bc762 100644 --- a/palette/src/cam16/parameters.rs +++ b/palette/src/cam16/parameters.rs @@ -1,14 +1,19 @@ use core::marker::PhantomData; use crate::{ + angle::{RealAngle, SignedAngle}, bool_mask::LazySelect, + convert::{Convert, ConvertOnce}, num::{ - Abs, Arithmetics, Clamp, Exp, FromScalar, One, PartialCmp, Powf, Real, Signum, Sqrt, Zero, + Abs, Arithmetics, Clamp, Exp, FromScalar, One, PartialCmp, Powf, Real, Signum, Sqrt, + Trigonometry, Zero, }, white_point::{self, WhitePoint}, Xyz, }; +use super::{Cam16, Cam16Jch, Cam16Jmh, Cam16Jsh, Cam16Qch, Cam16Qmh, Cam16Qsh}; + /// Parameters for CAM16 that describe the viewing conditions. /// /// These parameters describe the viewing conditions for a more accurate color @@ -164,6 +169,116 @@ pub struct BakedParameters { white_point: PhantomData, } +impl Convert for BakedParameters +where + Self: ConvertOnce + Copy, +{ + #[inline] + fn convert(&self, input: I) -> O { + Self::convert_once(*self, input) + } +} + +impl ConvertOnce, Cam16> + for BakedParameters +where + WpParam: WhitePointParameter, + T: Real + + FromScalar + + Arithmetics + + Powf + + Sqrt + + Abs + + Signum + + Trigonometry + + RealAngle + + Clone, + T::Scalar: Clone, +{ + #[inline] + fn convert_once(self, input: Xyz) -> Cam16 { + super::math::xyz_to_cam16(input.with_white_point(), self.inner) + } +} + +impl ConvertOnce, Xyz> + for BakedParameters +where + T: FromScalar, + WpParam: WhitePointParameter, + Self: ConvertOnce, Xyz>, +{ + #[inline] + fn convert_once(self, input: Cam16) -> Xyz { + self.convert_once(Cam16Jch::from(input)) + } +} + +macro_rules! impl_convert_cam16_partial { + ($($name: ident),+) => { + $( + impl ConvertOnce<$name, Cam16> for BakedParameters + where + WpParam: WhitePointParameter, + T: Real + FromScalar + Zero + Arithmetics + Sqrt + PartialCmp + Clone, + T::Mask: LazySelect + Clone, + T::Scalar: Clone + { + #[inline] + fn convert_once(self, input: $name) -> Cam16 { + let ( + luminance, + chromaticity, + hue, + ) = input.into_dynamic(); + + let (lightness, brightness) = luminance.into_cam16(self.clone()); + let (chroma, colorfulness, saturation) = + chromaticity.into_cam16(lightness.clone(), self.clone()); + + Cam16 { + lightness, + chroma, + hue, + brightness, + colorfulness, + saturation, + } + } + } + + impl ConvertOnce<$name, Xyz> for BakedParameters + 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, + { + #[inline] + fn convert_once(self, cam16: $name) -> Xyz { + crate::cam16::math::cam16_to_xyz(cam16.into_dynamic(), self.inner.clone()) + .with_white_point() + } + } + )+ + }; +} + +impl_convert_cam16_partial!(Cam16Jmh, Cam16Jch, Cam16Jsh, Cam16Qmh, Cam16Qch, Cam16Qsh); + impl Clone for BakedParameters where T: Clone, @@ -278,7 +393,7 @@ impl WhitePointParameter for Xyz { /// Represents a static white point in [`Parameters`], as opposed to a dynamic /// [`Xyz`] value. -pub struct StaticWp(PhantomData); +pub struct StaticWp(pub(crate) PhantomData); impl WhitePointParameter for StaticWp where diff --git a/palette/src/chromatic_adaptation.rs b/palette/src/chromatic_adaptation.rs index d9364400e..2bd4ec422 100644 --- a/palette/src/chromatic_adaptation.rs +++ b/palette/src/chromatic_adaptation.rs @@ -30,7 +30,7 @@ use crate::{ self, meta::{LmsToXyz, XyzToLms}, }, - matrix::{multiply_3x3, multiply_xyz, Mat3}, + matrix::{multiply_3x3, multiply_3x3_and_vec3, Mat3}, num::{Arithmetics, Real, Zero}, white_point::{Any, WhitePoint}, Xyz, @@ -74,14 +74,14 @@ where ) -> Mat3 { let adapt = self.get_cone_response(); - let resp_src = multiply_xyz(adapt.ma.clone(), source_wp); - let resp_dst = multiply_xyz(adapt.ma.clone(), destination_wp); + let [src_x, src_y, src_z] = multiply_3x3_and_vec3(adapt.ma.clone(), source_wp.into()); + let [dst_x, dst_y, dst_z] = multiply_3x3_and_vec3(adapt.ma.clone(), destination_wp.into()); #[rustfmt::skip] let resp = [ - resp_dst.x / resp_src.x, T::zero(), T::zero(), - T::zero(), resp_dst.y / resp_src.y, T::zero(), - T::zero(), T::zero(), resp_dst.z / resp_src.z, + dst_x / src_x, T::zero(), T::zero(), + T::zero(), dst_y / src_y, T::zero(), + T::zero(), T::zero(), dst_z / src_z, ]; let tmp = multiply_3x3(resp, adapt.ma); @@ -152,10 +152,10 @@ where { #[inline] fn adapt_from_using>(color: S, method: M) -> D { - let src_xyz = color.into_color_unclamped().with_white_point(); + let src_xyz: Xyz = color.into_color_unclamped(); let transform_matrix = method.generate_transform_matrix(Swp::get_xyz(), Dwp::get_xyz()); - let dst_xyz = multiply_xyz(transform_matrix, src_xyz); - D::from_color_unclamped(dst_xyz.with_white_point()) + let dst_xyz: Xyz = multiply_3x3_and_vec3(transform_matrix, src_xyz.into()).into(); + D::from_color_unclamped(dst_xyz) } } diff --git a/palette/src/convert.rs b/palette/src/convert.rs index 9ba20854f..e0bf655fb 100644 --- a/palette/src/convert.rs +++ b/palette/src/convert.rs @@ -76,7 +76,67 @@ //! } //! ``` //! -//! # Deriving +//! # Reusing Intermediate Data +//! +//! Some conversions produce intermediate data that is the same for all values. +//! For example, conversion between [`Rgb`][crate::rgb::Rgb] and +//! [`Xyz`][crate::xyz::Xyz] uses a [`Matrix3`], while +//! [`Cam16`][crate::cam16::Cam16] uses a set of viewing condition as +//! [`BakedParameters`][crate::cam16::BakedParameters]. These values can be used +//! as "converters" via the [`Convert`] and [`ConvertOnce`] traits. +//! +//! **Remember:** As with anything related to optimization, the performance +//! difference may vary, depending on the compiler's ability to detect and +//! optimize the repeated work on its own. +//! +//! It's sometimes possible to save some computation time by explicitly caching +//! and reusing these intermediate values: +//! +//! ``` +//! use palette::{ +//! Srgb, Xyz, FromColor, +//! cam16::{Cam16, Parameters}, +//! convert::Convert, +//! }; +//! # let image_data: [u8;0] = []; +//! +//! // Parameters only need to "bake" once per set of viewing conditions: +//! let parameters = Parameters::default_static_wp(40.0).bake(); +//! +//! let pixels: &[Srgb] = palette::cast::from_component_slice(&image_data); +//! for &color in pixels { +//! let input = Xyz::from_color(color.into_linear()); +//! let cam16: Cam16 = parameters.convert(input); +//! +//! // ... +//! } +//! ``` +//! +//! It may also be possible to combine multiple matrix steps into a single +//! matrix, instead of applying them one-by-one: +//! +//! ``` +//! use palette::{ +//! Srgb, Xyz, lms::BradfordLms, +//! convert::Convert, +//! white_point::D65, +//! }; +//! # let image_data: [u8;0] = []; +//! +//! // While each matrix may be a compile time constant, the compiler may not +//! // combine them into a single matrix by itself: +//! let matrix = Xyz::matrix_from_rgb() +//! .then(BradfordLms::::matrix_from_xyz()); +//! +//! let pixels: &[Srgb] = palette::cast::from_component_slice(&image_data); +//! for &color in pixels { +//! let lms = matrix.convert(color.into_linear()); +//! +//! // ... +//! } +//! ``` +//! +//! # Deriving `FromColorUnclamped` //! //! `FromColorUnclamped` can be derived in a mostly automatic way. The other //! traits are blanket implemented based on it. The default minimum requirement @@ -135,21 +195,21 @@ //! ### Item Attributes //! //! * `skip_derives(Luma, Rgb)`: No conversion derives will be implemented for -//! these colors. They are instead to be implemented manually, and serve as the -//! basis for the automatic implementations. +//! these colors. They are instead to be implemented manually, and serve as +//! the basis for the automatic implementations. //! //! * `white_point = "some::white_point::Type"`: Sets the white point type that -//! should be used when deriving. The default is `D65`, but it may be any other -//! type, including type parameters. +//! should be used when deriving. The default is `D65`, but it may be any +//! other type, including type parameters. //! //! * `component = "some::component::Type"`: Sets the color component type that -//! should be used when deriving. The default is `f32`, but it may be any other -//! type, including type parameters. +//! should be used when deriving. The default is `f32`, but it may be any +//! other type, including type parameters. //! //! * `rgb_standard = "some::rgb_standard::Type"`: Sets the RGB standard type -//! that should be used when deriving. The default is to either use `Srgb` or a -//! best effort to convert between standards, but sometimes it has to be set to -//! a specific type. This also accepts type parameters. +//! that should be used when deriving. The default is to either use `Srgb` or +//! a best effort to convert between standards, but sometimes it has to be set +//! to a specific type. This also accepts type parameters. //! //! * `luma_standard = "some::rgb_standard::Type"`: Sets the Luma standard type //! that should be used when deriving, similar to `rgb_standard`. @@ -353,15 +413,98 @@ pub use self::{ from_into_color::*, from_into_color_mut::*, from_into_color_unclamped::*, - from_into_color_unclamped_mut::*, try_from_into_color::*, + from_into_color_unclamped_mut::*, matrix3::*, try_from_into_color::*, }; mod from_into_color; mod from_into_color_mut; mod from_into_color_unclamped; mod from_into_color_unclamped_mut; +mod matrix3; mod try_from_into_color; +/// Represents types that can convert a value from one type to another. +pub trait Convert: ConvertOnce { + /// Convert an input value of type `I` to a value of type `O`. + /// + /// The conversion may produce values outside the typical range of `O`. + /// + /// ``` + /// use palette::{ + /// Xyz, Srgb, + /// convert::Convert, + /// }; + /// + /// let matrix = Xyz::matrix_from_rgb(); + /// let rgb = Srgb::new(0.8f32, 0.3, 0.3).into_linear(); + /// let xyz = matrix.convert(rgb); + /// ``` + #[must_use] + fn convert(&self, input: I) -> O; +} + +impl Convert for &T +where + T: Convert + ?Sized, +{ + #[inline] + fn convert(&self, input: I) -> O { + T::convert(self, input) + } +} + +#[cfg(feature = "alloc")] +impl Convert for alloc::boxed::Box +where + T: Convert, +{ + #[inline] + fn convert(&self, input: I) -> O { + T::convert(self, input) + } +} + +/// Represents types that can convert a value from one type to another at least once. +pub trait ConvertOnce { + /// Convert an input value of type `I` to a value of type `O`, while consuming itself. + /// + /// The conversion may produce values outside the typical range of `O`. + /// + /// ``` + /// use palette::{ + /// Xyz, Srgb, + /// convert::ConvertOnce, + /// }; + /// + /// let matrix = Xyz::matrix_from_rgb(); + /// let rgb = Srgb::new(0.8f32, 0.3, 0.3).into_linear(); + /// let xyz = matrix.convert_once(rgb); + /// ``` + #[must_use] + fn convert_once(self, input: I) -> O; +} + +impl ConvertOnce for &T +where + T: Convert + ?Sized, +{ + #[inline] + fn convert_once(self, input: I) -> O { + T::convert(self, input) + } +} + +#[cfg(feature = "alloc")] +impl ConvertOnce for alloc::boxed::Box +where + T: ConvertOnce, +{ + #[inline] + fn convert_once(self, input: I) -> O { + T::convert_once(*self, input) + } +} + #[cfg(test)] mod tests { use core::marker::PhantomData; diff --git a/palette/src/convert/matrix3.rs b/palette/src/convert/matrix3.rs new file mode 100644 index 000000000..eba54ae16 --- /dev/null +++ b/palette/src/convert/matrix3.rs @@ -0,0 +1,248 @@ +use core::marker::PhantomData; + +use crate::{ + cast::{self, ArrayCast}, + matrix::{matrix_inverse, multiply_3x3, multiply_3x3_and_vec3}, + num::{Arithmetics, IsValidDivisor, One, Recip, Zero}, + ArrayExt, Mat3, +}; + +use super::{Convert, ConvertOnce}; + +/// A statically typed 3x3 conversion matrix. +/// +/// It's applied via [`Convert`] or [`ConvertOnce`] and helps making some +/// conversions more efficient by only needing to be constructed once. +/// +/// ``` +/// use palette::{ +/// Xyz, Srgb, +/// convert::{Convert, Matrix3}, +/// }; +/// +/// // Multiple matrices can be combined into one: +/// let matrix = Xyz::matrix_from_rgb() +/// .then(Matrix3::scale(0.5, 0.5, 0.5)); +/// +/// let rgb = Srgb::new(0.8f32, 0.3, 0.3).into_linear(); +/// let scaled_xyz = matrix.convert(rgb); +/// ``` +pub struct Matrix3 +where + I: ArrayCast, +{ + matrix: Mat3<::Item>, + transform: PhantomData O>, +} + +impl Convert for Matrix3 +where + Self: ConvertOnce + Copy, + I: ArrayCast, +{ + #[inline] + fn convert(&self, input: I) -> O { + Self::convert_once(*self, input) + } +} + +impl ConvertOnce for Matrix3 +where + T: Arithmetics, + I: ArrayCast, + O: ArrayCast, +{ + #[inline] + fn convert_once(self, input: I) -> O { + cast::from_array(multiply_3x3_and_vec3(self.matrix, cast::into_array(input))) + } +} + +impl Matrix3 +where + C: ArrayCast, +{ + /// Produce an identity matrix, which leaves the components unchanged. + /// + /// ``` + /// use palette::{Srgb, convert::{Matrix3, Convert}}; + /// + /// let matrix = Matrix3::identity(); + /// + /// let input = Srgb::new(0.1, 0.2, 0.3); + /// let output = matrix.convert(input); + /// + /// assert_eq!(input, output); + /// ``` + #[rustfmt::skip] + #[inline] + pub fn identity() -> Self where T: One + Zero { + Self::from_array([ + T::one(), T::zero(), T::zero(), + T::zero(), T::one(), T::zero(), + T::zero(), T::zero(), T::one(), + ]) + } + + /// Produce a scale matrix, which scales each component separately. + /// + /// ``` + /// use palette::{Srgb, convert::{Matrix3, Convert}}; + /// + /// let matrix = Matrix3::scale(0.1, 0.2, 0.3); + /// + /// let input = Srgb::new(1.0, 1.0, 1.0); + /// let output = matrix.convert(input); + /// + /// assert_eq!(Srgb::new(0.1, 0.2, 0.3), output); + /// ``` + #[rustfmt::skip] + #[inline] + pub fn scale(s1: T, s2: T, s3: T) -> Self where T: Zero { + Self::from_array([ + s1, T::zero(), T::zero(), + T::zero(), s2, T::zero(), + T::zero(), T::zero(), s3, + ]) + } +} + +impl Matrix3 +where + I: ArrayCast, + O: ArrayCast, +{ + /// Chain another matrix after this one. + /// + /// The combined matrix will result in the same values as if the two + /// matrices were applied separately, at the cost of only applying a single + /// matrix. This may speed up computations if the matrix can be constructed + /// once, and applied multiple times. + /// + /// ``` + /// use palette::{ + /// lms::VonKriesLms, Xyz, Srgb, + /// convert::Convert, + /// white_point::D65, + /// }; + /// use approx::assert_relative_eq; + /// + /// let rgb_to_xyz = Xyz::matrix_from_rgb(); + /// let xyz_to_lms = VonKriesLms::::matrix_from_xyz(); + /// + /// let rgb_to_lms = rgb_to_xyz.then(xyz_to_lms); + /// + /// let rgb = Srgb::new(0.8f32, 0.3, 0.3).into_linear(); + /// let lms = rgb_to_lms.convert(rgb); + /// + /// // Applying the matrices separately for comparison: + /// let xyz = rgb_to_xyz.convert(rgb); + /// let lms2 = xyz_to_lms.convert(xyz); + /// assert_relative_eq!(lms, lms2); + /// ``` + #[inline] + pub fn then(self, next: Matrix3) -> Matrix3 + where + U: ArrayCast, + T: Arithmetics + Clone, + { + Matrix3 { + matrix: multiply_3x3(next.matrix, self.matrix), + transform: PhantomData, + } + } + + /// Invert the matrix to create a reversed conversion. + /// + /// ## Panics + /// + /// A matrix that cannot be inverted will result in a panic. + /// + /// ## Examples + /// + /// ``` + /// use palette::{ + /// Xyz, Srgb, + /// convert::Convert, + /// }; + /// use approx::assert_relative_eq; + /// + /// let rgb_to_xyz = Xyz::matrix_from_rgb(); + /// let xyz_to_rgb = rgb_to_xyz.invert(); + /// + /// let rgb = Srgb::new(0.8f32, 0.3, 0.3).into_linear(); + /// let xyz = rgb_to_xyz.convert(rgb); + /// let rgb2 = xyz_to_rgb.convert(xyz); + /// + /// assert_relative_eq!(rgb, rgb2); + /// ``` + #[inline] + pub fn invert(self) -> Matrix3 + where + T: Recip + IsValidDivisor + Arithmetics + Clone, + { + Matrix3 { + matrix: matrix_inverse(self.matrix), + transform: PhantomData, + } + } + + /// Create a conversion matrix from a plain array. + /// + /// The matrix elements are expected to be in row-major order. + /// + ///

+ /// This doesn't verify that the conversion results in correct or expected values. + ///

+ /// + /// ``` + /// use palette::{Srgb, convert::{Matrix3, Convert}}; + /// + /// let matrix = Matrix3::from_array([ + /// 1.0, 0.0, 0.0, + /// 0.0, 1.0, 0.0, + /// 0.0, 0.0, 1.0, + /// ]); + /// + /// let input = Srgb::new(0.1, 0.2, 0.3); + /// let output = matrix.convert(input); + /// + /// assert_eq!(input, output); + /// ``` + #[inline] + pub const fn from_array(matrix: Mat3) -> Self { + Self { + matrix, + transform: PhantomData, + } + } + + /// Extract the inner array. + /// + /// The matrix elements are stored in row-major order. + #[inline] + pub fn into_array(self) -> Mat3 { + self.matrix + } +} + +impl Clone for Matrix3 +where + I: ArrayCast, + ::Item: Clone, +{ + #[inline] + fn clone(&self) -> Self { + Self { + matrix: self.matrix.clone(), + transform: self.transform, + } + } +} + +impl Copy for Matrix3 +where + I: ArrayCast, + ::Item: Copy, +{ +} diff --git a/palette/src/lms/lms.rs b/palette/src/lms/lms.rs index 166b6f457..bfec6b797 100644 --- a/palette/src/lms/lms.rs +++ b/palette/src/lms/lms.rs @@ -2,8 +2,7 @@ use core::marker::PhantomData; use crate::{ bool_mask::HasBoolMask, - convert::FromColorUnclamped, - matrix::multiply_3x3_and_vec3, + convert::{ConvertOnce, FromColorUnclamped, Matrix3}, num::{Arithmetics, Zero}, stimulus::{FromStimulus, Stimulus, StimulusColor}, xyz::meta::HasXyzMeta, @@ -62,7 +61,7 @@ pub type Lmsa = Alpha, T>; /// ``` #[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)] #[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))] -#[palette(palette_internal, component = "T", skip_derives(Xyz))] +#[palette(palette_internal, component = "T", skip_derives(Lms, Xyz))] #[repr(C)] pub struct Lms { /// Stimulus from long wavelengths, or red, or ρ. The typical range is @@ -180,6 +179,18 @@ where } } +impl Lms { + /// Produce a conversion matrix from [`Xyz`] to [`Lms`]. + #[inline] + pub fn matrix_from_xyz() -> Matrix3, Self> + where + M: HasXyzMeta + HasLmsMatrix, + M::LmsMatrix: XyzToLms, + { + Matrix3::from_array(M::LmsMatrix::xyz_to_lms_matrix()) + } +} + /// [`Lmsa`][Lmsa] implementations. impl Alpha, A> { /// Create an LMSA color. @@ -258,14 +269,22 @@ impl Alpha, A> { } } +impl FromColorUnclamped> for Lms { + #[inline] + fn from_color_unclamped(val: Lms) -> Self { + val + } +} + impl FromColorUnclamped> for Lms where M: HasLmsMatrix + HasXyzMeta, M::LmsMatrix: XyzToLms, T: Arithmetics, { + #[inline] fn from_color_unclamped(val: Xyz) -> Self { - multiply_3x3_and_vec3(M::LmsMatrix::xyz_to_lms_matrix(), val.into()).into() + Self::matrix_from_xyz().convert_once(val) } } diff --git a/palette/src/matrix.rs b/palette/src/matrix.rs index bc808a34d..4585d8a00 100644 --- a/palette/src/matrix.rs +++ b/palette/src/matrix.rs @@ -1,13 +1,10 @@ //! This module provides simple matrix operations on 3x3 matrices to aid in //! chromatic adaptation and conversion calculations. -use core::marker::PhantomData; - use crate::{ convert::IntoColorUnclamped, - encoding::Linear, num::{Arithmetics, FromScalar, IsValidDivisor, Recip}, - rgb::{Primaries, Rgb, RgbSpace}, + rgb::{Primaries, RgbSpace}, white_point::{Any, WhitePoint}, Xyz, Yxy, }; @@ -16,15 +13,6 @@ use crate::{ pub type Mat3 = [T; 9]; pub type Vec3 = [T; 3]; -/// Multiply the 3x3 matrix with an XYZ color. -#[inline] -pub fn multiply_xyz(matrix: Mat3, color: Xyz) -> Xyz -where - T: Arithmetics, -{ - multiply_3x3_and_vec3(matrix, color.into()).into() -} - /// Multiply the 3x3 matrix with an XYZ color. #[inline] pub fn multiply_3x3_and_vec3(matrix: Mat3, vector: Vec3) -> Vec3 @@ -49,52 +37,6 @@ where [x1 + x2 + x3, y1 + y2 + y3, z1 + z2 + z3] } -/// Multiply the 3x3 matrix with an XYZ color to return an RGB color. -#[inline] -pub fn multiply_xyz_to_rgb(c: Mat3, f: Xyz) -> Rgb, V> -where - S: RgbSpace, - V: Arithmetics + FromScalar, -{ - // Input Mat3 is destructured to avoid panic paths. red, green, and blue - // can't be extracted like in `multiply_xyz` to get a performance increase - let [c0, c1, c2, c3, c4, c5, c6, c7, c8] = c; - - Rgb { - red: (V::from_scalar(c0) * &f.x) - + (V::from_scalar(c1) * &f.y) - + (V::from_scalar(c2) * &f.z), - green: (V::from_scalar(c3) * &f.x) - + (V::from_scalar(c4) * &f.y) - + (V::from_scalar(c5) * &f.z), - blue: (V::from_scalar(c6) * f.x) + (V::from_scalar(c7) * f.y) + (V::from_scalar(c8) * f.z), - standard: PhantomData, - } -} -/// Multiply the 3x3 matrix with an RGB color to return an XYZ color. -#[inline] -pub fn multiply_rgb_to_xyz(c: Mat3, f: Rgb, V>) -> Xyz -where - S: RgbSpace, - V: Arithmetics + FromScalar, -{ - // Input Mat3 is destructured to avoid panic paths. Same problem as - // `multiply_xyz_to_rgb` for extracting x, y, z - let [c0, c1, c2, c3, c4, c5, c6, c7, c8] = c; - - Xyz { - x: (V::from_scalar(c0) * &f.red) - + (V::from_scalar(c1) * &f.green) - + (V::from_scalar(c2) * &f.blue), - y: (V::from_scalar(c3) * &f.red) - + (V::from_scalar(c4) * &f.green) - + (V::from_scalar(c5) * &f.blue), - z: (V::from_scalar(c6) * f.red) - + (V::from_scalar(c7) * f.green) - + (V::from_scalar(c8) * f.blue), - white_point: PhantomData, - } -} /// Multiply two 3x3 matrices. #[inline] @@ -195,24 +137,24 @@ where let matrix = mat3_from_primaries(r, g, b); - let s_matrix: Rgb, T> = multiply_xyz_to_rgb( + let [s_red, s_green, s_blue] = multiply_3x3_and_vec3( matrix_inverse(matrix.clone()), - S::WhitePoint::get_xyz().with_white_point(), + S::WhitePoint::get_xyz().into(), ); // Destructuring has some performance benefits, don't change unless measured let [t0, t1, t2, t3, t4, t5, t6, t7, t8] = matrix; [ - t0 * &s_matrix.red, - t1 * &s_matrix.green, - t2 * &s_matrix.blue, - t3 * &s_matrix.red, - t4 * &s_matrix.green, - t5 * &s_matrix.blue, - t6 * s_matrix.red, - t7 * s_matrix.green, - t8 * s_matrix.blue, + t0 * &s_red, + t1 * &s_green, + t2 * &s_blue, + t3 * &s_red, + t4 * &s_green, + t5 * &s_blue, + t6 * s_red, + t7 * s_green, + t8 * s_blue, ] } @@ -229,12 +171,9 @@ fn mat3_from_primaries(r: Xyz, g: Xyz, b: Xyz) -> Mat #[cfg(feature = "approx")] #[cfg(test)] mod test { - use super::{matrix_inverse, multiply_3x3, multiply_xyz, rgb_to_xyz_matrix}; - use crate::chromatic_adaptation::AdaptInto; - use crate::encoding::{Linear, Srgb}; - use crate::rgb::Rgb; - use crate::white_point::D50; - use crate::Xyz; + use super::{matrix_inverse, multiply_3x3, rgb_to_xyz_matrix}; + use crate::encoding::Srgb; + use crate::matrix::multiply_3x3_and_vec3; #[test] fn matrix_multiply_3x3() { @@ -249,14 +188,16 @@ mod test { } #[test] - fn matrix_multiply_xyz() { + fn matrix_multiply_vec3() { let inp1 = [0.1, 0.2, 0.3, 0.3, 0.2, 0.1, 0.2, 0.1, 0.3]; - let inp2 = Xyz::new(0.4, 0.6, 0.8); + let inp2 = [0.4, 0.6, 0.8]; - let expected = Xyz::new(0.4, 0.32, 0.38); + let expected = [0.4, 0.32, 0.38]; - let computed = multiply_xyz(inp1, inp2); - assert_relative_eq!(expected, computed) + let computed = multiply_3x3_and_vec3(inp1, inp2); + for (t1, t2) in expected.iter().zip(computed.iter()) { + assert_relative_eq!(t1, t2); + } } #[test] @@ -299,13 +240,4 @@ mod test { assert_relative_eq!(e, c, epsilon = 0.000001) } } - - #[test] - fn d65_to_d50() { - let input: Rgb> = Rgb::new(1.0, 1.0, 1.0); - let expected: Rgb> = Rgb::new(1.0, 1.0, 1.0); - - let computed: Rgb> = input.adapt_into(); - assert_relative_eq!(expected, computed, epsilon = 0.000001); - } } diff --git a/palette/src/oklab.rs b/palette/src/oklab.rs index 51db0f31e..6501c250f 100644 --- a/palette/src/oklab.rs +++ b/palette/src/oklab.rs @@ -9,7 +9,7 @@ use crate::{ bool_mask::HasBoolMask, convert::{FromColorUnclamped, IntoColorUnclamped}, encoding::{IntoLinear, Srgb}, - matrix::multiply_xyz, + matrix::multiply_3x3_and_vec3, num::{Arithmetics, Cbrt, Hypot, MinMax, One, Powi, Real, Sqrt, Trigonometry, Zero}, ok_utils::{toe_inv, ChromaValues, LC, ST}, rgb::{Rgb, RgbSpace, RgbStandard}, @@ -267,15 +267,8 @@ where let m1 = m1(); let m2 = m2(); - let Xyz { - x: l, y: m, z: s, .. - } = multiply_xyz(m1, color.with_white_point()); - - let l_m_s_ = Xyz::new(l.cbrt(), m.cbrt(), s.cbrt()); - - let Xyz { - x: l, y: a, z: b, .. - } = multiply_xyz(m2, l_m_s_); + let [l, m, s] = multiply_3x3_and_vec3(m1, color.into()); + let [l, a, b] = multiply_3x3_and_vec3(m2, [l.cbrt(), m.cbrt(), s.cbrt()]); Self::new(l, a, b) } diff --git a/palette/src/rgb/rgb.rs b/palette/src/rgb/rgb.rs index b3c99c838..f91fb5046 100644 --- a/palette/src/rgb/rgb.rs +++ b/palette/src/rgb/rgb.rs @@ -14,10 +14,10 @@ use crate::{ bool_mask::{BitOps, HasBoolMask, LazySelect}, cast::{ComponentOrder, Packed}, color_difference::Wcag21RelativeContrast, - convert::{FromColorUnclamped, IntoColorUnclamped}, - encoding::{FromLinear, IntoLinear, Linear, Srgb}, + convert::{ConvertOnce, FromColorUnclamped, IntoColorUnclamped, Matrix3}, + encoding::{linear::LinearFn, FromLinear, IntoLinear, Linear, Srgb}, luma::LumaStandard, - matrix::{matrix_inverse, matrix_map, multiply_xyz_to_rgb, rgb_to_xyz_matrix}, + matrix::{matrix_inverse, matrix_map, rgb_to_xyz_matrix}, num::{ Abs, Arithmetics, FromScalar, IsValidDivisor, MinMax, One, PartialCmp, Real, Recip, Round, Trigonometry, Zero, @@ -309,6 +309,33 @@ where } } +impl Rgb { + /// Produce a conversion matrix from [`Xyz`] to linear [`Rgb`]. + #[allow(clippy::type_complexity)] + #[inline] + pub fn matrix_from_xyz() -> Matrix3::WhitePoint, T>, Self> + where + S: RgbStandard, + ::Primaries: Primaries, + ::WhitePoint: WhitePoint, + T: FromScalar, + T::Scalar: Real + + Recip + + IsValidDivisor + + Arithmetics + + Clone + + FromScalar, + Yxy: IntoColorUnclamped>, + { + let transform_matrix = S::Space::xyz_to_rgb_matrix().map_or_else( + || matrix_inverse(rgb_to_xyz_matrix::()), + |matrix| matrix_map(matrix, T::Scalar::from_f64), + ); + + Matrix3::from_array(matrix_map(transform_matrix, T::from_scalar)) + } +} + impl Rgb { /// Convert to a packed `u32` with with specifiable component order. /// @@ -751,12 +778,10 @@ where + FromScalar, Yxy: IntoColorUnclamped>, { + #[inline] fn from_color_unclamped(color: Xyz<::WhitePoint, T>) -> Self { - let transform_matrix = S::Space::xyz_to_rgb_matrix().map_or_else( - || matrix_inverse(rgb_to_xyz_matrix::()), - |matrix| matrix_map(matrix, T::Scalar::from_f64), - ); - Self::from_linear(multiply_xyz_to_rgb(transform_matrix, color)) + let transform_matrix = Rgb::, T>::matrix_from_xyz(); + Self::from_linear(transform_matrix.convert_once(color)) } } diff --git a/palette/src/xyz.rs b/palette/src/xyz.rs index 616bf29c6..751615a21 100644 --- a/palette/src/xyz.rs +++ b/palette/src/xyz.rs @@ -5,26 +5,17 @@ pub mod meta; 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, + cam16::{FromCam16Unclamped, WhitePointParameter}, + convert::{ConvertOnce, FromColorUnclamped, IntoColorUnclamped, Matrix3}, + encoding::{linear::LinearFn, IntoLinear, Linear}, lms::{ meta::{HasLmsMatrix, LmsToXyz}, Lms, }, luma::LumaStandard, - matrix::{ - matrix_map, multiply_3x3_and_vec3, multiply_rgb_to_xyz, multiply_xyz, rgb_to_xyz_matrix, - }, - num::{ - Abs, Arithmetics, FromScalar, IsValidDivisor, One, PartialCmp, Powf, Powi, Real, Recip, - Signum, Sqrt, Trigonometry, Zero, - }, + matrix::{matrix_map, multiply_3x3_and_vec3, rgb_to_xyz_matrix}, + num::{Arithmetics, FromScalar, IsValidDivisor, One, PartialCmp, Powi, Real, Recip, Zero}, oklab, rgb::{Primaries, Rgb, RgbSpace, RgbStandard}, stimulus::{Stimulus, StimulusColor}, @@ -149,6 +140,43 @@ where } } +impl Xyz { + /// Produce a conversion matrix from linear [`Rgb`] to [`Xyz`]. + #[inline] + pub fn matrix_from_rgb() -> Matrix3, Self> + where + T: FromScalar, + T::Scalar: Real + + Recip + + IsValidDivisor + + Arithmetics + + FromScalar + + Clone, + Wp: WhitePoint, + S: RgbStandard, + S::Space: RgbSpace, + ::Primaries: Primaries, + Yxy: IntoColorUnclamped>, + { + let transform_matrix = S::Space::rgb_to_xyz_matrix() + .map_or_else(rgb_to_xyz_matrix::, |matrix| { + matrix_map(matrix, T::Scalar::from_f64) + }); + + Matrix3::from_array(matrix_map(transform_matrix, T::from_scalar)) + } + + /// Produce a conversion matrix from [`Lms`] to [`Xyz`]. + #[inline] + pub fn matrix_from_lms() -> Matrix3, Self> + where + M: HasXyzMeta + HasLmsMatrix, + M::LmsMatrix: LmsToXyz, + { + Matrix3::from_array(M::LmsMatrix::lms_to_xyz_matrix()) + } +} + ///[`Xyza`](crate::Xyza) implementations. impl Alpha, A> { /// Create a CIE XYZ color with transparency. @@ -188,6 +216,7 @@ impl_reference_component_methods!(Xyz, [x, y, z], white_point); impl_struct_of_arrays_methods!(Xyz, [x, y, z], white_point); impl FromColorUnclamped> for Xyz { + #[inline] fn from_color_unclamped(color: Xyz) -> Self { color } @@ -209,12 +238,10 @@ where ::Primaries: Primaries, Yxy: IntoColorUnclamped>, { + #[inline] fn from_color_unclamped(color: Rgb) -> Self { - let transform_matrix = S::Space::rgb_to_xyz_matrix() - .map_or_else(rgb_to_xyz_matrix::, |matrix| { - matrix_map(matrix, T::Scalar::from_f64) - }); - multiply_rgb_to_xyz(transform_matrix, color.into_linear()) + let transform_matrix = Self::matrix_from_rgb::>(); + transform_matrix.convert_once(color.into_linear()) } } @@ -223,6 +250,7 @@ where T: Zero + One + IsValidDivisor + Arithmetics + Clone, T::Mask: LazySelect + Clone, { + #[inline] fn from_color_unclamped(color: Yxy) -> Self { let Yxy { x, y, luma, .. } = color; @@ -251,6 +279,7 @@ where T::Mask: LazySelect, Wp: WhitePoint, { + #[inline] fn from_color_unclamped(color: Lab) -> Self { // Recip call shows performance benefits in benchmarks for this function let y = (color.l + T::from_f64(16.0)) * T::from_f64(116.0).recip(); @@ -311,16 +340,15 @@ impl FromColorUnclamped> for Xyz where T: Real + Powi + Arithmetics, { + #[inline] fn from_color_unclamped(color: Oklab) -> Self { let m1_inv = oklab::m1_inv(); let m2_inv = oklab::m2_inv(); - let Xyz { - x: l, y: m, z: s, .. - } = multiply_xyz(m2_inv, Xyz::new(color.l, color.a, color.b)); + let [l, m, s] = multiply_3x3_and_vec3(m2_inv, [color.l, color.a, color.b]); + let [x, y, z] = multiply_3x3_and_vec3(m1_inv, [l.powi(3), m.powi(3), s.powi(3)]); - let lms = Xyz::new(l.powi(3), m.powi(3), s.powi(3)); - multiply_xyz(m1_inv, lms).with_white_point() + Self::new(x, y, z) } } @@ -331,6 +359,7 @@ where S: LumaStandard, S::TransferFn: IntoLinear, { + #[inline] fn from_color_unclamped(color: Luma) -> Self { Wp::get_xyz().with_white_point::() * color.into_linear().luma } @@ -342,66 +371,29 @@ where M::LmsMatrix: LmsToXyz, T: Arithmetics, { + #[inline] fn from_color_unclamped(val: Lms) -> Self { - multiply_3x3_and_vec3(M::LmsMatrix::lms_to_xyz_matrix(), val.into()).into() + Self::matrix_from_lms().convert_once(val) } } -impl FromCam16Unclamped> for Xyz +impl FromCam16Unclamped for Xyz where WpParam: WhitePointParameter, T: FromScalar, - Cam16Jch: Cam16IntoUnclamped, + crate::cam16::BakedParameters: ConvertOnce, { type Scalar = T::Scalar; + #[inline] fn from_cam16_unclamped( - cam16: Cam16, + cam16: C, parameters: crate::cam16::BakedParameters, ) -> Self { - Cam16Jch::from(cam16).cam16_into_unclamped(parameters) + parameters.convert_once(cam16) } } -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! {