Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Color gradient curve #14976

Merged
merged 15 commits into from
Sep 2, 2024
104 changes: 104 additions & 0 deletions crates/bevy_color/src/color_gradient.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use crate::Mix;
use bevy_math::curve::{cores::EvenCoreError, Curve, Interval, SampleCurve};

/// A curve whose samples are defined by a collection of colors.
#[derive(Clone, Debug)]
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
pub struct ColorCurve<T: Mix + Clone, I> {
curve: SampleCurve<T, I>,
}
RobWalt marked this conversation as resolved.
Show resolved Hide resolved

impl<T, I> ColorCurve<T, I>
where
T: Mix + Clone,
{
RobWalt marked this conversation as resolved.
Show resolved Hide resolved
/// Create a new [`ColorCurve`] from a collection of [mixable] types and a mixing function. The
/// domain of this curve will always be `[0.0, len - 1]` where `len` is the amount of mixable
/// objects in the collection.
///
/// This fails if there's not at least two mixable things in the collection.
///
/// [mixable]: `Mix`
///
/// # Example
///
/// ```
/// # use bevy_color::palettes::basic::*;
/// # use bevy_color::Mix;
/// # use bevy_color::Srgba;
/// # use bevy_color::ColorCurve;
/// # use bevy_math::curve::Interval;
/// # use bevy_math::curve::Curve;
/// let broken = ColorCurve::new([RED], Srgba::mix);
/// assert!(broken.is_err());
/// let gradient = ColorCurve::new([RED, GREEN, BLUE], Srgba::mix);
/// assert!(gradient.is_ok());
/// assert_eq!(gradient.unwrap().domain(), Interval::new(0.0, 2.0).unwrap());
/// ```
RobWalt marked this conversation as resolved.
Show resolved Hide resolved
pub fn new(
colors: impl IntoIterator<Item = impl Into<T>>,
interpolation: I,
) -> Result<Self, EvenCoreError>
where
I: Fn(&T, &T, f32) -> T,
{
let colors = colors.into_iter().map(Into::into).collect::<Vec<_>>();
Interval::new(0.0, colors.len().saturating_sub(1) as f32)
.map_err(|_| EvenCoreError::NotEnoughSamples {
samples: colors.len(),
})
.and_then(|domain| SampleCurve::new(domain, colors, interpolation))
.map(|curve| Self { curve })
}
RobWalt marked this conversation as resolved.
Show resolved Hide resolved
}

impl<T, F> Curve<T> for ColorCurve<T, F>
where
T: Mix + Clone,
F: Fn(&T, &T, f32) -> T,
{
RobWalt marked this conversation as resolved.
Show resolved Hide resolved
fn domain(&self) -> Interval {
self.curve.domain()
}

fn sample_unchecked(&self, t: f32) -> T {
self.curve.sample_unchecked(t)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::palettes::basic;
use crate::Srgba;

#[test]
fn test_color_curve() {
let broken = ColorCurve::new([basic::RED], Srgba::mix);
assert!(broken.is_err());

let gradient = [basic::RED, basic::LIME, basic::BLUE];
let curve = ColorCurve::new(gradient, Srgba::mix).unwrap();
RobWalt marked this conversation as resolved.
Show resolved Hide resolved

assert_eq!(curve.domain(), Interval::new(0.0, 2.0).unwrap());

let brighter_curve = curve.map(|c| c.mix(&basic::WHITE, 0.5));

[
(-0.1, None),
(0.0, Some([1.0, 0.5, 0.5, 1.0])),
(0.5, Some([0.75, 0.75, 0.5, 1.0])),
(1.0, Some([0.5, 1.0, 0.5, 1.0])),
(1.5, Some([0.5, 0.75, 0.75, 1.0])),
(2.0, Some([0.5, 0.5, 1.0, 1.0])),
(2.1, None),
]
.map(|(t, maybe_rgba)| {
let maybe_srgba = maybe_rgba.map(|[r, g, b, a]| Srgba::new(r, g, b, a));
(t, maybe_srgba)
})
.into_iter()
.for_each(|(t, maybe_color)| {
assert_eq!(brighter_curve.sample(t), maybe_color);
});
}
}
6 changes: 5 additions & 1 deletion crates/bevy_color/src/color_range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub trait ColorRange<T: Mix> {

impl<T: Mix> ColorRange<T> for Range<T> {
fn at(&self, factor: f32) -> T {
self.start.mix(&self.end, factor)
self.start.mix(&self.end, factor.clamp(0.0, 1.0))
}
}

Expand All @@ -28,16 +28,20 @@ mod tests {
#[test]
fn test_color_range() {
let range = basic::RED..basic::BLUE;
assert_eq!(range.at(-0.5), basic::RED);
assert_eq!(range.at(0.0), basic::RED);
assert_eq!(range.at(0.5), Srgba::new(0.5, 0.0, 0.5, 1.0));
assert_eq!(range.at(1.0), basic::BLUE);
assert_eq!(range.at(1.5), basic::BLUE);

let lred: LinearRgba = basic::RED.into();
let lblue: LinearRgba = basic::BLUE.into();

let range = lred..lblue;
assert_eq!(range.at(-0.5), lred);
assert_eq!(range.at(0.0), lred);
assert_eq!(range.at(0.5), LinearRgba::new(0.5, 0.0, 0.5, 1.0));
assert_eq!(range.at(1.0), lblue);
assert_eq!(range.at(1.5), lblue);
}
}
2 changes: 2 additions & 0 deletions crates/bevy_color/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@

mod color;
pub mod color_difference;
mod color_gradient;
mod color_ops;
mod color_range;
mod hsla;
Expand Down Expand Up @@ -127,6 +128,7 @@ pub mod prelude {
}

pub use color::*;
pub use color_gradient::*;
pub use color_ops::*;
pub use color_range::*;
pub use hsla::*;
Expand Down