From 887bc27a6f9dbee5a8d17e53f67aa69f34d4bbdc Mon Sep 17 00:00:00 2001 From: Lynn <62256001+solis-lumine-vorago@users.noreply.github.com> Date: Fri, 22 Mar 2024 01:06:24 +0100 Subject: [PATCH] `Animatable` for colors (#12614) # Objective - Fixes #12202 ## Solution - Implements `Animatable` for all color types implementing arithmetic operations. - the colors returned by `Animatable`s methods are already clamped. - Adds a `color_animation.rs` example. - Implements the `*Assign` operators for color types that already had the corresponding operators. This is just a 'nice to have' and I am happy to remove this if it's not wanted. --- ## Changelog - `bevy_animation` now depends on `bevy_color`. - `LinearRgba`, `Laba`, `Oklaba` and `Xyza` implement `Animatable`. --------- Co-authored-by: Alice Cecile Co-authored-by: Zachary Harrold --- Cargo.toml | 11 ++ crates/bevy_animation/Cargo.toml | 1 + crates/bevy_animation/src/animatable.rs | 31 ++++++ crates/bevy_color/src/lib.rs | 24 +++++ examples/README.md | 1 + examples/animation/color_animation.rs | 136 ++++++++++++++++++++++++ 6 files changed, 204 insertions(+) create mode 100644 examples/animation/color_animation.rs diff --git a/Cargo.toml b/Cargo.toml index 807a06f62e..d057f02200 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1013,6 +1013,17 @@ description = "Create and play an animation defined by code that operates on the category = "Animation" wasm = true +[[example]] +name = "color_animation" +path = "examples/animation/color_animation.rs" +doc-scrape-examples = true + +[package.metadata.example.color_animation] +name = "Color animation" +description = "Demonstrates how to animate colors using mixing and splines in different color spaces" +category = "Animation" +wasm = true + [[example]] name = "cubic_curve" path = "examples/animation/cubic_curve.rs" diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 495ddd7c99..13787f46a1 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["bevy"] # bevy bevy_app = { path = "../bevy_app", version = "0.14.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.14.0-dev" } bevy_core = { path = "../bevy_core", version = "0.14.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } bevy_log = { path = "../bevy_log", version = "0.14.0-dev" } diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index 17e11bfe5f..201b8e6919 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -1,4 +1,5 @@ use crate::util; +use bevy_color::{ClampColor, Laba, LinearRgba, Oklaba, Xyza}; use bevy_ecs::world::World; use bevy_math::*; use bevy_reflect::Reflect; @@ -57,6 +58,31 @@ macro_rules! impl_float_animatable { }; } +macro_rules! impl_color_animatable { + ($ty: ident) => { + impl Animatable for $ty { + #[inline] + fn interpolate(a: &Self, b: &Self, t: f32) -> Self { + let value = *a * (1. - t) + *b * t; + value.clamped() + } + + #[inline] + fn blend(inputs: impl Iterator>) -> Self { + let mut value = Default::default(); + for input in inputs { + if input.additive { + value += input.weight * input.value; + } else { + value = Self::interpolate(&value, &input.value, input.weight); + } + } + value.clamped() + } + } + }; +} + impl_float_animatable!(f32, f32); impl_float_animatable!(Vec2, f32); impl_float_animatable!(Vec3A, f32); @@ -67,6 +93,11 @@ impl_float_animatable!(DVec2, f64); impl_float_animatable!(DVec3, f64); impl_float_animatable!(DVec4, f64); +impl_color_animatable!(LinearRgba); +impl_color_animatable!(Laba); +impl_color_animatable!(Oklaba); +impl_color_animatable!(Xyza); + // Vec3 is special cased to use Vec3A internally for blending impl Animatable for Vec3 { #[inline] diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index 937921503e..139c381cd2 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -170,6 +170,12 @@ macro_rules! impl_componentwise_point { } } + impl std::ops::AddAssign for $ty { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } + } + impl std::ops::Sub for $ty { type Output = Self; @@ -180,6 +186,12 @@ macro_rules! impl_componentwise_point { } } + impl std::ops::SubAssign for $ty { + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } + } + impl std::ops::Mul for $ty { type Output = Self; @@ -200,6 +212,12 @@ macro_rules! impl_componentwise_point { } } + impl std::ops::MulAssign for $ty { + fn mul_assign(&mut self, rhs: f32) { + *self = *self * rhs; + } + } + impl std::ops::Div for $ty { type Output = Self; @@ -210,6 +228,12 @@ macro_rules! impl_componentwise_point { } } + impl std::ops::DivAssign for $ty { + fn div_assign(&mut self, rhs: f32) { + *self = *self / rhs; + } + } + impl bevy_math::cubic_splines::Point for $ty {} }; } diff --git a/examples/README.md b/examples/README.md index 0eca6e27e9..a336ede7f4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -165,6 +165,7 @@ Example | Description [Animated Fox](../examples/animation/animated_fox.rs) | Plays an animation from a skinned glTF [Animated Transform](../examples/animation/animated_transform.rs) | Create and play an animation defined by code that operates on the `Transform` component [Animation Graph](../examples/animation/animation_graph.rs) | Blends multiple animations together with a graph +[Color animation](../examples/animation/color_animation.rs) | Demonstrates how to animate colors using mixing and splines in different color spaces [Cubic Curve](../examples/animation/cubic_curve.rs) | Bezier curve example showing a cube following a cubic curve [Custom Skinned Mesh](../examples/animation/custom_skinned_mesh.rs) | Skinned mesh example with mesh and joints data defined in code [Morph Targets](../examples/animation/morph_targets.rs) | Plays an animation from a glTF file with meshes with morph targets diff --git a/examples/animation/color_animation.rs b/examples/animation/color_animation.rs new file mode 100644 index 0000000000..386f60f7f3 --- /dev/null +++ b/examples/animation/color_animation.rs @@ -0,0 +1,136 @@ +//! Demonstrates how to animate colors in different color spaces using mixing and splines. + +use bevy::{math::cubic_splines::Point, prelude::*}; + +// We define this trait so we can reuse the same code for multiple color types that may be implemented using curves. +trait CurveColor: Point + Into + Send + Sync + 'static {} +impl + Send + Sync + 'static> CurveColor for T {} + +// We define this trait so we can reuse the same code for multiple color types that may be implemented using mixing. +trait MixedColor: Mix + Into + Send + Sync + 'static {} +impl + Send + Sync + 'static> MixedColor for T {} + +#[derive(Debug, Component)] +struct Curve(CubicCurve); + +#[derive(Debug, Component)] +struct Mixed([T; 4]); + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + animate_curve::, + animate_curve::, + animate_curve::, + animate_mixed::, + animate_mixed::, + animate_mixed::, + ), + ) + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); + + // The color spaces `Oklaba`, `Laba`, `LinearRgba` and `Xyza` all are either perceptually or physically linear. + // This property allows us to define curves, e.g. bezier curves through these spaces. + + // Define the control points for the curve. + // For more information, please see the cubic curve example. + let colors = [ + LinearRgba::WHITE, + LinearRgba::rgb(1., 1., 0.), // Yellow + LinearRgba::RED, + LinearRgba::BLACK, + ]; + // Spawn a sprite using the provided colors as control points. + spawn_curve_sprite(&mut commands, 275., colors); + + // Spawn another sprite using the provided colors as control points after converting them to the `Xyza` color space. + spawn_curve_sprite(&mut commands, 175., colors.map(Xyza::from)); + + spawn_curve_sprite(&mut commands, 75., colors.map(Oklaba::from)); + + // Other color spaces like `Srgba` or `Hsva` are neither perceptually nor physically linear. + // As such, we cannot use curves in these spaces. + // However, we can still mix these colours and animate that way. In fact, mixing colors works in any color space. + + // Spawn a spritre using the provided colors for mixing. + spawn_mixed_sprite(&mut commands, -75., colors.map(Hsla::from)); + + spawn_mixed_sprite(&mut commands, -175., colors.map(Srgba::from)); + + spawn_mixed_sprite(&mut commands, -275., colors.map(Oklcha::from)); +} + +fn spawn_curve_sprite(commands: &mut Commands, y: f32, points: [T; 4]) { + commands.spawn(( + SpriteBundle { + transform: Transform::from_xyz(0., y, 0.), + sprite: Sprite { + custom_size: Some(Vec2::new(75., 75.)), + ..Default::default() + }, + ..Default::default() + }, + Curve(CubicBezier::new([points]).to_curve()), + )); +} + +fn spawn_mixed_sprite(commands: &mut Commands, y: f32, colors: [T; 4]) { + commands.spawn(( + SpriteBundle { + transform: Transform::from_xyz(0., y, 0.), + sprite: Sprite { + custom_size: Some(Vec2::new(75., 75.)), + ..Default::default() + }, + ..Default::default() + }, + Mixed(colors), + )); +} + +fn animate_curve( + time: Res