diff --git a/.vscode/settings.json b/.vscode/settings.json index d3116486c..b073b7af7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,6 @@ "rust-analyzer.imports.granularity.enforce": true, "rust-analyzer.imports.granularity.group": "crate", "rust-analyzer.imports.group.enable": true, - "rust-analyzer.check.command": "clippy" + "rust-analyzer.check.command": "clippy", + "rust-analyzer.imports.preferNoStd": true } diff --git a/palette/README.md b/palette/README.md index 0b91f3239..91822ce07 100644 --- a/palette/README.md +++ b/palette/README.md @@ -215,7 +215,6 @@ use palette::{ encoding, white_point, rgb::Rgb, - chromatic_adaptation::AdaptFrom, Srgb }; @@ -227,9 +226,6 @@ type EqualEnergyStandard = (encoding::Srgb, white_point::E, encoding::Srgb); type EqualEnergySrgb = Rgb; let ee_rgb = EqualEnergySrgb::new(1.0, 0.5, 0.3); - -// We need to use chromatic adaptation when going between white points. -let srgb = Srgb::adapt_from(ee_rgb); ``` It's also possible to implement the traits for a custom type, for when the built-in options are not enough. diff --git a/palette/src/chromatic_adaptation.rs b/palette/src/chromatic_adaptation.rs index 2bd4ec422..52e5653cc 100644 --- a/palette/src/chromatic_adaptation.rs +++ b/palette/src/chromatic_adaptation.rs @@ -1,42 +1,353 @@ //! Convert colors from one reference white point to another //! -//! Chromatic adaptation is the human visual system’s ability to adjust to -//! changes in illumination in order to preserve the appearance of object -//! colors. It is responsible for the stable appearance of object colours -//! despite the wide variation of light which might be reflected from an object -//! and observed by our eyes. +//! Chromatic adaptation is the ability to adjust the appearance of colors to +//! changes in illumination. This happens naturally in our body's visual system, +//! and can be emulated with a "chromatic adaptation transform" (CAT). //! -//! This library provides three methods for chromatic adaptation Bradford (which -//! is the default), VonKries and XyzScaling +//! This library implements a one-step adaptation transform, known as the von +//! Kries method. It's provided as [`AdaptFromUnclamped`] or +//! [`AdaptIntoUnclamped`] for convenience, or [`adaptation_matrix`] for control +//! and reusability. All of them can be customized with different LMS matrices. //! -//! ``` -//! use palette::Xyz; -//! use palette::white_point::{A, C}; -//! use palette::chromatic_adaptation::AdaptInto; +//! The provided LMS matrices are: +//! +//! - [`Bradford`] - A "spectrally sharpened" matrix, which may improve +//! chromatic adaptation. This is the default for [`AdaptFromUnclamped`] and +//! [`AdaptIntoUnclamped`]. +//! - [`VonKries`][lms::matrix::VonKries] - Produces cone-describing LMS values, +//! as opposed to many other matrices, but may perform worse than other +//! matrices. +//! - [`UnitMatrix`][lms::matrix::UnitMatrix] - Included for completeness, but +//! generally considered a bad option. Also called "XYZ scaling" or "wrong von +//! Kries". //! +//! ``` +//! use palette::{ +//! Xyz, white_point::{A, C}, +//! chromatic_adaptation::AdaptIntoUnclamped, +//! }; +//! use approx::assert_relative_eq; //! -//! let a = Xyz::::new(0.315756, 0.162732, 0.015905); +//! let input = Xyz::::new(0.315756, 0.162732, 0.015905); //! -//! //Will convert Xyz to Xyz using Bradford chromatic adaptation -//! let c: Xyz = a.adapt_into(); +//! //Will convert Xyz to Xyz using Bradford chromatic adaptation; +//! let output: Xyz = input.adapt_into_unclamped(); //! -//! //Should print {x: 0.257963, y: 0.139776,z: 0.058825} -//! println!("{:?}", c) +//! let expected = Xyz::new(0.257963, 0.139776, 0.058825); +//! assert_relative_eq!(output, expected, epsilon = 0.0001); //! ``` +use core::ops::Div; + use crate::{ - convert::{FromColorUnclamped, IntoColorUnclamped}, + convert::{FromColorUnclamped, IntoColorUnclamped, Matrix3}, lms::{ self, - meta::{LmsToXyz, XyzToLms}, + matrix::{Bradford, LmsToXyz, WithLmsMatrix, XyzToLms}, + Lms, }, matrix::{multiply_3x3, multiply_3x3_and_vec3, Mat3}, num::{Arithmetics, Real, Zero}, white_point::{Any, WhitePoint}, + xyz::meta::HasXyzMeta, Xyz, }; +/// Construct a one-step chromatic adaptation matrix. +/// +/// The matrix uses the von Kries method to fully adapt a color from an input +/// white point to an output white point, using a provided LMS matrix. See the +/// [`chromatic_adaptation`][self] module for more details. +/// +/// ## Static White Points +/// +/// The `input_wp` and `output_wp` parameters represent the color "white" for +/// the input and output colors, respectively. Passing `None` will make it use +/// `I` and `O` to calculate the white points: +/// +/// ``` +/// use palette::{ +/// chromatic_adaptation::adaptation_matrix, +/// lms::matrix::Bradford, +/// convert::Convert, +/// white_point::{A, C}, +/// Xyz, +/// }; +/// use approx::assert_relative_eq; +/// +/// // Adapts from white point A to white point C: +/// let matrix = adaptation_matrix::(None, None); +/// +/// // Explicit types added for illustration. +/// let input: Xyz = Xyz::new(0.315756, 0.162732, 0.015905); +/// let output: Xyz = matrix.convert(input); +/// +/// let expected = Xyz::new(0.257963, 0.139776, 0.058825); +/// assert_relative_eq!(output, expected, epsilon = 0.0001); +/// ``` +/// +/// ## Dynamic White Points +/// +/// It's also possible to use arbitrary colors as white points, as long as they +/// are brighter than black. This can be useful for white balancing a photo, +/// where we may want to use the same static white point for both the input and +/// the output: +/// +/// ``` +/// use palette::{ +/// chromatic_adaptation::adaptation_matrix, +/// lms::matrix::Bradford, +/// convert::{FromColorUnclampedMut, Convert}, +/// Srgb, Xyz, +/// }; +/// use approx::assert_relative_eq; +/// +/// fn simple_white_balance(image: &mut [Srgb]) { +/// // Temporarily convert to Xyz: +/// let mut image = <[Xyz<_, f32>]>::from_color_unclamped_mut(image); +/// +/// // Find the average Xyz color: +/// let sum = image.iter().fold(Xyz::new(0.0, 0.0, 0.0), |sum, &c| sum + c); +/// let average = sum / image.len() as f32; +/// +/// // Considering the average color to be "white", this matrix adapts from the +/// // average to default sRGB white, D65: +/// let matrix = adaptation_matrix::<_, _, _, Bradford>(Some(average), None); +/// +/// for pixel in &mut *image { +/// *pixel = matrix.convert(*pixel); +/// } +/// } +/// +/// // Minimal test case. This one pixel becomes gray after white balancing: +/// let mut image = [Srgb::new(0.8, 0.3, 0.9)]; +/// simple_white_balance(&mut image); +/// +/// let expected = Srgb::new(0.524706, 0.524706, 0.524706); +/// assert_relative_eq!(image[0], expected, epsilon = 0.000001); +/// ``` +/// +/// See also [Wikipedia - Von Kries transform][wikipedia]. +/// +/// [wikipedia]: +/// https://en.wikipedia.org/wiki/Chromatic_adaptation#Von_Kries_transform +pub fn adaptation_matrix( + input_wp: Option>, + output_wp: Option>, +) -> Matrix3, Xyz> +where + T: Zero + Arithmetics + Clone, + I: WhitePoint + HasXyzMeta, + O: WhitePoint + HasXyzMeta, + M: XyzToLms + LmsToXyz, + Xyz: IntoColorUnclamped, T>>, + Xyz: IntoColorUnclamped, T>>, +{ + let input_to_lms = Lms::, T>::matrix_from_xyz(); + let lms_to_output = Xyz::::matrix_from_lms::>(); + + let input_wp = input_wp + .unwrap_or_else(|| I::get_xyz().with_white_point()) + .normalize() + .into_color_unclamped(); + + let output_wp = output_wp + .unwrap_or_else(|| O::get_xyz().with_white_point()) + .normalize() + .into_color_unclamped(); + + input_to_lms + .then(diagonal_matrix(input_wp, output_wp)) + .then(lms_to_output) +} + +/// Construct a diagonal matrix for full adaptation of [`Lms`] colors. +/// +/// This is the core matrix in the von Kries adaptation method and is a central +/// part of the matrix from [`adaptation_matrix`]. It's offered separately, as +/// an option for building more advanced adaptation matrices. +/// +/// The produced matrix is a diagonal matrix, containing the output white point +/// divided by the input white point: +/// +/// ```text +/// [out.l / in.l, 0, 0] +/// [ 0, out.m / in.m, 0] +/// [ 0, 0, out.s / in.s] +/// ``` +/// +/// See also [Wikipedia - Von Kries transform][wikipedia]. +/// +/// [wikipedia]: +/// https://en.wikipedia.org/wiki/Chromatic_adaptation#Von_Kries_transform +#[inline] +pub fn diagonal_matrix( + input_wp: Lms, + output_wp: Lms, +) -> Matrix3, Lms> +where + T: Zero + Div, +{ + let gain = output_wp / input_wp.with_meta(); + + #[rustfmt::skip] + let matrix = [ + gain.long, T::zero(), T::zero(), + T::zero(), gain.medium, T::zero(), + T::zero(), T::zero(), gain.short, + ]; + + Matrix3::from_array(matrix) +} + +/// A trait for unchecked conversion of one color from another via chromatic +/// adaptation. +/// +/// See [`FromColor`][crate::convert::FromColor], +/// [`TryFromColor`][crate::convert::TryFromColor] and [`FromColorUnclamped`] +/// for when there's no need for chromatic adaptation. +/// +/// Some conversions require the reference white point to be changed, while +/// maintaining the appearance of the color. This is called "chromatic +/// adaptation" or "white balancing", and typically involves converting the +/// color to the [`Lms`] color space. This trait defaults to using the +/// [`Bradford`] matrix as part of the process, but other options are available +/// in [`lms::matrix`]. +/// +/// The [`adaptation_matrix`] function offers more options and control. This +/// trait can be a convenient alternative when the source and destination white +/// points are statically known. +pub trait AdaptFromUnclamped: Sized { + /// The number type that's used as the color's components. + type Scalar; + + /// Adapt a color of type `T` into a color of type `Self`, using the + /// [`Bradford`] matrix. + /// + /// ``` + /// use palette::{ + /// Xyz, white_point::{A, C}, + /// chromatic_adaptation::AdaptFromUnclamped, + /// }; + /// + /// let input = Xyz::::new(0.315756, 0.162732, 0.015905); + /// + /// //Will convert Xyz to Xyz using Bradford chromatic adaptation: + /// let output = Xyz::::adapt_from_unclamped(input); + /// ``` + #[must_use] + #[inline] + fn adapt_from_unclamped(input: T) -> Self + where + Bradford: LmsToXyz + XyzToLms, + { + Self::adapt_from_unclamped_with::(input) + } + + /// Adapt a color of type `T` into a color of type `Self`, using the custom + /// matrix `M`. + /// + /// ``` + /// use palette::{ + /// Xyz, white_point::{A, C}, lms::matrix::VonKries, + /// chromatic_adaptation::AdaptFromUnclamped, + /// }; + /// + /// let input = Xyz::::new(0.315756, 0.162732, 0.015905); + /// + /// //Will convert Xyz to Xyz using von Kries chromatic adaptation: + /// let output = Xyz::::adapt_from_unclamped_with::(input); + /// ``` + #[must_use] + fn adapt_from_unclamped_with(input: T) -> Self + where + M: LmsToXyz + XyzToLms; +} + +/// A trait for unchecked conversion of one color into another via chromatic +/// adaptation. +/// +/// See [`IntoColor`][crate::convert::IntoColor], +/// [`TryIntoColor`][crate::convert::TryIntoColor] and [`IntoColorUnclamped`] +/// for when there's no need for chromatic adaptation. +/// +/// Some conversions require the reference white point to be changed, while +/// maintaining the appearance of the color. This is called "chromatic +/// adaptation" or "white balancing", and typically involves converting the +/// color to the [`Lms`] color space. This trait defaults to using the +/// [`Bradford`] matrix as part of the process, but other options are available +/// in [`lms::matrix`]. +/// +/// The [`adaptation_matrix`] function offers more options and control. This +/// trait can be a convenient alternative when the source and destination white +/// points are statically known. +pub trait AdaptIntoUnclamped: Sized { + /// The number type that's used as the color's components. + type Scalar; + + /// Adapt a color of type `Self` into a color of type `T`, using the + /// [`Bradford`] matrix. + /// + /// ``` + /// use palette::{ + /// Xyz, white_point::{A, C}, + /// chromatic_adaptation::AdaptIntoUnclamped, + /// }; + /// + /// let input = Xyz::::new(0.315756, 0.162732, 0.015905); + /// + /// //Will convert Xyz to Xyz using Bradford chromatic adaptation: + /// let output: Xyz = input.adapt_into_unclamped(); + /// ``` + #[must_use] + #[inline] + fn adapt_into_unclamped(self) -> T + where + Bradford: LmsToXyz + XyzToLms, + { + self.adapt_into_unclamped_with::() + } + + /// Adapt a color of type `Self` into a color of type `T`, using the custom + /// matrix `M`. + /// + /// ``` + /// use palette::{ + /// Xyz, white_point::{A, C}, lms::matrix::VonKries, + /// chromatic_adaptation::AdaptIntoUnclamped, + /// }; + /// + /// let input = Xyz::::new(0.315756, 0.162732, 0.015905); + /// + /// //Will convert Xyz to Xyz using von Kries chromatic adaptation: + /// let output: Xyz = input.adapt_into_unclamped_with::(); + /// ``` + #[must_use] + fn adapt_into_unclamped_with(self) -> T + where + M: LmsToXyz + XyzToLms; +} + +impl AdaptIntoUnclamped for C +where + T: AdaptFromUnclamped, +{ + type Scalar = T::Scalar; + + #[inline] + fn adapt_into_unclamped_with(self) -> T + where + M: LmsToXyz + XyzToLms, + { + T::adapt_from_unclamped_with::(self) + } +} + /// Chromatic adaptation methods implemented in the library +#[deprecated( + since = "0.7.7", + note = "use the options from `palette::lms::matrix` or a custom matrix" +)] pub enum Method { /// Bradford chromatic adaptation method Bradford, @@ -47,6 +358,10 @@ pub enum Method { } /// Holds the matrix coefficients for the chromatic adaptation methods +#[deprecated( + since = "0.7.7", + note = "use the options from `palette::lms::matrix` or a custom matrix" +)] pub struct ConeResponseMatrices { ///3x3 matrix for the cone response domains pub ma: Mat3, @@ -56,6 +371,11 @@ pub struct ConeResponseMatrices { /// Generates a conversion matrix to convert the Xyz tristimulus values from /// one illuminant to another (`source_wp` to `destination_wp`) +#[deprecated( + since = "0.7.7", + note = "use the options from `palette::lms::matrix` or a custom matrix" +)] +#[allow(deprecated)] pub trait TransformMatrix where T: Zero + Arithmetics + Clone, @@ -74,21 +394,19 @@ where ) -> Mat3 { let adapt = self.get_cone_response(); - 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()); + let resp_src: Lms = + multiply_3x3_and_vec3(adapt.ma.clone(), source_wp.into()).into(); + let resp_dst: Lms = + multiply_3x3_and_vec3(adapt.ma.clone(), destination_wp.into()).into(); - #[rustfmt::skip] - let resp = [ - 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 resp = diagonal_matrix(resp_src, resp_dst).into_array(); let tmp = multiply_3x3(resp, adapt.ma); multiply_3x3(adapt.inv_ma, tmp) } } +#[allow(deprecated)] impl TransformMatrix for Method where T: Real + Zero + Arithmetics + Clone, @@ -99,20 +417,20 @@ where match *self { Method::Bradford => { ConeResponseMatrices:: { - ma: lms::meta::Bradford::xyz_to_lms_matrix(), - inv_ma: lms::meta::Bradford::lms_to_xyz_matrix(), + ma: lms::matrix::Bradford::xyz_to_lms_matrix(), + inv_ma: lms::matrix::Bradford::lms_to_xyz_matrix(), } } Method::VonKries => { ConeResponseMatrices:: { - ma: lms::meta::VonKries::xyz_to_lms_matrix(), - inv_ma: lms::meta::VonKries::lms_to_xyz_matrix(), + ma: lms::matrix::VonKries::xyz_to_lms_matrix(), + inv_ma: lms::matrix::VonKries::lms_to_xyz_matrix(), } } Method::XyzScaling => { ConeResponseMatrices:: { - ma: lms::meta::UnitMatrix::xyz_to_lms_matrix(), - inv_ma: lms::meta::UnitMatrix::lms_to_xyz_matrix(), + ma: lms::matrix::UnitMatrix::xyz_to_lms_matrix(), + inv_ma: lms::matrix::UnitMatrix::lms_to_xyz_matrix(), } } } @@ -123,6 +441,11 @@ where /// /// Converts a color from the source white point (Swp) to the destination white /// point (Dwp). Uses the bradford method for conversion by default. +#[deprecated( + since = "0.7.7", + note = "replaced by `palette::chromatic_adaptation::AdaptFromUnclamped`" +)] +#[allow(deprecated)] pub trait AdaptFrom: Sized where T: Real + Zero + Arithmetics + Clone, @@ -142,6 +465,7 @@ where fn adapt_from_using>(color: S, method: M) -> Self; } +#[allow(deprecated)] impl AdaptFrom for D where T: Real + Zero + Arithmetics + Clone, @@ -163,6 +487,11 @@ where /// /// Converts a color with the source white point (Swp) into the destination /// white point (Dwp). Uses the bradford method for conversion by default. +#[deprecated( + since = "0.7.7", + note = "replaced by `palette::chromatic_adaptation::AdaptIntoUnclamped`" +)] +#[allow(deprecated)] pub trait AdaptInto: Sized where T: Real + Zero + Arithmetics + Clone, @@ -182,6 +511,7 @@ where fn adapt_into_using>(self, method: M) -> D; } +#[allow(deprecated)] impl AdaptInto for S where T: Real + Zero + Arithmetics + Clone, @@ -198,9 +528,17 @@ where #[cfg(feature = "approx")] #[cfg(test)] mod test { + #![allow(deprecated)] + use super::{AdaptFrom, AdaptInto, Method, TransformMatrix}; - use crate::white_point::{WhitePoint, A, C, D50, D65}; - use crate::Xyz; + use crate::{ + encoding::{Linear, Srgb}, + Xyz, + }; + use crate::{ + rgb::Rgb, + white_point::{WhitePoint, A, C, D50, D65}, + }; #[test] fn d65_to_d50_matrix_xyz_scaling() { @@ -274,4 +612,13 @@ mod test { let computed_xyz_scaling: Xyz = input_a.adapt_into_using(Method::XyzScaling); assert_relative_eq!(expected_xyz_scaling, computed_xyz_scaling, epsilon = 0.0001); } + + #[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/lms.rs b/palette/src/lms.rs index 2328a60b5..94c513e60 100644 --- a/palette/src/lms.rs +++ b/palette/src/lms.rs @@ -3,20 +3,20 @@ #[allow(clippy::module_inception)] mod lms; -pub mod meta; +pub mod matrix; use crate::Alpha; pub use self::lms::*; /// LMS that uses the von Kries matrix. -pub type VonKriesLms = Lms, T>; +pub type VonKriesLms = Lms, T>; /// LMSA that uses the von Kries matrix. -pub type VonKriesLmsa = Alpha, T>, T>; +pub type VonKriesLmsa = Alpha, T>, T>; /// LMS that uses the Bradford matrix. -pub type BradfordLms = Lms, T>; +pub type BradfordLms = Lms, T>; /// LMSA that uses the Bradford matrix. -pub type BradfordLmsa = Alpha, T>, T>; +pub type BradfordLmsa = Alpha, T>, T>; diff --git a/palette/src/lms/lms.rs b/palette/src/lms/lms.rs index bfec6b797..afb440521 100644 --- a/palette/src/lms/lms.rs +++ b/palette/src/lms/lms.rs @@ -9,7 +9,7 @@ use crate::{ Alpha, Xyz, }; -use super::meta::{HasLmsMatrix, XyzToLms}; +use super::matrix::{HasLmsMatrix, XyzToLms}; /// Generic LMS with an alpha component. See [`Lmsa` implementation in /// `Alpha`][crate::Alpha#Lmsa]. @@ -33,15 +33,15 @@ pub type Lmsa = Alpha, T>; /// # Creating a Value /// /// An LMS value is often derived from another color space, through a conversion -/// matrix. Two such matrices are [`VonKries`][super::meta::VonKries] and -/// [`Bradford`][super::meta::Bradford], and Palette offers type aliases in the +/// matrix. Two such matrices are [`VonKries`][super::matrix::VonKries] and +/// [`Bradford`][super::matrix::Bradford], and Palette offers type aliases in the /// [`lms`][crate::lms] module to make using them a bit more convenient. It's of /// course also possible to simply use [`Lms::new`], but it may not be as /// intuitive. /// /// ``` /// use palette::{ -/// lms::{Lms, VonKriesLms, meta::VonKries}, +/// lms::{Lms, VonKriesLms, matrix::VonKries}, /// white_point::D65, /// Srgb, FromColor /// }; diff --git a/palette/src/lms/meta.rs b/palette/src/lms/matrix.rs similarity index 98% rename from palette/src/lms/meta.rs rename to palette/src/lms/matrix.rs index 693666b41..04ccb8fb0 100644 --- a/palette/src/lms/meta.rs +++ b/palette/src/lms/matrix.rs @@ -1,4 +1,4 @@ -//! Meta types and traits for [`Lms`][super::Lms]. +//! Matrix types and traits for [`Lms`][super::Lms]. use core::marker::PhantomData; diff --git a/palette/src/xyz.rs b/palette/src/xyz.rs index 751615a21..8567fb169 100644 --- a/palette/src/xyz.rs +++ b/palette/src/xyz.rs @@ -2,15 +2,20 @@ pub mod meta; -use core::{marker::PhantomData, ops::Mul}; +use core::{ + any::TypeId, + marker::PhantomData, + ops::{Div, Mul}, +}; use crate::{ bool_mask::{HasBoolMask, LazySelect}, cam16::{FromCam16Unclamped, WhitePointParameter}, + chromatic_adaptation::{adaptation_matrix, AdaptFromUnclamped}, convert::{ConvertOnce, FromColorUnclamped, IntoColorUnclamped, Matrix3}, encoding::{linear::LinearFn, IntoLinear, Linear}, lms::{ - meta::{HasLmsMatrix, LmsToXyz}, + matrix::{HasLmsMatrix, LmsToXyz, XyzToLms}, Lms, }, luma::LumaStandard, @@ -89,6 +94,18 @@ impl Xyz { Self::new(x, y, z) } + /// Normalize `y` to `1.0`. + /// + /// May produce unexpected values if `y` is `0.0`. + pub fn normalize(self) -> Self + where + T: Div + Clone, + { + let y = self.y.clone(); + + self / y + } + /// Changes the reference white point without changing the color value. /// /// This function doesn't change the numerical values, and thus the color it @@ -222,6 +239,27 @@ impl FromColorUnclamped> for Xyz { } } +impl AdaptFromUnclamped> for Xyz +where + T: Zero + Arithmetics + Clone, + Wp1: WhitePoint + HasXyzMeta, + Wp2: WhitePoint + HasXyzMeta, +{ + type Scalar = T; + + #[inline] + fn adapt_from_unclamped_with(input: Xyz) -> Self + where + M: LmsToXyz + XyzToLms, + { + if TypeId::of::() != TypeId::of::() { + adaptation_matrix::(None, None).convert_once(input) + } else { + input.with_white_point() + } + } +} + impl FromColorUnclamped> for Xyz where T: Arithmetics + FromScalar,