Color gradient curve (#14976)
# Objective - Currently we have the `ColorRange` trait to interpolate linearly between two colors - It would be cool to have: 1. linear interpolation between n colors where `n >= 1` 2. other kinds of interpolation ## Solution 1. Implement `ColorGradient` which takes `n >= 1` colors and linearly interpolates between consecutive pairs of them 2. Implement `Curve` intergration for this `ColorGradient` which yields a curve struct. After that we can apply all of the cool curve adaptors like `.reparametrize()` and `.map()` to the gradient ## Testing - Added doc tests - Added tests ## Showcase ```rust // let gradient = ColorGradient::new(vec![]).unwrap(); // panic! 💥 let gradient = ColorGradient::new([basic::RED, basic::LIME, basic::BLUE]).expect("non-empty"); let curve = gradient.to_curve(); let brighter_curve = curve.map(|c| c.mix(&basic::WHITE, 0.5)); ``` --- Kind of related to https://github.com/bevyengine/bevy/pull/14971#discussion_r1736337631 --------- Co-authored-by: Zachary Harrold <zac@harrold.com.au> Co-authored-by: Matty <weatherleymatthew@gmail.com>
This commit is contained in:
parent
01a3b0e830
commit
8a64b7621d
102
crates/bevy_color/src/color_gradient.rs
Normal file
102
crates/bevy_color/src/color_gradient.rs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
use crate::Mix;
|
||||||
|
use bevy_math::curve::{
|
||||||
|
cores::{EvenCore, EvenCoreError},
|
||||||
|
Curve, Interval,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A curve whose samples are defined by a collection of colors.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
|
||||||
|
pub struct ColorCurve<T> {
|
||||||
|
core: EvenCore<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ColorCurve<T>
|
||||||
|
where
|
||||||
|
T: Mix + Clone,
|
||||||
|
{
|
||||||
|
/// Create a new [`ColorCurve`] from a collection of [mixable] types. 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]);
|
||||||
|
/// assert!(broken.is_err());
|
||||||
|
/// let gradient = ColorCurve::new([RED, GREEN, BLUE]);
|
||||||
|
/// assert!(gradient.is_ok());
|
||||||
|
/// assert_eq!(gradient.unwrap().domain(), Interval::new(0.0, 2.0).unwrap());
|
||||||
|
/// ```
|
||||||
|
pub fn new(colors: impl IntoIterator<Item = T>) -> Result<Self, EvenCoreError> {
|
||||||
|
let colors = colors.into_iter().collect::<Vec<_>>();
|
||||||
|
Interval::new(0.0, colors.len().saturating_sub(1) as f32)
|
||||||
|
.map_err(|_| EvenCoreError::NotEnoughSamples {
|
||||||
|
samples: colors.len(),
|
||||||
|
})
|
||||||
|
.and_then(|domain| EvenCore::new(domain, colors))
|
||||||
|
.map(|core| Self { core })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Curve<T> for ColorCurve<T>
|
||||||
|
where
|
||||||
|
T: Mix + Clone,
|
||||||
|
{
|
||||||
|
fn domain(&self) -> Interval {
|
||||||
|
self.core.domain()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_unchecked(&self, t: f32) -> T {
|
||||||
|
self.core.sample_with(t, T::mix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::palettes::basic;
|
||||||
|
use crate::Srgba;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_color_curve() {
|
||||||
|
let broken = ColorCurve::new([basic::RED]);
|
||||||
|
assert!(broken.is_err());
|
||||||
|
|
||||||
|
let gradient = [basic::RED, basic::LIME, basic::BLUE];
|
||||||
|
let curve = ColorCurve::new(gradient).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(curve.domain(), Interval::new(0.0, 2.0).unwrap());
|
||||||
|
|
||||||
|
let brighter_curve = curve.map(|c: Srgba| 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ pub trait ColorRange<T: Mix> {
|
|||||||
|
|
||||||
impl<T: Mix> ColorRange<T> for Range<T> {
|
impl<T: Mix> ColorRange<T> for Range<T> {
|
||||||
fn at(&self, factor: f32) -> T {
|
fn at(&self, factor: f32) -> T {
|
||||||
self.start.mix(&self.end, factor)
|
self.start.mix(&self.end, factor.clamp(0.0, 1.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,16 +28,20 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_color_range() {
|
fn test_color_range() {
|
||||||
let range = basic::RED..basic::BLUE;
|
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.0), basic::RED);
|
||||||
assert_eq!(range.at(0.5), Srgba::new(0.5, 0.0, 0.5, 1.0));
|
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.0), basic::BLUE);
|
||||||
|
assert_eq!(range.at(1.5), basic::BLUE);
|
||||||
|
|
||||||
let lred: LinearRgba = basic::RED.into();
|
let lred: LinearRgba = basic::RED.into();
|
||||||
let lblue: LinearRgba = basic::BLUE.into();
|
let lblue: LinearRgba = basic::BLUE.into();
|
||||||
|
|
||||||
let range = lred..lblue;
|
let range = lred..lblue;
|
||||||
|
assert_eq!(range.at(-0.5), lred);
|
||||||
assert_eq!(range.at(0.0), 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(0.5), LinearRgba::new(0.5, 0.0, 0.5, 1.0));
|
||||||
assert_eq!(range.at(1.0), lblue);
|
assert_eq!(range.at(1.0), lblue);
|
||||||
|
assert_eq!(range.at(1.5), lblue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,6 +92,7 @@
|
|||||||
|
|
||||||
mod color;
|
mod color;
|
||||||
pub mod color_difference;
|
pub mod color_difference;
|
||||||
|
mod color_gradient;
|
||||||
mod color_ops;
|
mod color_ops;
|
||||||
mod color_range;
|
mod color_range;
|
||||||
mod hsla;
|
mod hsla;
|
||||||
@ -127,6 +128,7 @@ pub mod prelude {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub use color::*;
|
pub use color::*;
|
||||||
|
pub use color_gradient::*;
|
||||||
pub use color_ops::*;
|
pub use color_ops::*;
|
||||||
pub use color_range::*;
|
pub use color_range::*;
|
||||||
pub use hsla::*;
|
pub use hsla::*;
|
||||||
|
Loading…
Reference in New Issue
Block a user