Skip to content

Commit

Permalink
Add methods to map color components (#9)
Browse files Browse the repository at this point in the history
These are convenience methods, but should provide a nice concise way to
express common operations on colors, subsuming a lot of single-use
methods.

Arguably 'map_lightness' should become a method on `ColorSpace`, very
similar to `scale_chroma`, as similar math optimizations apply.
  • Loading branch information
raphlinus authored Nov 6, 2024
1 parent e31d125 commit e824f67
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 13 deletions.
67 changes: 64 additions & 3 deletions color/src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use core::any::TypeId;
use core::marker::PhantomData;

use crate::{ColorSpace, ColorSpaceLayout};
use crate::{ColorSpace, ColorSpaceLayout, ColorSpaceTag, Oklab};

#[cfg(all(not(feature = "std"), not(test)))]
use crate::floatfuncs::FloatFuncs;
Expand Down Expand Up @@ -179,9 +179,40 @@ impl<CS: ColorSpace> OpaqueColor<CS> {
///
/// This can be useful for choosing contrasting colors, and follows the
/// WCAG 2.1 spec.
#[must_use]
pub fn relative_luminance(self) -> f32 {
let rgb = CS::to_linear_srgb(self.components);
0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]
let [r, g, b] = CS::to_linear_srgb(self.components);
0.2126 * r + 0.7152 * g + 0.0722 * b
}

/// Map components.
#[must_use]
pub fn map(self, f: impl Fn(f32, f32, f32) -> [f32; 3]) -> Self {
let [x, y, z] = self.components;
Self::new(f(x, y, z))
}

/// Map components in a given color space.
#[must_use]
pub fn map_in<TargetCS: ColorSpace>(self, f: impl Fn(f32, f32, f32) -> [f32; 3]) -> Self {
self.convert::<TargetCS>().map(f).convert()
}

/// Map the lightness of the color.
///
/// In a color space that naturally has a lightness component, map that value.
/// Otherwise, do the mapping in Oklab. The lightness range is normalized so
/// that 1.0 is white.
#[must_use]
pub fn map_lightness(self, f: impl Fn(f32) -> f32) -> Self {
match CS::TAG {
Some(ColorSpaceTag::Oklab)
| Some(ColorSpaceTag::Oklch)
| Some(ColorSpaceTag::Lab)
| Some(ColorSpaceTag::Lch) => self.map(|l, c1, c2| [f(l), c1, c2]),
Some(ColorSpaceTag::Hsl) => self.map(|h, s, l| [h, s, 100.0 * f(l * 0.01)]),
_ => self.map_in::<Oklab>(|l, a, b| [f(l), a, b]),
}
}
}

Expand Down Expand Up @@ -249,6 +280,36 @@ impl<CS: ColorSpace> AlphaColor<CS> {
let (opaque, alpha) = split_alpha(self.components);
Self::new(add_alpha(CS::scale_chroma(opaque, scale), alpha))
}

/// Map components.
#[must_use]
pub fn map(self, f: impl Fn(f32, f32, f32, f32) -> [f32; 4]) -> Self {
let [x, y, z, a] = self.components;
Self::new(f(x, y, z, a))
}

/// Map components in a given color space.
#[must_use]
pub fn map_in<TargetCS: ColorSpace>(self, f: impl Fn(f32, f32, f32, f32) -> [f32; 4]) -> Self {
self.convert::<TargetCS>().map(f).convert()
}

/// Map the lightness of the color.
///
/// In a color space that naturally has a lightness component, map that value.
/// Otherwise, do the mapping in Oklab. The lightness range is normalized so
/// that 1.0 is white.
#[must_use]
pub fn map_lightness(self, f: impl Fn(f32) -> f32) -> Self {
match CS::TAG {
Some(ColorSpaceTag::Oklab)
| Some(ColorSpaceTag::Oklch)
| Some(ColorSpaceTag::Lab)
| Some(ColorSpaceTag::Lch) => self.map(|l, c1, c2, a| [f(l), c1, c2, a]),
Some(ColorSpaceTag::Hsl) => self.map(|h, s, l, a| [h, s, 100.0 * f(l * 0.01), a]),
_ => self.map_in::<Oklab>(|l, a, b, alpha| [f(l), a, b, alpha]),
}
}
}

impl<CS: ColorSpace> PremulColor<CS> {
Expand Down
61 changes: 51 additions & 10 deletions color/src/css.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,25 +106,30 @@ impl CssColor {
}
}

/// Scale the chroma by the given amount.
///
/// See [`ColorSpace::scale_chroma`] for more details.
#[must_use]
pub fn scale_chroma(self, scale: f32) -> Self {
let (opaque, alpha) = split_alpha(self.components);
let mut components = self.cs.scale_chroma(opaque, scale);
fn zero_missing_components(mut self) -> Self {
if self.missing.any() {
for (i, component) in components.iter_mut().enumerate() {
for (i, component) in self.components.iter_mut().enumerate() {
if self.missing.contains(i) {
*component = 0.0;
}
}
}
self
}

/// Scale the chroma by the given amount.
///
/// See [`ColorSpace::scale_chroma`] for more details.
#[must_use]
pub fn scale_chroma(self, scale: f32) -> Self {
let (opaque, alpha) = split_alpha(self.components);
let components = self.cs.scale_chroma(opaque, scale);
Self {
cs: self.cs,
missing: self.missing,
components: add_alpha(components, alpha),
}
.zero_missing_components()
}

/// Clip the color's components to fit within the natural gamut of the color space, and clamp
Expand Down Expand Up @@ -217,9 +222,45 @@ impl CssColor {
/// Note that this method only considers the opaque color, not the alpha.
/// Blending semi-transparent colors will reduce contrast, and that
/// should also be taken into account.
#[must_use]
pub fn relative_luminance(self) -> f32 {
let rgb = self.convert(ColorSpaceTag::LinearSrgb).components;
0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]
let [r, g, b, _] = self.convert(ColorSpaceTag::LinearSrgb).components;
0.2126 * r + 0.7152 * g + 0.0722 * b
}

/// Map components.
#[must_use]
pub fn map(self, f: impl Fn(f32, f32, f32, f32) -> [f32; 4]) -> Self {
let [x, y, z, a] = self.components;
Self {
cs: self.cs,
missing: self.missing,
components: f(x, y, z, a),
}
.zero_missing_components()
}

/// Map components in a given color space.
#[must_use]
pub fn map_in(self, cs: ColorSpaceTag, f: impl Fn(f32, f32, f32, f32) -> [f32; 4]) -> Self {
self.convert(cs).map(f).convert(self.cs)
}

/// Map the lightness of the color.
///
/// In a color space that naturally has a lightness component, map that value.
/// Otherwise, do the mapping in Oklab. The lightness range is normalized so
/// that 1.0 is white.
#[must_use]
pub fn map_lightness(self, f: impl Fn(f32) -> f32) -> Self {
match self.cs {
ColorSpaceTag::Oklab
| ColorSpaceTag::Oklch
| ColorSpaceTag::Lab
| ColorSpaceTag::Lch => self.map(|l, c1, c2, a| [f(l), c1, c2, a]),
ColorSpaceTag::Hsl => self.map(|h, s, l, a| [h, s, 100.0 * f(l * 0.01), a]),
_ => self.map_in(ColorSpaceTag::Oklab, |l, a, b, alpha| [f(l), a, b, alpha]),
}
}
}

Expand Down

0 comments on commit e824f67

Please sign in to comment.