From eca2b21e6a092380441956a42f85952afa0fcb64 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sat, 11 Nov 2023 17:57:26 -0800 Subject: [PATCH] contrast_between is now smarter --- src/animation.rs | 6 +++ src/styles.rs | 106 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 93 insertions(+), 19 deletions(-) diff --git a/src/animation.rs b/src/animation.rs index 2509b8444..14d680e39 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -800,6 +800,12 @@ impl ZeroToOne { Self(value.clamp(0., 1.)) } + /// Returns the difference between `self` and `other` as a positive number. + #[must_use] + pub fn difference_between(self, other: Self) -> Self { + Self((self.0 - other.0).abs()) + } + /// Returns the contained floating point value. #[must_use] pub fn into_f32(self) -> f32 { diff --git a/src/styles.rs b/src/styles.rs index e9476cf35..19a137b46 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -1150,6 +1150,32 @@ impl ColorSource { Okhsl::new(self.hue, *self.saturation, *lightness.into_lightness()).into_color(); Color::new_f32(rgb.red, rgb.blue, rgb.green, 1.0) } + + /// Calculates an approximate ratio between 0.0 and 1.0 of how contrasting + /// these colors are, with perfect constrast being two clors that are + /// opposite of each other on the hue circle and one fully desaturated and + /// the other fully saturated. + #[must_use] + pub fn contrast_between(self, other: Self) -> ZeroToOne { + let saturation_delta = self.saturation.difference_between(other.saturation); + let self_hue = self.hue.into_positive_degrees(); + let other_hue = other.hue.into_positive_degrees(); + // Calculate the shortest distance between the hues, taking into account + // that 0 and 359 are one degree apart. + let hue_delta = ZeroToOne::new( + if self_hue < other_hue { + let hue_delta_a = other_hue - self_hue; + let hue_delta_b = self_hue + 360. - other_hue; + hue_delta_a.min(hue_delta_b) + } else { + let hue_delta_a = self_hue - other_hue; + let hue_delta_b = other_hue + 360. - self_hue; + hue_delta_a.min(hue_delta_b) + } / 180., + ); + + saturation_delta * hue_delta + } } /// A value that can represent the lightness of a color. @@ -1197,29 +1223,31 @@ pub trait ColorExt: Copy { self.into_source_and_lightness().1 } + /// Returns the contrast between this color and the components provided. + /// + /// To achieve a contrast of 1.0: + /// + /// - `self`'s hue and `check_source.hue` must be 180 degrees apart. + /// - `self`'s saturation and `check_source.saturation` must be different by + /// 1.0. + /// - `self`'s lightness and `check_lightness` must be different by 1.0. + /// - `self`'s alpha and `check_alpha` must be different by 1.0. + /// + /// The algorithm currently used is purposely left undocumented as it will + /// likely change. It should be a reasonable heuristic until someone smarter + /// than @ecton comes along. + fn contrast_between( + self, + check_source: ColorSource, + check_lightness: ZeroToOne, + check_alpha: ZeroToOne, + ) -> ZeroToOne; + /// Returns the color in `others` that contrasts the most from `self`. #[must_use] fn most_contrasting(self, others: &[Self]) -> Self where - Self: Copy, - { - // TODO this currently only checks lightness. We should probably factor - // in hue/saturation changes too. - let check = self.lightness(); - - let mut others = others.iter().copied(); - let mut most_contrasting = others.next().expect("at least one comparison"); - let mut most_contrast_amount = (*most_contrasting.lightness() - *check).abs(); - for other in others { - let contrast_amount = (*other.lightness() - *check).abs(); - if contrast_amount > most_contrast_amount { - most_contrasting = other; - most_contrast_amount = contrast_amount; - } - } - - most_contrasting - } + Self: Copy; } impl ColorExt for Color { @@ -1234,4 +1262,44 @@ impl ColorExt for Color { ZeroToOne::new(hsl.lightness * self.alpha_f32()), ) } + + fn contrast_between( + self, + check_source: ColorSource, + check_lightness: ZeroToOne, + check_alpha: ZeroToOne, + ) -> ZeroToOne { + let (other_source, other_lightness) = self.into_source_and_lightness(); + let lightness_delta = other_lightness.difference_between(check_lightness); + + let source_change = check_source.contrast_between(other_source); + + let other_alpha = ZeroToOne::new(self.alpha_f32()); + let alpha_delta = check_alpha.difference_between(other_alpha); + + lightness_delta * source_change * alpha_delta + } + + fn most_contrasting(self, others: &[Self]) -> Self + where + Self: Copy, + { + let (check_source, check_lightness) = self.into_source_and_lightness(); + let check_alpha = ZeroToOne::new(self.alpha_f32()); + + let mut others = others.iter().copied(); + let mut most_contrasting = others.next().expect("at least one comparison"); + let mut most_contrast_amount = + most_contrasting.contrast_between(check_source, check_lightness, check_alpha); + for other in others { + let contrast_amount = + other.contrast_between(check_source, check_lightness, check_alpha); + if contrast_amount > most_contrast_amount { + most_contrasting = other; + most_contrast_amount = contrast_amount; + } + } + + most_contrasting + } }