diff --git a/crates/bevy_color/src/color.rs b/crates/bevy_color/src/color.rs
index 25ed54809e..4ddc9599a1 100644
--- a/crates/bevy_color/src/color.rs
+++ b/crates/bevy_color/src/color.rs
@@ -1,5 +1,6 @@
use crate::{
- Alpha, Hsla, Hsva, Hwba, Laba, Lcha, LinearRgba, Oklaba, Oklcha, Srgba, StandardColor, Xyza,
+ color_difference::EuclideanDistance, Alpha, Hsla, Hsva, Hue, Hwba, Laba, Lcha, LinearRgba,
+ Luminance, Mix, Oklaba, Oklcha, Srgba, StandardColor, Xyza,
};
use bevy_reflect::prelude::*;
@@ -11,6 +12,33 @@ use bevy_reflect::prelude::*;
///
#[doc = include_str!("../docs/diagrams/model_graph.svg")]
///
+///
+/// # Operations
+///
+/// [`Color`] supports all the standard color operations, such as [mixing](Mix),
+/// [luminance](Luminance) and [hue](Hue) adjustment, [clamping](ClampColor),
+/// and [diffing](EuclideanDistance). These operations delegate to the concrete color space contained
+/// by [`Color`], but will convert to [`Oklch`](Oklcha) for operations which aren't supported in the
+/// current space. After performing the operation, if a conversion was required, the result will be
+/// converted back into the original color space.
+///
+/// ```rust
+/// # use bevy_color::{Hue, Color};
+/// let red_hsv = Color::hsv(0., 1., 1.);
+/// let red_srgb = Color::srgb(1., 0., 0.);
+///
+/// // HSV has a definition of hue, so it will be returned.
+/// red_hsv.hue();
+///
+/// // SRGB doesn't have a native definition for hue.
+/// // Converts to Oklch and returns that result.
+/// red_srgb.hue();
+/// ```
+///
+/// [`Oklch`](Oklcha) has been chosen as the intermediary space in cases where conversion is required
+/// due to its perceptual uniformity and broad support for Bevy's color operations.
+/// To avoid the cost of repeated conversion, and ensure consistent results where that is desired,
+/// first convert this [`Color`] into your desired color space.
#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
#[reflect(PartialEq, Default)]
#[cfg_attr(
@@ -621,3 +649,158 @@ impl From for Xyza {
}
}
}
+
+/// Color space chosen for operations on `Color`.
+type ChosenColorSpace = Oklcha;
+
+impl Luminance for Color {
+ fn luminance(&self) -> f32 {
+ match self {
+ Color::Srgba(x) => x.luminance(),
+ Color::LinearRgba(x) => x.luminance(),
+ Color::Hsla(x) => x.luminance(),
+ Color::Hsva(x) => ChosenColorSpace::from(*x).luminance(),
+ Color::Hwba(x) => ChosenColorSpace::from(*x).luminance(),
+ Color::Laba(x) => x.luminance(),
+ Color::Lcha(x) => x.luminance(),
+ Color::Oklaba(x) => x.luminance(),
+ Color::Oklcha(x) => x.luminance(),
+ Color::Xyza(x) => x.luminance(),
+ }
+ }
+
+ fn with_luminance(&self, value: f32) -> Self {
+ let mut new = *self;
+
+ match &mut new {
+ Color::Srgba(x) => *x = x.with_luminance(value),
+ Color::LinearRgba(x) => *x = x.with_luminance(value),
+ Color::Hsla(x) => *x = x.with_luminance(value),
+ Color::Hsva(x) => *x = ChosenColorSpace::from(*x).with_luminance(value).into(),
+ Color::Hwba(x) => *x = ChosenColorSpace::from(*x).with_luminance(value).into(),
+ Color::Laba(x) => *x = x.with_luminance(value),
+ Color::Lcha(x) => *x = x.with_luminance(value),
+ Color::Oklaba(x) => *x = x.with_luminance(value),
+ Color::Oklcha(x) => *x = x.with_luminance(value),
+ Color::Xyza(x) => *x = x.with_luminance(value),
+ }
+
+ new
+ }
+
+ fn darker(&self, amount: f32) -> Self {
+ let mut new = *self;
+
+ match &mut new {
+ Color::Srgba(x) => *x = x.darker(amount),
+ Color::LinearRgba(x) => *x = x.darker(amount),
+ Color::Hsla(x) => *x = x.darker(amount),
+ Color::Hsva(x) => *x = ChosenColorSpace::from(*x).darker(amount).into(),
+ Color::Hwba(x) => *x = ChosenColorSpace::from(*x).darker(amount).into(),
+ Color::Laba(x) => *x = x.darker(amount),
+ Color::Lcha(x) => *x = x.darker(amount),
+ Color::Oklaba(x) => *x = x.darker(amount),
+ Color::Oklcha(x) => *x = x.darker(amount),
+ Color::Xyza(x) => *x = x.darker(amount),
+ }
+
+ new
+ }
+
+ fn lighter(&self, amount: f32) -> Self {
+ let mut new = *self;
+
+ match &mut new {
+ Color::Srgba(x) => *x = x.lighter(amount),
+ Color::LinearRgba(x) => *x = x.lighter(amount),
+ Color::Hsla(x) => *x = x.lighter(amount),
+ Color::Hsva(x) => *x = ChosenColorSpace::from(*x).lighter(amount).into(),
+ Color::Hwba(x) => *x = ChosenColorSpace::from(*x).lighter(amount).into(),
+ Color::Laba(x) => *x = x.lighter(amount),
+ Color::Lcha(x) => *x = x.lighter(amount),
+ Color::Oklaba(x) => *x = x.lighter(amount),
+ Color::Oklcha(x) => *x = x.lighter(amount),
+ Color::Xyza(x) => *x = x.lighter(amount),
+ }
+
+ new
+ }
+}
+
+impl Hue for Color {
+ fn with_hue(&self, hue: f32) -> Self {
+ let mut new = *self;
+
+ match &mut new {
+ Color::Srgba(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(),
+ Color::LinearRgba(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(),
+ Color::Hsla(x) => *x = x.with_hue(hue),
+ Color::Hsva(x) => *x = x.with_hue(hue),
+ Color::Hwba(x) => *x = x.with_hue(hue),
+ Color::Laba(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(),
+ Color::Lcha(x) => *x = x.with_hue(hue),
+ Color::Oklaba(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(),
+ Color::Oklcha(x) => *x = x.with_hue(hue),
+ Color::Xyza(x) => *x = ChosenColorSpace::from(*x).with_hue(hue).into(),
+ }
+
+ new
+ }
+
+ fn hue(&self) -> f32 {
+ match self {
+ Color::Srgba(x) => ChosenColorSpace::from(*x).hue(),
+ Color::LinearRgba(x) => ChosenColorSpace::from(*x).hue(),
+ Color::Hsla(x) => x.hue(),
+ Color::Hsva(x) => x.hue(),
+ Color::Hwba(x) => x.hue(),
+ Color::Laba(x) => ChosenColorSpace::from(*x).hue(),
+ Color::Lcha(x) => x.hue(),
+ Color::Oklaba(x) => ChosenColorSpace::from(*x).hue(),
+ Color::Oklcha(x) => x.hue(),
+ Color::Xyza(x) => ChosenColorSpace::from(*x).hue(),
+ }
+ }
+
+ fn set_hue(&mut self, hue: f32) {
+ *self = self.with_hue(hue);
+ }
+}
+
+impl Mix for Color {
+ fn mix(&self, other: &Self, factor: f32) -> Self {
+ let mut new = *self;
+
+ match &mut new {
+ Color::Srgba(x) => *x = x.mix(&(*other).into(), factor),
+ Color::LinearRgba(x) => *x = x.mix(&(*other).into(), factor),
+ Color::Hsla(x) => *x = x.mix(&(*other).into(), factor),
+ Color::Hsva(x) => *x = x.mix(&(*other).into(), factor),
+ Color::Hwba(x) => *x = x.mix(&(*other).into(), factor),
+ Color::Laba(x) => *x = x.mix(&(*other).into(), factor),
+ Color::Lcha(x) => *x = x.mix(&(*other).into(), factor),
+ Color::Oklaba(x) => *x = x.mix(&(*other).into(), factor),
+ Color::Oklcha(x) => *x = x.mix(&(*other).into(), factor),
+ Color::Xyza(x) => *x = x.mix(&(*other).into(), factor),
+ }
+
+ new
+ }
+}
+
+impl EuclideanDistance for Color {
+ fn distance_squared(&self, other: &Self) -> f32 {
+ match self {
+ Color::Srgba(x) => x.distance_squared(&(*other).into()),
+ Color::LinearRgba(x) => x.distance_squared(&(*other).into()),
+ Color::Hsla(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()),
+ Color::Hsva(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()),
+ Color::Hwba(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()),
+ Color::Laba(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()),
+ Color::Lcha(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()),
+ Color::Oklaba(x) => x.distance_squared(&(*other).into()),
+ Color::Oklcha(x) => x.distance_squared(&(*other).into()),
+ Color::Xyza(x) => ChosenColorSpace::from(*x).distance_squared(&(*other).into()),
+ }
+ }
+}