Implement Color Operations for Color (#13285)

# Objective

- Fixes #13214

## Solution

Delegates to internal type when possible, otherwise uses
`ChosenColorSpace` as an intermediary. This _will_ double convert, but
this is considered an acceptable compromise since use of specific colour
types in performance critical colour operations is already encouraged.

`ChosenColorSpace` is `Oklcha` since it's perceptually uniform while
supporting all required operations, and in my opinion is the "best" for
this task. Using different spaces for different operations will make
documenting this double-conversion behaviour more challenging.

## Testing

Changes straightforward enough to not require testing beyond current CI
in my opinion.

---

## Changelog

- Implemented the following traits for `Color`:
  - `Luminance`
  - `Hue`
  - `Mix`
  - `EuclideanDistance`
  - `ClampColor`
- Added documentation to `Color` explaining the behaviour of these
operations (possible conversion, etc.)
This commit is contained in:
Zachary Harrold 2024-05-14 20:15:47 +10:00 committed by GitHub
parent 519ed5de42
commit 6482a036cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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::*;
/// <div>
#[doc = include_str!("../docs/diagrams/model_graph.svg")]
/// </div>
///
/// # 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<Color> 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()),
}
}
}