 e563f86a1d
			
		
	
	
		e563f86a1d
		
			
		
	
	
	
	
		
			
			# Objective
Simplify the API surrounding easing curves. Broaden the base of types
that support easing.
## Solution
There is now a single library function, `easing_curve`, which constructs
a unit-parametrized easing curve between two values based on an
`EaseFunction`:
```rust
/// Given a `start` and `end` value, create a curve parametrized over [the unit interval]
/// that connects them, using the given [ease function] to determine the form of the
/// curve in between.
///
/// [the unit interval]: Interval::UNIT
/// [ease function]: EaseFunction
pub fn easing_curve<T: Ease>(start: T, end: T, ease_fn: EaseFunction) -> EasingCurve<T> { //... }
```
As this shows, the type of the output curve is generic only in `T`. In
particular, as long as `T` is `Reflect` (and `FromReflect` etc. — i.e.,
a standard "well-behaved" reflectable type), `EasingCurve<T>` is also
`Reflect`, and there is no special field handling nonsense. Therefore,
`EasingCurve` is the kind of thing that would be able to be easily
changed in an editor. This is made possible by storing the actual
`EaseFunction` on `EasingCurve<T>` instead of indirecting through some
kind of function type (which generally leads to issues with reflection).
The types that can be eased are those that implement a trait `Ease`:
```rust
/// A type whose values can be eased between.
///
/// This requires the construction of an interpolation curve that actually extends
/// beyond the curve segment that connects two values, because an easing curve may
/// extrapolate before the starting value and after the ending value. This is
/// especially common in easing functions that mimic elastic or springlike behavior.
pub trait Ease: Sized {
    /// Given `start` and `end` values, produce a curve with [unlimited domain]
    /// that:
    /// - takes a value equivalent to `start` at `t = 0`
    /// - takes a value equivalent to `end` at `t = 1`
    /// - has constant speed everywhere, including outside of `[0, 1]`
    ///
    /// [unlimited domain]: Interval::EVERYWHERE
    fn interpolating_curve_unbounded(start: &Self, end: &Self) -> impl Curve<Self>;
}
```
(I know, I know, yet *another* interpolation trait. See 'Future
direction'.)
The other existing easing functions from the previous version of this
module have also become new members of `EaseFunction`: `Linear`,
`Steps`, and `Elastic` (which maybe needs a different name). The latter
two are parametrized.
## Testing
Tested using the `easing_functions` example. I also axed the
`cubic_curve` example which was of questionable value and replaced it
with `eased_motion`, which uses this API in the context of animation:
https://github.com/user-attachments/assets/3c802992-6b9b-4b56-aeb1-a47501c29ce2
---
## Future direction
Morally speaking, `Ease` is incredibly similar to `StableInterpolate`.
Probably, we should just merge `StableInterpolate` into `Ease`, and then
make `SmoothNudge` an automatic extension trait of `Ease`. The reason I
didn't do that is that `StableInterpolate` is not implemented for
`VectorSpace` because of concerns about the `Color` types, and I wanted
to avoid controversy. I think that may be a good idea though.
As Alice mentioned before, we should also probably get rid of the
`interpolation` dependency.
The parametrized `Elastic` variant probably also needs some additional
work (e.g. renaming, in/out/in-out variants, etc.) if we want to keep
it.
		
	
			
		
			
				
	
	
		
			150 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			150 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! Demonstrates the application of easing curves to animate a transition.
 | |
| 
 | |
| use std::f32::consts::FRAC_PI_2;
 | |
| 
 | |
| use bevy::{
 | |
|     animation::{AnimationTarget, AnimationTargetId},
 | |
|     color::palettes::css::{ORANGE, SILVER},
 | |
|     math::vec3,
 | |
|     prelude::*,
 | |
| };
 | |
| 
 | |
| fn main() {
 | |
|     App::new()
 | |
|         .add_plugins(DefaultPlugins)
 | |
|         .add_systems(Startup, setup)
 | |
|         .run();
 | |
| }
 | |
| 
 | |
| fn setup(
 | |
|     mut commands: Commands,
 | |
|     mut meshes: ResMut<Assets<Mesh>>,
 | |
|     mut materials: ResMut<Assets<StandardMaterial>>,
 | |
|     mut animation_graphs: ResMut<Assets<AnimationGraph>>,
 | |
|     mut animation_clips: ResMut<Assets<AnimationClip>>,
 | |
| ) {
 | |
|     // Create the animation:
 | |
|     let AnimationInfo {
 | |
|         target_name: animation_target_name,
 | |
|         target_id: animation_target_id,
 | |
|         graph: animation_graph,
 | |
|         node_index: animation_node_index,
 | |
|     } = AnimationInfo::create(&mut animation_graphs, &mut animation_clips);
 | |
| 
 | |
|     // Build an animation player that automatically plays the animation.
 | |
|     let mut animation_player = AnimationPlayer::default();
 | |
|     animation_player.play(animation_node_index).repeat();
 | |
| 
 | |
|     // A cube together with the components needed to animate it
 | |
|     let cube_entity = commands
 | |
|         .spawn((
 | |
|             Mesh3d(meshes.add(Cuboid::from_length(2.0))),
 | |
|             MeshMaterial3d(materials.add(Color::from(ORANGE))),
 | |
|             Transform::from_translation(vec3(-6., 2., 0.)),
 | |
|             animation_target_name,
 | |
|             animation_player,
 | |
|             animation_graph,
 | |
|         ))
 | |
|         .id();
 | |
| 
 | |
|     commands.entity(cube_entity).insert(AnimationTarget {
 | |
|         id: animation_target_id,
 | |
|         player: cube_entity,
 | |
|     });
 | |
| 
 | |
|     // Some light to see something
 | |
|     commands.spawn((
 | |
|         PointLight {
 | |
|             shadows_enabled: true,
 | |
|             intensity: 10_000_000.,
 | |
|             range: 100.0,
 | |
|             ..default()
 | |
|         },
 | |
|         Transform::from_xyz(8., 16., 8.),
 | |
|     ));
 | |
| 
 | |
|     // Ground plane
 | |
|     commands.spawn((
 | |
|         Mesh3d(meshes.add(Plane3d::default().mesh().size(50., 50.))),
 | |
|         MeshMaterial3d(materials.add(Color::from(SILVER))),
 | |
|     ));
 | |
| 
 | |
|     // The camera
 | |
|     commands.spawn((
 | |
|         Camera3d::default(),
 | |
|         Transform::from_xyz(0., 6., 12.).looking_at(Vec3::new(0., 1.5, 0.), Vec3::Y),
 | |
|     ));
 | |
| }
 | |
| 
 | |
| // Holds information about the animation we programmatically create.
 | |
| struct AnimationInfo {
 | |
|     // The name of the animation target (in this case, the text).
 | |
|     target_name: Name,
 | |
|     // The ID of the animation target, derived from the name.
 | |
|     target_id: AnimationTargetId,
 | |
|     // The animation graph asset.
 | |
|     graph: Handle<AnimationGraph>,
 | |
|     // The index of the node within that graph.
 | |
|     node_index: AnimationNodeIndex,
 | |
| }
 | |
| 
 | |
| impl AnimationInfo {
 | |
|     // Programmatically creates the UI animation.
 | |
|     fn create(
 | |
|         animation_graphs: &mut Assets<AnimationGraph>,
 | |
|         animation_clips: &mut Assets<AnimationClip>,
 | |
|     ) -> AnimationInfo {
 | |
|         // Create an ID that identifies the text node we're going to animate.
 | |
|         let animation_target_name = Name::new("Cube");
 | |
|         let animation_target_id = AnimationTargetId::from_name(&animation_target_name);
 | |
| 
 | |
|         // Allocate an animation clip.
 | |
|         let mut animation_clip = AnimationClip::default();
 | |
| 
 | |
|         // Each leg of the translation motion should take 3 seconds.
 | |
|         let animation_domain = interval(0.0, 3.0).unwrap();
 | |
| 
 | |
|         // The easing curve is parametrized over [0, 1], so we reparametrize it and
 | |
|         // then ping-pong, which makes it spend another 3 seconds on the return journey.
 | |
|         let translation_curve = easing_curve(
 | |
|             vec3(-6., 2., 0.),
 | |
|             vec3(6., 2., 0.),
 | |
|             EaseFunction::CubicInOut,
 | |
|         )
 | |
|         .reparametrize_linear(animation_domain)
 | |
|         .expect("this curve has bounded domain, so this should never fail")
 | |
|         .ping_pong()
 | |
|         .expect("this curve has bounded domain, so this should never fail");
 | |
| 
 | |
|         // Something similar for rotation. The repetition here is an illusion caused
 | |
|         // by the symmetry of the cube; it rotates on the forward journey and never
 | |
|         // rotates back.
 | |
|         let rotation_curve = easing_curve(
 | |
|             Quat::IDENTITY,
 | |
|             Quat::from_rotation_y(FRAC_PI_2),
 | |
|             EaseFunction::ElasticInOut,
 | |
|         )
 | |
|         .reparametrize_linear(interval(0.0, 4.0).unwrap())
 | |
|         .expect("this curve has bounded domain, so this should never fail");
 | |
| 
 | |
|         animation_clip
 | |
|             .add_curve_to_target(animation_target_id, TranslationCurve(translation_curve));
 | |
|         animation_clip.add_curve_to_target(animation_target_id, RotationCurve(rotation_curve));
 | |
| 
 | |
|         // Save our animation clip as an asset.
 | |
|         let animation_clip_handle = animation_clips.add(animation_clip);
 | |
| 
 | |
|         // Create an animation graph with that clip.
 | |
|         let (animation_graph, animation_node_index) =
 | |
|             AnimationGraph::from_clip(animation_clip_handle);
 | |
|         let animation_graph_handle = animation_graphs.add(animation_graph);
 | |
| 
 | |
|         AnimationInfo {
 | |
|             target_name: animation_target_name,
 | |
|             target_id: animation_target_id,
 | |
|             graph: animation_graph_handle,
 | |
|             node_index: animation_node_index,
 | |
|         }
 | |
|     }
 | |
| }
 |