diff --git a/color/src/colorspace.rs b/color/src/colorspace.rs index 914a87f..46b4422 100644 --- a/color/src/colorspace.rs +++ b/color/src/colorspace.rs @@ -75,6 +75,26 @@ pub trait ColorSpace: Clone + Copy + 'static { TargetCS::from_linear_srgb(lin_rgb) } } + + /// Clip the color's components to fit within the natural gamut of the color space. + /// + /// 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 + /// + /// ```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 +159,10 @@ impl ColorSpace for LinearSrgb { ]; matmul(&OKLAB_LMS_TO_SRGB, lms_scaled.map(|x| x * x * x)) } + + fn clip([r, g, b]: [f32; 3]) -> [f32; 3] { + [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)] + } } // It might be a better idea to write custom debug impls for AlphaColor and friends @@ -171,6 +195,10 @@ impl ColorSpace for Srgb { fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] { src.map(lin_to_srgb) } + + fn clip([r, g, b]: [f32; 3]) -> [f32; 3] { + [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)] + } } #[derive(Clone, Copy, Debug)] @@ -196,6 +224,10 @@ impl ColorSpace for DisplayP3 { ]; matmul(&LINEAR_SRGB_TO_DISPLAYP3, src).map(lin_to_srgb) } + + fn clip([r, g, b]: [f32; 3]) -> [f32; 3] { + [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)] + } } #[derive(Clone, Copy, Debug)] @@ -223,6 +255,10 @@ impl ColorSpace for XyzD65 { ]; matmul(&LINEAR_SRGB_TO_XYZ, src) } + + fn clip([x, y, z]: [f32; 3]) -> [f32; 3] { + [x, y, z] + } } #[derive(Clone, Copy, Debug)] @@ -282,6 +318,10 @@ impl ColorSpace for Oklab { TargetCS::from_linear_srgb(lin_rgb) } } + + fn clip([l, a, b]: [f32; 3]) -> [f32; 3] { + [l.clamp(0., 1.), a, b] + } } /// Rectangular to cylindrical conversion. @@ -332,4 +372,8 @@ impl ColorSpace for Oklch { TargetCS::from_linear_srgb(lin_rgb) } } + + 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 47a3bee..1076709 100644 --- a/color/src/css.rs +++ b/color/src/css.rs @@ -127,6 +127,22 @@ impl CssColor { } } + /// 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, + 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..a7e74e9 100644 --- a/color/src/tagged.rs +++ b/color/src/tagged.rs @@ -178,6 +178,21 @@ impl ColorSpaceTag { } } } + + /// 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] { + 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 {