From aee28d4b065e6480c583c572cc005503049348f9 Mon Sep 17 00:00:00 2001 From: Thomas Churchman Date: Tue, 5 Nov 2024 16:22:09 +0100 Subject: [PATCH] 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 34465fc..90b2f34 100644 --- a/color/src/colorspace.rs +++ b/color/src/colorspace.rs @@ -58,6 +58,21 @@ pub trait Colorspace: Clone + Copy + 'static { let scaled = LinearSrgb::scale_chroma(rgb, scale); Self::from_linear_srgb(scaled) } + + /// 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 colorspace, particularly the hue channel. @@ -122,6 +137,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 @@ -154,6 +177,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)] @@ -179,6 +210,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)] @@ -206,6 +245,10 @@ impl Colorspace for XyzD65 { ]; matmul(&LINEAR_SRGB_TO_XYZ, src) } + + fn clip(src: [f32; 3]) -> [f32; 3] { + src + } } #[derive(Clone, Copy, Debug)] @@ -252,6 +295,10 @@ impl Colorspace for Oklab { fn scale_chroma(src: [f32; 3], scale: f32) -> [f32; 3] { [src[0], src[1] * scale, src[2] * scale] } + + fn clip(src: [f32; 3]) -> [f32; 3] { + [src[0].clamp(0., 1.), src[1], src[2]] + } } #[derive(Clone, Copy, Debug)] @@ -284,4 +331,8 @@ impl Colorspace for Oklch { fn scale_chroma(src: [f32; 3], scale: f32) -> [f32; 3] { [src[0], src[1] * scale, src[2]] } + + 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 b080f8e..6c09723 100644 --- a/color/src/css.rs +++ b/color/src/css.rs @@ -124,6 +124,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 dc3e9d9..d645151 100644 --- a/color/src/tagged.rs +++ b/color/src/tagged.rs @@ -160,6 +160,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 {