use bevy::prelude::*;
use crate::tokens::{Duration, Easing};
pub struct MotionPlugin;
impl Plugin for MotionPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, animate_state_layers);
}
}
pub fn cubic_bezier(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
let mut low = 0.0_f32;
let mut high = 1.0_f32;
let mut mid;
for _ in 0..20 {
mid = (low + high) / 2.0;
let x = bezier_component(mid, x1, x2);
if (x - t).abs() < 0.0001 {
return bezier_component(mid, y1, y2);
}
if x < t {
low = mid;
} else {
high = mid;
}
}
bezier_component((low + high) / 2.0, y1, y2)
}
fn bezier_component(t: f32, p1: f32, p2: f32) -> f32 {
let t2 = t * t;
let t3 = t2 * t;
let mt = 1.0 - t;
let mt2 = mt * mt;
3.0 * mt2 * t * p1 + 3.0 * mt * t2 * p2 + t3
}
pub fn ease(t: f32, easing: Easing) -> f32 {
let t = t.clamp(0.0, 1.0);
let (x1, y1, x2, y2) = easing.control_points();
cubic_bezier(t, x1, y1, x2, y2)
}
pub fn ease_standard(t: f32) -> f32 {
ease(t, Easing::Standard)
}
pub fn ease_standard_accelerate(t: f32) -> f32 {
ease(t, Easing::StandardAccelerate)
}
pub fn ease_standard_decelerate(t: f32) -> f32 {
ease(t, Easing::StandardDecelerate)
}
pub fn ease_emphasized(t: f32) -> f32 {
ease(t, Easing::Emphasized)
}
pub fn ease_emphasized_accelerate(t: f32) -> f32 {
ease(t, Easing::EmphasizedAccelerate)
}
pub fn ease_emphasized_decelerate(t: f32) -> f32 {
ease(t, Easing::EmphasizedDecelerate)
}
pub fn ease_out_cubic(t: f32) -> f32 {
1.0 - (1.0 - t).powi(3)
}
pub fn ease_in_cubic(t: f32) -> f32 {
t * t * t
}
pub fn ease_in_out_cubic(t: f32) -> f32 {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct SpringConfig {
pub stiffness: f32,
pub damping: f32,
pub mass: f32,
}
impl Default for SpringConfig {
fn default() -> Self {
Self {
stiffness: 300.0,
damping: 20.0,
mass: 1.0,
}
}
}
impl SpringConfig {
pub fn bouncy() -> Self {
Self {
stiffness: 400.0,
damping: 15.0,
mass: 1.0,
}
}
pub fn smooth() -> Self {
Self {
stiffness: 300.0,
damping: 30.0,
mass: 1.0,
}
}
pub fn stiff() -> Self {
Self {
stiffness: 500.0,
damping: 40.0,
mass: 1.0,
}
}
pub fn gentle() -> Self {
Self {
stiffness: 150.0,
damping: 20.0,
mass: 1.0,
}
}
}
#[derive(Component, Debug, Clone)]
pub struct SpringAnimation {
pub value: f32,
pub target: f32,
pub velocity: f32,
pub config: SpringConfig,
pub settled: bool,
}
impl SpringAnimation {
pub fn new(initial: f32, target: f32, config: SpringConfig) -> Self {
Self {
value: initial,
target,
velocity: 0.0,
config,
settled: false,
}
}
pub fn update(&mut self, dt: f32) {
if self.settled {
return;
}
let SpringConfig {
stiffness,
damping,
mass,
} = self.config;
let displacement = self.value - self.target;
let spring_force = -stiffness * displacement;
let damping_force = -damping * self.velocity;
let acceleration = (spring_force + damping_force) / mass;
self.velocity += acceleration * dt;
self.value += self.velocity * dt;
if displacement.abs() < 0.001 && self.velocity.abs() < 0.001 {
self.value = self.target;
self.velocity = 0.0;
self.settled = true;
}
}
pub fn set_target(&mut self, target: f32) {
self.target = target;
self.settled = false;
}
pub fn progress(&self) -> f32 {
self.value
}
}
#[derive(Component, Debug, Clone)]
pub struct StateLayer {
pub opacity: f32,
pub target_opacity: f32,
pub timer: Timer,
pub animating: bool,
pub color: Color,
}
impl Default for StateLayer {
fn default() -> Self {
Self {
opacity: 0.0,
target_opacity: 0.0,
timer: Timer::from_seconds(Duration::SHORT3, TimerMode::Once),
animating: false,
color: Color::WHITE,
}
}
}
impl StateLayer {
pub fn new(color: Color) -> Self {
Self { color, ..default() }
}
pub fn on_primary(theme_primary: Color) -> Self {
Self::new(theme_primary)
}
pub fn set_hovered(&mut self) {
self.set_target(Self::HOVER_OPACITY);
}
pub fn set_focused(&mut self) {
self.set_target(Self::FOCUS_OPACITY);
}
pub fn set_pressed(&mut self) {
self.set_target(Self::PRESSED_OPACITY);
}
pub fn set_dragged(&mut self) {
self.set_target(Self::DRAGGED_OPACITY);
}
pub fn clear(&mut self) {
self.set_target(0.0);
}
pub fn set_target(&mut self, target: f32) {
if (self.target_opacity - target).abs() > 0.001 {
self.target_opacity = target;
self.animating = true;
self.timer = Timer::from_seconds(Duration::SHORT3, TimerMode::Once);
}
}
pub fn update(&mut self, dt: f32) {
if !self.animating {
return;
}
self.timer.tick(std::time::Duration::from_secs_f32(dt));
let progress = ease_standard_decelerate(self.timer.fraction());
let start = self.opacity;
let end = self.target_opacity;
self.opacity = start + (end - start) * progress;
if self.timer.is_finished() {
self.opacity = self.target_opacity;
self.animating = false;
}
}
pub fn current_color(&self) -> Color {
self.color.with_alpha(self.opacity)
}
pub const HOVER_OPACITY: f32 = 0.08;
pub const FOCUS_OPACITY: f32 = 0.12;
pub const PRESSED_OPACITY: f32 = 0.12;
pub const DRAGGED_OPACITY: f32 = 0.16;
}
fn animate_state_layers(
time: Res<Time>,
mut state_layers: Query<(&mut StateLayer, Option<&mut BackgroundColor>)>,
) {
for (mut layer, bg_color) in state_layers.iter_mut() {
layer.update(time.delta_secs());
if let Some(mut bg) = bg_color {
*bg = BackgroundColor(layer.current_color());
}
}
}
#[derive(Component, Debug, Clone)]
pub struct AnimatedValue {
pub current: f32,
pub target: f32,
pub duration: f32,
pub elapsed: f32,
pub start: f32,
pub easing: Easing,
pub complete: bool,
}
impl AnimatedValue {
pub fn new(initial: f32) -> Self {
Self {
current: initial,
target: initial,
duration: Duration::MEDIUM2,
elapsed: 0.0,
start: initial,
easing: Easing::Standard,
complete: true,
}
}
pub fn set_target(&mut self, target: f32) {
if (self.target - target).abs() > 0.0001 {
self.start = self.current;
self.target = target;
self.elapsed = 0.0;
self.complete = false;
}
}
pub fn with_duration(mut self, duration: f32) -> Self {
self.duration = duration;
self
}
pub fn with_easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
pub fn update(&mut self, dt: f32) {
if self.complete {
return;
}
self.elapsed += dt;
let progress = (self.elapsed / self.duration).clamp(0.0, 1.0);
let eased = ease(progress, self.easing);
self.current = self.start + (self.target - self.start) * eased;
if progress >= 1.0 {
self.current = self.target;
self.complete = true;
}
}
pub fn value(&self) -> f32 {
self.current
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ease_out_cubic() {
assert!((ease_out_cubic(0.0) - 0.0).abs() < 0.001);
assert!((ease_out_cubic(1.0) - 1.0).abs() < 0.001);
assert!(ease_out_cubic(0.5) > 0.5);
}
#[test]
fn test_ease_in_cubic() {
assert!((ease_in_cubic(0.0) - 0.0).abs() < 0.001);
assert!((ease_in_cubic(1.0) - 1.0).abs() < 0.001);
assert!(ease_in_cubic(0.5) < 0.5);
}
#[test]
fn test_ease_in_out_cubic() {
assert!((ease_in_out_cubic(0.0) - 0.0).abs() < 0.001);
assert!((ease_in_out_cubic(1.0) - 1.0).abs() < 0.001);
assert!((ease_in_out_cubic(0.5) - 0.5).abs() < 0.001);
}
#[test]
fn test_cubic_bezier_linear() {
let result = cubic_bezier(0.5, 0.0, 0.0, 1.0, 1.0);
assert!((result - 0.5).abs() < 0.01);
}
#[test]
fn test_spring_animation() {
let mut spring = SpringAnimation::new(0.0, 1.0, SpringConfig::smooth());
for _ in 0..120 {
spring.update(1.0 / 60.0);
}
assert!(spring.settled);
assert!((spring.value - 1.0).abs() < 0.01);
}
#[test]
fn test_state_layer_opacity() {
let mut layer = StateLayer::default();
assert!((layer.opacity - 0.0).abs() < 0.001);
layer.set_hovered();
assert!((layer.target_opacity - StateLayer::HOVER_OPACITY).abs() < 0.001);
layer.set_pressed();
assert!((layer.target_opacity - StateLayer::PRESSED_OPACITY).abs() < 0.001);
}
#[test]
fn test_animated_value() {
let mut value = AnimatedValue::new(0.0).with_duration(0.3);
value.set_target(100.0);
assert!(!value.complete);
for _ in 0..20 {
value.update(0.02);
}
assert!(value.complete);
assert!((value.current - 100.0).abs() < 0.01);
}
}