Add Saturation trait to bevy_color (#18202)

# Objective

- Allow for convenient access and mutation of color saturation providing
following the `Hue`, `Luminance` traits.

- `with_saturation()` builder method
- `saturation()` to get the saturation of a `Color`
- `set_saturation()` to set the saturation of a mutable `Color`

## Solution

- Defined `Saturation` trait in `color_ops.rs`
- Implemented `Saturation` on `Hsla` and `Hsva`
- Implemented `Saturation` on `Color` which proxies to other color space
impls.
- In the case of colorspaces which don't have native saturation
components
  the color is converted to 'Hsla` internally

## Testing

- Empirically tested
---

## Showcase

```rust
fn next_golden(&mut self) -> Color {
    self.current
        .rotate_hue((1.0 / GOLDEN_RATIO) * 360.0)
        .with_saturation(self.rand_saturation())
        .with_luminance(self.rand_luminance())
        .into()
}
```

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Matt Thompson 2025-03-10 14:52:04 -07:00 committed by GitHub
parent 65a9e6fd9f
commit 4e2dc4b15f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 98 additions and 4 deletions

View File

@ -1,6 +1,6 @@
use crate::{
color_difference::EuclideanDistance, Alpha, Hsla, Hsva, Hue, Hwba, Laba, Lcha, LinearRgba,
Luminance, Mix, Oklaba, Oklcha, Srgba, StandardColor, Xyza,
Luminance, Mix, Oklaba, Oklcha, Saturation, Srgba, StandardColor, Xyza,
};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::prelude::*;
@ -810,6 +810,44 @@ impl Hue for Color {
}
}
impl Saturation for Color {
fn with_saturation(&self, saturation: f32) -> Self {
let mut new = *self;
match &mut new {
Color::Srgba(x) => Hsla::from(*x).with_saturation(saturation).into(),
Color::LinearRgba(x) => Hsla::from(*x).with_saturation(saturation).into(),
Color::Hsla(x) => x.with_saturation(saturation).into(),
Color::Hsva(x) => x.with_saturation(saturation).into(),
Color::Hwba(x) => Hsla::from(*x).with_saturation(saturation).into(),
Color::Laba(x) => Hsla::from(*x).with_saturation(saturation).into(),
Color::Lcha(x) => Hsla::from(*x).with_saturation(saturation).into(),
Color::Oklaba(x) => Hsla::from(*x).with_saturation(saturation).into(),
Color::Oklcha(x) => Hsla::from(*x).with_saturation(saturation).into(),
Color::Xyza(x) => Hsla::from(*x).with_saturation(saturation).into(),
}
}
fn saturation(&self) -> f32 {
match self {
Color::Srgba(x) => Hsla::from(*x).saturation(),
Color::LinearRgba(x) => Hsla::from(*x).saturation(),
Color::Hsla(x) => x.saturation(),
Color::Hsva(x) => x.saturation(),
Color::Hwba(x) => Hsla::from(*x).saturation(),
Color::Laba(x) => Hsla::from(*x).saturation(),
Color::Lcha(x) => Hsla::from(*x).saturation(),
Color::Oklaba(x) => Hsla::from(*x).saturation(),
Color::Oklcha(x) => Hsla::from(*x).saturation(),
Color::Xyza(x) => Hsla::from(*x).saturation(),
}
}
fn set_saturation(&mut self, saturation: f32) {
*self = self.with_saturation(saturation);
}
}
impl Mix for Color {
fn mix(&self, other: &Self, factor: f32) -> Self {
let mut new = *self;

View File

@ -95,6 +95,21 @@ pub trait Hue: Sized {
}
}
/// Trait for manipulating the saturation of a color.
///
/// When working with color spaces that do not have native saturation components
/// the operations are performed in [`crate::Hsla`].
pub trait Saturation: Sized {
/// Return a new version of this color with the saturation channel set to the given value.
fn with_saturation(&self, saturation: f32) -> Self;
/// Return the saturation of this color [0.0, 1.0].
fn saturation(&self) -> f32;
/// Sets the saturation of this color.
fn set_saturation(&mut self, saturation: f32);
}
/// Trait with methods for converting colors to non-color types
pub trait ColorToComponents {
/// Convert to an f32 array

View File

@ -1,6 +1,6 @@
use crate::{
Alpha, ColorToComponents, Gray, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba,
StandardColor, Xyza,
Alpha, ColorToComponents, Gray, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Saturation,
Srgba, StandardColor, Xyza,
};
use bevy_math::{Vec3, Vec4};
#[cfg(feature = "bevy_reflect")]
@ -159,6 +159,26 @@ impl Hue for Hsla {
}
}
impl Saturation for Hsla {
#[inline]
fn with_saturation(&self, saturation: f32) -> Self {
Self {
saturation,
..*self
}
}
#[inline]
fn saturation(&self) -> f32 {
self.saturation
}
#[inline]
fn set_saturation(&mut self, saturation: f32) {
self.saturation = saturation;
}
}
impl Luminance for Hsla {
#[inline]
fn with_luminance(&self, lightness: f32) -> Self {

View File

@ -1,5 +1,6 @@
use crate::{
Alpha, ColorToComponents, Gray, Hue, Hwba, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza,
Alpha, ColorToComponents, Gray, Hue, Hwba, Lcha, LinearRgba, Mix, Saturation, Srgba,
StandardColor, Xyza,
};
use bevy_math::{Vec3, Vec4};
#[cfg(feature = "bevy_reflect")]
@ -129,6 +130,26 @@ impl Hue for Hsva {
}
}
impl Saturation for Hsva {
#[inline]
fn with_saturation(&self, saturation: f32) -> Self {
Self {
saturation,
..*self
}
}
#[inline]
fn saturation(&self) -> f32 {
self.saturation
}
#[inline]
fn set_saturation(&mut self, saturation: f32) {
self.saturation = saturation;
}
}
impl From<Hsva> for Hwba {
fn from(
Hsva {