From bd3f1a03b3d733e636aec7bc78ba937111eb95e6 Mon Sep 17 00:00:00 2001 From: Thomas Churchman Date: Tue, 5 Nov 2024 16:22:09 +0100 Subject: [PATCH 1/4] Add color clipping Clipping colors to colorspaces' bounds is useful for e.g. final display or as part of gamut mapping. A related method would be `Colorspace::is_in_bounds(src: [f32; 3])`, but I'm undecided whether that's useful enough to include. It could have a default implementation (`src == Self::clip(src)`). Roughly the same considerations hold for a const `Colorspace::IS_(UN)BOUNDED`. --- color/src/colorspace.rs | 51 +++++++++++++++++++++++++++++++++++++++++ color/src/css.rs | 14 +++++++++++ color/src/tagged.rs | 15 ++++++++++++ 3 files changed, 80 insertions(+) diff --git a/color/src/colorspace.rs b/color/src/colorspace.rs index 914a87f..40e90ea 100644 --- a/color/src/colorspace.rs +++ b/color/src/colorspace.rs @@ -75,6 +75,21 @@ pub trait ColorSpace: Clone + Copy + 'static { TargetCS::from_linear_srgb(lin_rgb) } } + + /// Clip the color's components to the range allowed by the colorspace. + /// + /// The resultant color is guaranteed to be inside the bounds (and thus gamut) of the + /// colorspace, but may be perceptually quite distinct from the original color. + /// + /// # Examples + /// + /// ```rust + /// use color::{Colorspace, Srgb, XyzD65}; + /// + /// assert_eq!(Srgb::clip([0.4, -0.2, 1.2]), [0.4, 0., 1.]); + /// assert_eq!(XyzD65::clip([0.4, -0.2, 1.2]), [0.4, -0.2, 1.2]); + /// ``` + fn clip(src: [f32; 3]) -> [f32; 3]; } /// The layout of a color space, particularly the hue channel. @@ -139,6 +154,14 @@ impl ColorSpace for LinearSrgb { ]; matmul(&OKLAB_LMS_TO_SRGB, lms_scaled.map(|x| x * x * x)) } + + fn clip(src: [f32; 3]) -> [f32; 3] { + [ + src[0].clamp(0., 1.), + src[1].clamp(0., 1.), + src[2].clamp(0., 1.), + ] + } } // It might be a better idea to write custom debug impls for AlphaColor and friends @@ -171,6 +194,14 @@ impl ColorSpace for Srgb { fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] { src.map(lin_to_srgb) } + + fn clip(src: [f32; 3]) -> [f32; 3] { + [ + src[0].clamp(0., 1.), + src[1].clamp(0., 1.), + src[2].clamp(0., 1.), + ] + } } #[derive(Clone, Copy, Debug)] @@ -196,6 +227,14 @@ impl ColorSpace for DisplayP3 { ]; matmul(&LINEAR_SRGB_TO_DISPLAYP3, src).map(lin_to_srgb) } + + fn clip(src: [f32; 3]) -> [f32; 3] { + [ + src[0].clamp(0., 1.), + src[1].clamp(0., 1.), + src[2].clamp(0., 1.), + ] + } } #[derive(Clone, Copy, Debug)] @@ -223,6 +262,10 @@ impl ColorSpace for XyzD65 { ]; matmul(&LINEAR_SRGB_TO_XYZ, src) } + + fn clip(src: [f32; 3]) -> [f32; 3] { + src + } } #[derive(Clone, Copy, Debug)] @@ -282,6 +325,10 @@ impl ColorSpace for Oklab { TargetCS::from_linear_srgb(lin_rgb) } } + + fn clip(src: [f32; 3]) -> [f32; 3] { + [src[0].clamp(0., 1.), src[1], src[2]] + } } /// Rectangular to cylindrical conversion. @@ -332,4 +379,8 @@ impl ColorSpace for Oklch { TargetCS::from_linear_srgb(lin_rgb) } } + + fn clip(src: [f32; 3]) -> [f32; 3] { + [src[0].clamp(0., 1.), src[1].max(0.), src[2]] + } } diff --git a/color/src/css.rs b/color/src/css.rs index 47a3bee..12ae919 100644 --- a/color/src/css.rs +++ b/color/src/css.rs @@ -127,6 +127,20 @@ impl CssColor { } } + /// Clip the color's components to the range allowed by the colorspace. + /// + /// See [`Colorspace::clip`] for more details. + #[must_use] + pub fn clip(self) -> Self { + let (opaque, alpha) = split_alpha(self.components); + let components = self.cs.clip(opaque); + Self { + cs: self.cs, + missing: self.missing, + components: add_alpha(components, alpha), + } + } + fn premultiply_split(self) -> ([f32; 3], f32) { // Reference: ยง12.3 of Color 4 spec let (opaque, alpha) = split_alpha(self.components); diff --git a/color/src/tagged.rs b/color/src/tagged.rs index 9118759..3577e9c 100644 --- a/color/src/tagged.rs +++ b/color/src/tagged.rs @@ -178,6 +178,21 @@ impl ColorSpaceTag { } } } + + /// Clip the color's components to the range allowed by the colorspace. + /// + /// See [`Colorspace::clip`] for more details. + pub fn clip(self, src: [f32; 3]) -> [f32; 3] { + match self { + Self::Srgb => Srgb::clip(src), + Self::LinearSrgb => LinearSrgb::clip(src), + Self::Oklab => Oklab::clip(src), + Self::Oklch => Oklch::clip(src), + Self::DisplayP3 => DisplayP3::clip(src), + Self::XyzD65 => XyzD65::clip(src), + _ => todo!(), + } + } } impl TaggedColor { From 220c61222e1ed75a866721b2fb31b5fb58e848c5 Mon Sep 17 00:00:00 2001 From: Thomas Churchman Date: Wed, 6 Nov 2024 15:14:21 +0100 Subject: [PATCH 2/4] Style --- color/src/colorspace.rs | 44 +++++++++++++++-------------------------- color/src/css.rs | 4 ++-- color/src/tagged.rs | 4 ++-- 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/color/src/colorspace.rs b/color/src/colorspace.rs index 40e90ea..6d9e391 100644 --- a/color/src/colorspace.rs +++ b/color/src/colorspace.rs @@ -76,15 +76,15 @@ pub trait ColorSpace: Clone + Copy + 'static { } } - /// Clip the color's components to the range allowed by the colorspace. + /// Clip the color's components to the range allowed by the color space. /// - /// The resultant color is guaranteed to be inside the bounds (and thus gamut) of the - /// colorspace, but may be perceptually quite distinct from the original color. + /// The resultant color is guaranteed to be inside the bounds (and thus gamut) of the color + /// space, but may be perceptually quite distinct from the original color. /// /// # Examples /// /// ```rust - /// use color::{Colorspace, Srgb, XyzD65}; + /// use color::{ColorSpace, Srgb, XyzD65}; /// /// assert_eq!(Srgb::clip([0.4, -0.2, 1.2]), [0.4, 0., 1.]); /// assert_eq!(XyzD65::clip([0.4, -0.2, 1.2]), [0.4, -0.2, 1.2]); @@ -155,12 +155,8 @@ impl ColorSpace for LinearSrgb { matmul(&OKLAB_LMS_TO_SRGB, lms_scaled.map(|x| x * x * x)) } - fn clip(src: [f32; 3]) -> [f32; 3] { - [ - src[0].clamp(0., 1.), - src[1].clamp(0., 1.), - src[2].clamp(0., 1.), - ] + fn clip([r, g, b]: [f32; 3]) -> [f32; 3] { + [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)] } } @@ -195,12 +191,8 @@ impl ColorSpace for Srgb { src.map(lin_to_srgb) } - fn clip(src: [f32; 3]) -> [f32; 3] { - [ - src[0].clamp(0., 1.), - src[1].clamp(0., 1.), - src[2].clamp(0., 1.), - ] + fn clip([r, g, b]: [f32; 3]) -> [f32; 3] { + [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)] } } @@ -228,12 +220,8 @@ impl ColorSpace for DisplayP3 { matmul(&LINEAR_SRGB_TO_DISPLAYP3, src).map(lin_to_srgb) } - fn clip(src: [f32; 3]) -> [f32; 3] { - [ - src[0].clamp(0., 1.), - src[1].clamp(0., 1.), - src[2].clamp(0., 1.), - ] + fn clip([r, g, b]: [f32; 3]) -> [f32; 3] { + [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)] } } @@ -263,8 +251,8 @@ impl ColorSpace for XyzD65 { matmul(&LINEAR_SRGB_TO_XYZ, src) } - fn clip(src: [f32; 3]) -> [f32; 3] { - src + fn clip([x, y, z]: [f32; 3]) -> [f32; 3] { + [x, y, z] } } @@ -326,8 +314,8 @@ impl ColorSpace for Oklab { } } - fn clip(src: [f32; 3]) -> [f32; 3] { - [src[0].clamp(0., 1.), src[1], src[2]] + fn clip([l, a, b]: [f32; 3]) -> [f32; 3] { + [l.clamp(0., 1.), a, b] } } @@ -380,7 +368,7 @@ impl ColorSpace for Oklch { } } - fn clip(src: [f32; 3]) -> [f32; 3] { - [src[0].clamp(0., 1.), src[1].max(0.), src[2]] + fn clip([l, c, h]: [f32; 3]) -> [f32; 3] { + [l.clamp(0., 1.), c.max(0.), h] } } diff --git a/color/src/css.rs b/color/src/css.rs index 12ae919..a0e414c 100644 --- a/color/src/css.rs +++ b/color/src/css.rs @@ -127,9 +127,9 @@ impl CssColor { } } - /// Clip the color's components to the range allowed by the colorspace. + /// Clip the color's components to the range allowed by the color space. /// - /// See [`Colorspace::clip`] for more details. + /// See [`ColorSpace::clip`] for more details. #[must_use] pub fn clip(self) -> Self { let (opaque, alpha) = split_alpha(self.components); diff --git a/color/src/tagged.rs b/color/src/tagged.rs index 3577e9c..cd3e4f1 100644 --- a/color/src/tagged.rs +++ b/color/src/tagged.rs @@ -179,9 +179,9 @@ impl ColorSpaceTag { } } - /// Clip the color's components to the range allowed by the colorspace. + /// Clip the color's components to the range allowed by the color space. /// - /// See [`Colorspace::clip`] for more details. + /// See [`ColorSpace::clip`] for more details. pub fn clip(self, src: [f32; 3]) -> [f32; 3] { match self { Self::Srgb => Srgb::clip(src), From 0561276042b5ddb2475b1d823a7734e774c7e9d9 Mon Sep 17 00:00:00 2001 From: Thomas Churchman Date: Wed, 6 Nov 2024 15:20:30 +0100 Subject: [PATCH 3/4] Language --- color/src/colorspace.rs | 11 ++++++++--- color/src/css.rs | 2 +- color/src/tagged.rs | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/color/src/colorspace.rs b/color/src/colorspace.rs index 6d9e391..46b4422 100644 --- a/color/src/colorspace.rs +++ b/color/src/colorspace.rs @@ -76,10 +76,15 @@ pub trait ColorSpace: Clone + Copy + 'static { } } - /// Clip the color's components to the range allowed by the color space. + /// Clip the color's components to fit within the natural gamut of the color space. /// - /// The resultant color is guaranteed to be inside the bounds (and thus gamut) of the color - /// space, but may be perceptually quite distinct from the original color. + /// There are many possible ways to map colors outside of a color space's gamut to colors + /// inside the gamut. Some methods are perceptually better than others (for example, preserving + /// the mapped color's hue is usually preferred over preserving saturation). This method will + /// generally do the mathematically simplest thing, namely clamping the individual color + /// components' values to the color space's natural limits of those components, bringing + /// out-of-gamut colors just onto the gamut boundary. The resultant color may be perceptually + /// quite distinct from the original color. /// /// # Examples /// diff --git a/color/src/css.rs b/color/src/css.rs index a0e414c..48be405 100644 --- a/color/src/css.rs +++ b/color/src/css.rs @@ -127,7 +127,7 @@ impl CssColor { } } - /// Clip the color's components to the range allowed by the color space. + /// Clip the color's components to fit within the natural gamut of the color space. /// /// See [`ColorSpace::clip`] for more details. #[must_use] diff --git a/color/src/tagged.rs b/color/src/tagged.rs index cd3e4f1..a7e74e9 100644 --- a/color/src/tagged.rs +++ b/color/src/tagged.rs @@ -179,7 +179,7 @@ impl ColorSpaceTag { } } - /// Clip the color's components to the range allowed by the color space. + /// Clip the color's components to fit within the natural gamut of the color space. /// /// See [`ColorSpace::clip`] for more details. pub fn clip(self, src: [f32; 3]) -> [f32; 3] { From 79834d9a2ed83b2078b6fb75840dc4d7f486019d Mon Sep 17 00:00:00 2001 From: Thomas Churchman Date: Wed, 6 Nov 2024 15:23:27 +0100 Subject: [PATCH 4/4] Clamp alpha --- color/src/css.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/color/src/css.rs b/color/src/css.rs index 48be405..1076709 100644 --- a/color/src/css.rs +++ b/color/src/css.rs @@ -127,13 +127,15 @@ impl CssColor { } } - /// Clip the color's components to fit within the natural gamut of the color space. + /// Clip the color's components to fit within the natural gamut of the color space, and clamp + /// the color's alpha to be in the range `[0, 1]`. /// /// See [`ColorSpace::clip`] for more details. #[must_use] pub fn clip(self) -> Self { let (opaque, alpha) = split_alpha(self.components); let components = self.cs.clip(opaque); + let alpha = alpha.clamp(0., 1.); Self { cs: self.cs, missing: self.missing,