Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_animation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
31 changes: 31 additions & 0 deletions crates/bevy_animation/src/animatable.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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!(Vec2, f32);
impl_float_animatable!(Vec3A, f32);
Expand All @@ -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]
Expand Down
24 changes: 24 additions & 0 deletions crates/bevy_color/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
type Output = Self;

Expand All @@ -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 {
type Output = Self;

Expand All @@ -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 {
type Output = Self;

Expand All @@ -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 {}
};
}
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
136 changes: 136 additions & 0 deletions examples/animation/color_animation.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}