Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): 🎸 support keyframes for aniamtions #653

Merged
merged 1 commit into from
Nov 14, 2024
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he

## [@Unreleased] - @ReleaseDate

### Features

- **core**: The `keyframes!` macro has been introduced to manage the intermediate steps of animation states. (#653 @M-Adoo)


## [0.4.0-alpha.15] - 2024-11-13

### Features
Expand Down
2 changes: 2 additions & 0 deletions core/src/animation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ mod animate_state;
pub use animate_state::*;
mod stagger;
pub use stagger::Stagger;
mod keyframes;
pub use keyframes::*;

/// Trait to describe how to control the animation.
pub trait Animation {
Expand Down
219 changes: 219 additions & 0 deletions core/src/animation/keyframes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
//! Keyframes are used to manage the intermediate steps of animation states.
//!
//! This offers a straightforward method to define the state value and ensure
//! seamless animation transitions between the keyframes.
//!
//! You can employ the `keyframes!` macro to generate an animated state.
//!
//! # Example
//!
//! As the `keyframes!` macro returns a standard animated state, you can
//! smoothly transition through every change in the state.
//!
//! ```
//! use ribir::prelude::*;
//!
//! let _w = fn_widget! {
//! let mut color_rect = @SizedBox {
//! size: Size::new(100., 100.),
//! background: Color::RED,
//! };
//!
//! let state = part_writer!(&mut color_rect.background);
//!
//! keyframes! {
//! state: state,
//! 0.2 => Color::YELLOW.into(),
//! 0.5 => Color::BLUE.into(),
//! 0.8 => Color::GREEN.into(),
//! }
//! .transition(EasingTransition {
//! duration: Duration::from_millis(1000),
//! easing: easing::LinearEasing
//! });
//!
//! color_rect
//! };
//! ```
//!
//! Alternatively, you can utilize it to create an animation and control it.
//!
//! ```
//! use ribir::prelude::*;
//!
//! let _w = fn_widget! {
//! let mut opacity_rect = @SizedBox { size: Size::new(100., 100.) };
//!
//! let opacity = part_writer!(&mut opacity_rect.opacity);
//! let animate = @Animate {
//! state: keyframes! {
//! state: opacity,
//! 20% => 0.,
//! 50% => 0.5,
//! 80% => 0.,
//! },
//! from: 0.,
//! transition: EasingTransition {
//! duration: Duration::from_millis(1000),
//! easing: easing::LinearEasing
//! }.box_it()
//! };
//!
//! @ $opacity_rect {
//! on_tap: move |_| animate.run()
//! }
//! };
//! ```
#[derive(Debug)]
pub struct KeyFrames<S: AnimateStateSetter> {
/// The state for the keyframes.
pub state: S,
/// The keyframes that specify the animation frames.
frames: Box<[KeyFrame<S::Value>]>,
}

#[derive(Debug)]
pub struct KeyFrame<S> {
pub rate: f32,
pub state_value: S,
}

impl<S: AnimateStateSetter> KeyFrames<S> {
/// Creates a new `KeyFrames` instance with the given state and keyframes.
///
/// # Arguments
///
/// * `state` - The state for the keyframes.
/// * `keyframes` - A vector of `KeyFrame` instances specifying the animation
/// frames.
///
/// # Panics
///
/// Panics if the `keyframes` vector is empty.
///
/// # Remarks
///
/// The keyframes are sorted by their rate in ascending order before being
/// stored.
pub fn new(state: S, mut keyframes: Vec<KeyFrame<S::Value>>) -> Self {
assert!(!keyframes.is_empty(), "KeyFrames must have at least one frame");
keyframes.sort_by(|a, b| a.rate.total_cmp(&b.rate));

Self { state, frames: keyframes.into_boxed_slice() }
}

/// Converts the `KeyFrames` into a `LerpFnState` that can be used for
/// animations.
pub fn into_lerp_fn_state(
self,
) -> LerpFnState<S, impl FnMut(&S::Value, &S::Value, f32) -> S::Value>
where
S::Value: Lerp,
{
let Self { state, frames } = self;
LerpFnState::new(state, move |from, to, rate| {
let idx = frames
.binary_search_by(|f| f.rate.total_cmp(&rate))
.unwrap_or_else(|idx| idx);

if idx == 0 {
let rate = if rate > 0. { rate / frames[0].rate } else { 1. };
from.lerp(&frames[0].state_value, rate)
} else if idx == frames.len() {
let pre_rate = frames[idx - 1].rate;
let rate = (rate - pre_rate) / (1. - pre_rate);
frames[idx - 1].state_value.lerp(to, rate)
} else {
let f1 = &frames[idx - 1];
let f2 = &frames[idx];
let rate = (rate - f1.rate) / (f2.rate - f1.rate);
f1.state_value.lerp(&f2.state_value, rate)
}
})
}
}

/// Creates an animate state from a list of keyframes. This macro accepts the
/// following arguments:
///
/// * A state to use for the keyframes.
/// * A list of pairs of `rate` and `state_value` to specify the keyframes. The
/// `rate` is a float, where 0 represents the start, and 1 indicates the
/// finish, or a percentage suffixed by `%`.
///
/// # Examples
///
/// ```
/// use ribir::prelude::*;
///
/// let value = State::value(100.0);
/// let frames_state = keyframes! {
/// state: value,
/// // accepts value
/// 0.1 => 20.0,
/// 0.5 => 60.0,
/// 90% => 90.0
/// };
/// ```
///
/// Refer to the [module-level documentation](self) for more details on how to
/// use the macro in animations.
#[macro_export]
macro_rules! keyframes {
(state: $state:expr, frames: [ $($f:expr),*] $(,)?) => {
$crate::animation::KeyFrames::new($state, vec![ $($f),*]).into_lerp_fn_state()
};
(state: $state:expr, frames: [ $($f:expr),* ], $l: literal% => $v:expr $(, $($rest:tt)*)?) => {
$crate::keyframes!(
state: $state,
frames: [
$($f,)*
KeyFrame { rate: $l as f32 / 100., state_value: $v }
]
$(, $($rest)*)?
)
};
(state: $state:expr, frames: [$($f:expr),*], $rate: expr => $v:expr $(, $($rest:tt)*)?) => {
$crate::keyframes!(
state: $state,
frames: [
$($f,)*
KeyFrame { rate: $rate, state_value: $v }
]
$(, $($rest)*)?
)
};
(state: $state:expr, $($rest: tt)+) => {
$crate::keyframes!(state: $state, frames: [], $($rest)*)
};
}
pub use keyframes;

use super::{AnimateStateSetter, Lerp, LerpFnState};
#[cfg(test)]
mod tests {
use super::*;
use crate::{animation::animate_state::AnimateState, state::State};

#[test]
fn smoke() {
let state = State::value(1.);
let mut keyframes = keyframes! {
state: state,
0.1 => 0.4,
50% => 2.5,
1. => 1.
};

let p5 = keyframes.calc_lerp_value(&0., &1., 0.05);
assert!(0. < p5 && p5 < 0.4);
let p10 = keyframes.calc_lerp_value(&0., &1., 0.1);
assert_eq!(p10, 0.4);
let p25 = keyframes.calc_lerp_value(&0., &1., 0.25);
assert!(0.4 < p25 && p25 < 2.5);
let p50 = keyframes.calc_lerp_value(&0., &1., 0.5);
assert_eq!(p50, 2.5);
let p100 = keyframes.calc_lerp_value(&0., &1., 1.);
assert_eq!(p100, 1.);
}
}
Loading