Skip to content

Commit

Permalink
Add color clipping
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 committed Nov 5, 2024
1 parent f926fc1 commit aee28d4
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 0 deletions.
51 changes: 51 additions & 0 deletions color/src/colorspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)]
Expand All @@ -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)]
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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]]
}
}
14 changes: 14 additions & 0 deletions color/src/css.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 15 additions & 0 deletions color/src/tagged.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit aee28d4

Please sign in to comment.