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 <alice.i.cecile@gmail.com> Co-authored-by: Zachary Harrold <zac@harrold.com.au>
This commit is contained in:
		
							parent
							
								
									fcf01a7925
								
							
						
					
					
						commit
						887bc27a6f
					
				
							
								
								
									
										11
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								Cargo.toml
									
									
									
									
									
								
							| @ -1013,6 +1013,17 @@ description = "Create and play an animation defined by code that operates on the | |||||||
| category = "Animation" | category = "Animation" | ||||||
| wasm = true | 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]] | [[example]] | ||||||
| name = "cubic_curve" | name = "cubic_curve" | ||||||
| path = "examples/animation/cubic_curve.rs" | path = "examples/animation/cubic_curve.rs" | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ keywords = ["bevy"] | |||||||
| # bevy | # bevy | ||||||
| bevy_app = { path = "../bevy_app", version = "0.14.0-dev" } | bevy_app = { path = "../bevy_app", version = "0.14.0-dev" } | ||||||
| bevy_asset = { path = "../bevy_asset", 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_core = { path = "../bevy_core", version = "0.14.0-dev" } | ||||||
| bevy_derive = { path = "../bevy_derive", 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" } | bevy_log = { path = "../bevy_log", version = "0.14.0-dev" } | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| use crate::util; | use crate::util; | ||||||
|  | use bevy_color::{ClampColor, Laba, LinearRgba, Oklaba, Xyza}; | ||||||
| use bevy_ecs::world::World; | use bevy_ecs::world::World; | ||||||
| use bevy_math::*; | use bevy_math::*; | ||||||
| use bevy_reflect::Reflect; | 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<Item = BlendInput<Self>>) -> 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!(f32, f32); | ||||||
| impl_float_animatable!(Vec2, f32); | impl_float_animatable!(Vec2, f32); | ||||||
| impl_float_animatable!(Vec3A, f32); | impl_float_animatable!(Vec3A, f32); | ||||||
| @ -67,6 +93,11 @@ impl_float_animatable!(DVec2, f64); | |||||||
| impl_float_animatable!(DVec3, f64); | impl_float_animatable!(DVec3, f64); | ||||||
| impl_float_animatable!(DVec4, 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
 | // Vec3 is special cased to use Vec3A internally for blending
 | ||||||
| impl Animatable for Vec3 { | impl Animatable for Vec3 { | ||||||
|     #[inline] |     #[inline] | ||||||
|  | |||||||
| @ -170,6 +170,12 @@ macro_rules! impl_componentwise_point { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         impl std::ops::AddAssign<Self> for $ty { | ||||||
|  |             fn add_assign(&mut self, rhs: Self) { | ||||||
|  |                 *self = *self + rhs; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         impl std::ops::Sub<Self> for $ty { |         impl std::ops::Sub<Self> for $ty { | ||||||
|             type Output = Self; |             type Output = Self; | ||||||
| 
 | 
 | ||||||
| @ -180,6 +186,12 @@ macro_rules! impl_componentwise_point { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         impl std::ops::SubAssign<Self> for $ty { | ||||||
|  |             fn sub_assign(&mut self, rhs: Self) { | ||||||
|  |                 *self = *self - rhs; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         impl std::ops::Mul<f32> for $ty { |         impl std::ops::Mul<f32> for $ty { | ||||||
|             type Output = Self; |             type Output = Self; | ||||||
| 
 | 
 | ||||||
| @ -200,6 +212,12 @@ macro_rules! impl_componentwise_point { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         impl std::ops::MulAssign<f32> for $ty { | ||||||
|  |             fn mul_assign(&mut self, rhs: f32) { | ||||||
|  |                 *self = *self * rhs; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         impl std::ops::Div<f32> for $ty { |         impl std::ops::Div<f32> for $ty { | ||||||
|             type Output = Self; |             type Output = Self; | ||||||
| 
 | 
 | ||||||
| @ -210,6 +228,12 @@ macro_rules! impl_componentwise_point { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         impl std::ops::DivAssign<f32> for $ty { | ||||||
|  |             fn div_assign(&mut self, rhs: f32) { | ||||||
|  |                 *self = *self / rhs; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         impl bevy_math::cubic_splines::Point for $ty {} |         impl bevy_math::cubic_splines::Point for $ty {} | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -165,6 +165,7 @@ Example | Description | |||||||
| [Animated Fox](../examples/animation/animated_fox.rs) | Plays an animation from a skinned glTF | [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 | [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 | [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 | [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 | [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 | [Morph Targets](../examples/animation/morph_targets.rs) | Plays an animation from a glTF file with meshes with morph targets | ||||||
|  | |||||||
							
								
								
									
										136
									
								
								examples/animation/color_animation.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								examples/animation/color_animation.rs
									
									
									
									
									
										Normal file
									
								
							| @ -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<Color> + Send + Sync + 'static {} | ||||||
|  | impl<T: Point + Into<Color> + 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<Color> + Send + Sync + 'static {} | ||||||
|  | impl<T: Mix + Into<Color> + Send + Sync + 'static> MixedColor for T {} | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Component)] | ||||||
|  | struct Curve<T: CurveColor>(CubicCurve<T>); | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Component)] | ||||||
|  | struct Mixed<T: MixedColor>([T; 4]); | ||||||
|  | 
 | ||||||
|  | fn main() { | ||||||
|  |     App::new() | ||||||
|  |         .add_plugins(DefaultPlugins) | ||||||
|  |         .add_systems(Startup, setup) | ||||||
|  |         .add_systems( | ||||||
|  |             Update, | ||||||
|  |             ( | ||||||
|  |                 animate_curve::<LinearRgba>, | ||||||
|  |                 animate_curve::<Oklaba>, | ||||||
|  |                 animate_curve::<Xyza>, | ||||||
|  |                 animate_mixed::<Hsla>, | ||||||
|  |                 animate_mixed::<Srgba>, | ||||||
|  |                 animate_mixed::<Oklcha>, | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |         .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<T: CurveColor>(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<T: MixedColor>(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<T: CurveColor>( | ||||||
|  |     time: Res<Time>, | ||||||
|  |     mut query: Query<(&mut Transform, &mut Sprite, &Curve<T>)>, | ||||||
|  | ) { | ||||||
|  |     let t = (time.elapsed_seconds().sin() + 1.) / 2.; | ||||||
|  | 
 | ||||||
|  |     for (mut transform, mut sprite, cubic_curve) in &mut query { | ||||||
|  |         // position takes a point from the curve where 0 is the initial point
 | ||||||
|  |         // and 1 is the last point
 | ||||||
|  |         sprite.color = cubic_curve.0.position(t).into(); | ||||||
|  |         transform.translation.x = 600. * (t - 0.5); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn animate_mixed<T: MixedColor>( | ||||||
|  |     time: Res<Time>, | ||||||
|  |     mut query: Query<(&mut Transform, &mut Sprite, &Mixed<T>)>, | ||||||
|  | ) { | ||||||
|  |     let t = (time.elapsed_seconds().sin() + 1.) / 2.; | ||||||
|  | 
 | ||||||
|  |     for (mut transform, mut sprite, mixed) in &mut query { | ||||||
|  |         sprite.color = { | ||||||
|  |             // First, we determine the amount of intervals between colors.
 | ||||||
|  |             // For four colors, there are three intervals between those colors;
 | ||||||
|  |             let intervals = (mixed.0.len() - 1) as f32; | ||||||
|  | 
 | ||||||
|  |             // Next we determine the index of the first of the two colorts to mix.
 | ||||||
|  |             let start_i = (t * intervals).floor().min(intervals - 1.); | ||||||
|  | 
 | ||||||
|  |             // Lastly we determine the 'local' value of t in this interval.
 | ||||||
|  |             let local_t = (t * intervals) - start_i; | ||||||
|  | 
 | ||||||
|  |             let color = mixed.0[start_i as usize].mix(&mixed.0[start_i as usize + 1], local_t); | ||||||
|  |             color.into() | ||||||
|  |         }; | ||||||
|  |         transform.translation.x = 600. * (t - 0.5); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Lynn
						Lynn