Skip to content

Commit

Permalink
contrast_between is now smarter
Browse files Browse the repository at this point in the history
  • Loading branch information
ecton committed Nov 12, 2023
1 parent 0610466 commit eca2b21
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 19 deletions.
6 changes: 6 additions & 0 deletions src/animation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
106 changes: 87 additions & 19 deletions src/styles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
}

0 comments on commit eca2b21

Please sign in to comment.