Skip to content

Commit

Permalink
Add color clipping (#6)
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
tomcur authored Nov 6, 2024
1 parent 3c169ab commit e31d125
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 0 deletions.
44 changes: 44 additions & 0 deletions color/src/colorspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)]
Expand All @@ -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)]
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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]
}
}
16 changes: 16 additions & 0 deletions color/src/css.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 15 additions & 0 deletions color/src/tagged.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit e31d125

Please sign in to comment.