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