Skip to content

Commit

Permalink
factor normalization into three functions and expose mut variant for …
Browse files Browse the repository at this point in the history
…first two
  • Loading branch information
apparebit committed Jun 23, 2024
1 parent 90d43a8 commit bead83c
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 46 deletions.
66 changes: 48 additions & 18 deletions src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ pub(crate) mod core;

use self::core::{
clip, convert, delta_e_ok_internal, from_24_bit, in_gamut, interpolate, map_to_gamut,
normalize, normalize_eq, prepare_to_interpolate, scale_lightness, to_24_bit, to_contrast,
to_contrast_luminance, P3_CONTRAST, SRGB_CONTRAST,
normalize_eq, normalize_nan_mut, normalize_range_mut, prepare_to_interpolate, scale_lightness,
to_24_bit, to_contrast, to_contrast_luminance, P3_CONTRAST, SRGB_CONTRAST,
};
pub use self::core::{ColorSpace, HueInterpolation, OkVersion};
use super::parser::{format, parse};
Expand Down Expand Up @@ -65,6 +65,26 @@ impl Interpolator {

/// A high-resolution color object.
///
/// Every color object has a color space and three coordinates.
///
/// # Color Coordinates
///
/// For RGB color spaces, the coordinates of in-gamut colors have unit range.
///
/// So does the (revised) lightness for Oklab et al. Meanwhile, a/b may be
/// negative or positive, have no a-priori limits, but are contained by
/// `-0.4..=0.4` for all practical purposes. Chroma is non-negative, with no
/// a-priori upper limit, but is contained by `0.0..=0.4` for all practical
/// purposes. Finally, hue ranges `0..=360`.
///
/// A coordinate may be not-a-number indicating either a [powerless
/// component](https://www.w3.org/TR/css-color-4/#powerless), i.e., a component
/// that does not contribute towards the color such as the hue for Oklch colors
/// with zero chroma, or a [missing
/// component](https://www.w3.org/TR/css-color-4/#missing), i.e., a component
/// intentionally omitted by the user, notably for interpolation.
///
///
/// # Managing Gamut
///
/// Color objects have a uniform representation for all supported color spaces,
Expand Down Expand Up @@ -350,20 +370,27 @@ impl Color {

// ----------------------------------------------------------------------------------------------------------------

/// Normalize this color's coordinates.
/// Normalize not-a-number coordinates by zeroing them.
///
/// This method ensures that all coordinates are well-formed. To that end,
/// it replaces missing components, that is, not-a-number, with 0.0 and
/// clamps some Ok*** coordinates. In particular, it clamps (revised)
/// lightness to `0.0..=1.0`, chroma to `0.0..`, and hue in `0.0..=360.0`.
/// If the hue for Oklch/Oklrch is not-a-number, this method also zeros out
/// the chroma.
#[inline]
#[must_use = "method returns owned color and does not mutate original value"]
pub fn normalize_nan(mut self) -> Self {
normalize_nan_mut(self.space, &mut self.coordinates);
self
}

/// Normalize coordinate ranges.
///
/// This is a weaker form of normalization than that performed for equality
/// testing and hashing.
pub fn normalize(&self) -> Self {
Self {
space: self.space,
coordinates: normalize(self.space, &self.coordinates).data,
}
/// This method clamps (revised) lightness and chroma to the ranges
/// `0.0..=1.0` and `0.0..`, respectively. It also folds hue to the range
/// `0..360`.
#[inline]
#[must_use = "method returns owned color and does not mutate original value"]
pub fn normalize_range(mut self) -> Self {
normalize_range_mut(self.space, &mut self.coordinates);
self
}

// ----------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -397,6 +424,8 @@ impl Color {
/// <div style="background-color: #bd9547;"></div>
/// </div>
#[allow(non_snake_case)]
#[inline]
#[must_use = "method returns new color and does not mutate original value"]
pub fn lighten(&self, factor: f64) -> Color {
Color {
space: ColorSpace::Oklrch,
Expand All @@ -409,6 +438,7 @@ impl Color {
/// Since darkening by some factor is just lightening by the inverse, this
/// method delegates to [`Color::lighten`] with just that value.
#[inline]
#[must_use = "method returns new color and does not mutate original value"]
pub fn darken(&self, factor: f64) -> Color {
Color {
space: ColorSpace::Oklrch,
Expand Down Expand Up @@ -624,7 +654,7 @@ impl Color {
/// </div>
pub fn find_closest_ok<'c, C>(&self, candidates: C, version: OkVersion) -> Option<usize>
where
C: IntoIterator<Item = &'c Color>,
C: IntoIterator<Item = &'c Self>,
{
self.find_closest(candidates, version.cartesian_space(), delta_e_ok_internal)
}
Expand Down Expand Up @@ -747,10 +777,10 @@ impl Color {
///
/// Note that the arcs between the purple and orange are not strictly
/// circular because they need to make up for the difference in chroma,
/// i.e., 0.18546 vs 0.15466.
/// 0.18546 vs 0.15466, in addition to the difference in hue,
pub fn interpolate(
&self,
color: &Color,
color: &Self,
interpolation_space: ColorSpace,
interpolation_strategy: HueInterpolation,
) -> Interpolator {
Expand All @@ -767,7 +797,7 @@ impl Color {
/// algorithm that is surprisingly similar to the [Accessible Perceptual
/// Contrast Algorithm](https://github.com/Myndex/apca-w3), version
/// 0.0.98G-4g.
pub fn contrast_against(&self, background: Self) -> f64 {
pub fn contrast_against(&self, background: &Self) -> f64 {
let fg = self.to(ColorSpace::Srgb);
let bg = background.to(ColorSpace::Srgb);

Expand Down
54 changes: 29 additions & 25 deletions src/color/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,12 @@ pub extern "C" fn to_24_bit(coordinates: &[f64; 3]) -> ArrayData<u8, 3> {
// Normalization, Equality, and Difference
// ====================================================================================================================

/// Update not-a-number coordinates to their normalized representation.
fn normalize_domain_mut(space: ColorSpace, coordinates: &mut [f64; 3]) {
/// Normalize not-a-numbers [*core-only*].
///
/// This function zeros non-a-number coordinates. If the hue for Oklch/Oklrch is
/// not-a-number, it also zeros out chroma.
#[no_mangle]
pub extern "C" fn normalize_nan_mut(space: ColorSpace, coordinates: &mut [f64; 3]) {
let [c1, c2, c3] = coordinates;

// Ensure all coordinates are numbers
Expand All @@ -297,8 +301,17 @@ fn normalize_domain_mut(space: ColorSpace, coordinates: &mut [f64; 3]) {
*c2 = 0.0;
}
}
}

/// Normalize the range of coordinates [*core-only*].
///
/// This function ensures that (revised) lightness, chroma, and hue have valid
/// coordinates.
#[no_mangle]
pub extern "C" fn normalize_range_mut(space: ColorSpace, coordinates: &mut [f64; 3]) {
let [c1, c2, c3] = coordinates;

// Clamp lightness and chroma in Ok***
// Clamp lightness and chroma in Oklab et al
if space.is_ok() {
*c1 = c1.clamp(0.0, 1.0);

Expand All @@ -313,7 +326,10 @@ fn normalize_domain_mut(space: ColorSpace, coordinates: &mut [f64; 3]) {
}
}

/// Update coordinates for equality testing and hashing.
/// Normalize coordinates for equality testing and hashing.
///
/// When combined with normalization of not-a-numbers and ranges, this function
/// produces floating point values that, if converted to bits,
fn normalize_eq_mut(space: ColorSpace, coordinates: &mut [f64; 3]) {
let [c1, c2, c3] = coordinates;

Expand All @@ -340,25 +356,15 @@ fn normalize_eq_mut(space: ColorSpace, coordinates: &mut [f64; 3]) {
}
}

/// Normalize the coordinates [*core-only*].
///
/// This function replaces not-a-number by zero and forces Oklab's (revised)
/// lightness, chroma, and hue to respect their limits.
#[no_mangle]
pub extern "C" fn normalize(space: ColorSpace, coordinates: &[f64; 3]) -> ArrayData<f64, 3> {
let mut coordinates = *coordinates;
normalize_domain_mut(space, &mut coordinates);
coordinates.into()
}

/// Normalize the coordinates for equality testing and hashing [*core-only*].
///
/// Note: In-gamut RGB coordinates and the lightness for Oklab and Oklch have
/// unit range already. No coordinate-specific normalization is required.
#[no_mangle]
pub extern "C" fn normalize_eq(space: ColorSpace, coordinates: &[f64; 3]) -> ArrayData<u64, 3> {
let mut coordinates = *coordinates;
normalize_domain_mut(space, &mut coordinates);
normalize_nan_mut(space, &mut coordinates);
normalize_range_mut(space, &mut coordinates);
normalize_eq_mut(space, &mut coordinates);
let [c1, c2, c3] = coordinates;
[c1.to_bits(), c2.to_bits(), c3.to_bits()].into()
Expand Down Expand Up @@ -1240,24 +1246,22 @@ fn convert_with_nan(
to_space: ColorSpace,
coordinates: &[f64; 3],
) -> [f64; 3] {
// Convert normalized coordinates
let mut converted = convert(
from_space,
to_space,
&normalize(from_space, coordinates).data,
)
.data;
// Normalize coordinates and convert to interpolation space
let mut intermediate = *coordinates;
normalize_nan_mut(from_space, &mut intermediate);
normalize_range_mut(from_space, &mut intermediate);
let mut intermediate = convert(from_space, to_space, &intermediate).data;

// Carry forward missing components
for (index, coordinate) in coordinates.iter().enumerate() {
if coordinate.is_nan() {
if let Some(index) = carry_forward(from_space, to_space, index) {
converted[index] = f64::NAN;
intermediate[index] = f64::NAN;
}
}
}

converted
intermediate
}

/// A strategy for interpolating hues.
Expand Down
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,9 +549,9 @@ pub use color::core::{ColorSpace, HueInterpolation};

#[cfg(feature = "core")]
pub use color::core::{
clip, convert, delta_e_ok, from_24_bit, in_gamut, interpolate, map_to_gamut, normalize,
normalize_eq, prepare_to_interpolate, scale_lightness, to_24_bit, to_contrast,
to_contrast_luminance, ArrayData, P3_CONTRAST, SRGB_CONTRAST,
clip, convert, delta_e_ok, from_24_bit, in_gamut, interpolate, map_to_gamut, normalize_eq,
normalize_nan_mut, normalize_range_mut, prepare_to_interpolate, scale_lightness, to_24_bit,
to_contrast, to_contrast_luminance, ArrayData, P3_CONTRAST, SRGB_CONTRAST,
};

#[cfg(feature = "core")]
Expand Down

0 comments on commit bead83c

Please sign in to comment.