Skip to content
Snippets Groups Projects
transition.rs 10.7 KiB
Newer Older
Louis's avatar
Louis committed
use bevy::prelude::*;
use instant::Instant;
use interpolation::Ease;
use interpolation::EaseFunction;

use crate::{
    children::KChildren,
    context::WidgetName,
    prelude::KayakWidgetContext,
    styles::{ComputedStyles, KStyle},
    widget::Widget,
};

#[derive(Copy, Clone, PartialEq)]
pub enum TransitionEasing {
    Linear,
    QuadraticIn,
    QuadraticOut,
    QuadraticInOut,
    CubicIn,
    CubicOut,
    CubicInOut,
    QuarticIn,
    QuarticOut,
    QuarticInOut,
    QuinticIn,
    QuinticOut,
    QuinticInOut,
    SineIn,
    SineOut,
    SineInOut,
    CircularIn,
    CircularOut,
    CircularInOut,
    ExponentialIn,
    ExponentialOut,
    ExponentialInOut,
    ElasticIn,
    ElasticOut,
    ElasticInOut,
    BackIn,
    BackOut,
    BackInOut,
    BounceIn,
    BounceOut,
    BounceInOut,
}

impl TransitionEasing {
    fn try_into_easing_function(&self) -> Option<EaseFunction> {
        match self {
            TransitionEasing::QuadraticIn => Some(EaseFunction::QuadraticIn),
            TransitionEasing::QuadraticOut => Some(EaseFunction::QuadraticOut),
            TransitionEasing::QuadraticInOut => Some(EaseFunction::QuadraticInOut),
            TransitionEasing::CubicIn => Some(EaseFunction::CubicIn),
            TransitionEasing::CubicOut => Some(EaseFunction::CubicOut),
            TransitionEasing::CubicInOut => Some(EaseFunction::CubicInOut),
            TransitionEasing::QuarticIn => Some(EaseFunction::QuarticIn),
            TransitionEasing::QuarticOut => Some(EaseFunction::QuarticOut),
            TransitionEasing::QuarticInOut => Some(EaseFunction::QuarticInOut),
            TransitionEasing::QuinticIn => Some(EaseFunction::QuinticIn),
            TransitionEasing::QuinticOut => Some(EaseFunction::QuinticOut),
            TransitionEasing::QuinticInOut => Some(EaseFunction::QuinticInOut),
            TransitionEasing::SineIn => Some(EaseFunction::SineIn),
            TransitionEasing::SineOut => Some(EaseFunction::SineOut),
            TransitionEasing::SineInOut => Some(EaseFunction::SineInOut),
            TransitionEasing::CircularIn => Some(EaseFunction::CircularIn),
            TransitionEasing::CircularOut => Some(EaseFunction::CircularOut),
            TransitionEasing::CircularInOut => Some(EaseFunction::CircularInOut),
            TransitionEasing::ExponentialIn => Some(EaseFunction::ExponentialIn),
            TransitionEasing::ExponentialOut => Some(EaseFunction::ExponentialOut),
            TransitionEasing::ExponentialInOut => Some(EaseFunction::ExponentialInOut),
            TransitionEasing::ElasticIn => Some(EaseFunction::ElasticIn),
            TransitionEasing::ElasticOut => Some(EaseFunction::ElasticOut),
            TransitionEasing::ElasticInOut => Some(EaseFunction::ElasticInOut),
            TransitionEasing::BackIn => Some(EaseFunction::BackIn),
            TransitionEasing::BackOut => Some(EaseFunction::BackOut),
            TransitionEasing::BackInOut => Some(EaseFunction::BackInOut),
            TransitionEasing::BounceIn => Some(EaseFunction::BounceIn),
            TransitionEasing::BounceOut => Some(EaseFunction::BounceOut),
            TransitionEasing::BounceInOut => Some(EaseFunction::BounceInOut),
            _ => None,
        }
    }
}

#[derive(Component, Clone, PartialEq)]
pub struct Transition {
    pub playing: bool,
    /// The easing function that dictates the interpolation factor.
    pub easing: TransitionEasing,
    /// Indicates the direction of the animation
    pub reversing: bool,
    /// The start time of the animation.
    start: Instant,
    /// The time in milliseconds until the animation completed.
    pub timeout: f32,
    /// Does the animation loop?
    ///
    /// TODO: Change from boolean to enum that allows disabling the reversing animation.
    pub looping: bool,
    /// The starting styles of the widget.
    pub style_a: KStyle,
    /// The ending styles of the widget.
    pub style_b: KStyle,
}

impl Transition {
    /// Creates a new transition with the given values. Style's that can't be interpolated default to A.
    ///
    /// - transition: The transition props used to create the transition.
    ///
    pub fn new(transition: &TransitionProps) -> Transition {
        Self {
            playing: transition.autoplay,
            easing: transition.easing,
            start: Instant::now(),
            reversing: transition.reversing,
            timeout: transition.timeout,
            looping: transition.looping,
            style_a: transition.style_a.clone(),
            style_b: transition.style_b.clone(),
        }
    }

    pub(crate) fn update(&mut self) -> KStyle {
        let elapsed_time = self.start.elapsed().as_secs_f32() * 1000.0; // as Milliseconds
                                                                        // dbg!(elapsed_time, self.timeout, self.reversing, self.playing);
        if (elapsed_time < self.timeout) && self.playing {
            let mut x = if let Some(easing) = self.easing.try_into_easing_function() {
                Ease::calc((elapsed_time / self.timeout).clamp(0.0, 1.0), easing)
            } else {
                (elapsed_time / self.timeout).clamp(0.0, 1.0)
            };
            if self.reversing {
                x = 1.0 - x;
            }
            self.style_a.lerp(&self.style_b, x)
        } else if self.looping && self.playing {
            // Restart animation
            self.start = Instant::now();
            self.reversing = !self.reversing;
            if self.reversing {
                self.style_b.clone()
            } else {
                self.style_a.clone()
            }
        } else {
            // End of animation
            self.playing = false;
            if self.reversing {
                self.style_a.clone()
            } else {
                self.style_b.clone()
            }
        }
    }

    /// Is the animation currently playing?
    pub fn is_playing(&self) -> bool {
        self.playing
    }

    /// Starts the animation.
    pub fn start(&mut self) {
        self.reversing = false;
        self.start = Instant::now();
        self.playing = true;
    }

    /// Starts the animation in reverse.
    pub fn start_reverse(&mut self) {
        self.reversing = true;
        self.start = Instant::now();
        self.playing = true;
    }
}

impl Default for Transition {
    fn default() -> Self {
        Self {
            playing: false,
            easing: TransitionEasing::Linear,
            start: Instant::now(),
            reversing: false,
            timeout: 0.0,
            looping: Default::default(),
            style_a: KStyle::default(),
            style_b: KStyle::default(),
        }
    }
}

#[derive(Component, Clone, PartialEq)]
pub struct TransitionState {
    pub transition: Transition,
    pub widget_entity: Entity,
}

impl Default for TransitionState {
    fn default() -> Self {
        Self {
            transition: Default::default(),
            widget_entity: Entity::from_raw(0),
        }
    }
}

pub fn create_transition(
    widget_context: &KayakWidgetContext,
    commands: &mut Commands,
    widget_entity: Entity,
    transition: &Transition,
) -> Entity {
    widget_context.use_state(
        commands,
        widget_entity,
        TransitionState {
            transition: transition.clone(),
            widget_entity,
        },
    )
}

pub fn update_transitions(
    mut query: Query<&mut TransitionState>,
    mut computed_styles_query: Query<&mut ComputedStyles>,
) {
    for mut transition in query.iter_mut() {
        let new_styles = transition.transition.update();
        if let Ok(mut computed_styles) = computed_styles_query.get_mut(transition.widget_entity) {
            *computed_styles = ComputedStyles(new_styles);
        }
    }
}

/// The transition props that represent an animation.
///
/// Note: Styles that can't be interpolated will default to `style_a`.
#[derive(Component, Clone, PartialEq)]
pub struct TransitionProps {
    /// The easing function that dictates the interpolation factor.
    pub easing: TransitionEasing,
    /// Indicates the direction of the animation
    pub reversing: bool,
    /// The time in milliseconds until the animation completed.
    pub timeout: f32,
    /// Does the animation loop?
    ///
    /// TODO: Change from boolean to enum that allows disabling the reversing animation.
    pub looping: bool,
    /// The starting styles of the widget.
    pub style_a: KStyle,
    /// The ending styles of the widget.
    pub style_b: KStyle,
    // Should the animation start playing instantly.
    pub autoplay: bool,
}

impl Default for TransitionProps {
    fn default() -> Self {
        Self {
            easing: TransitionEasing::Linear,
            reversing: false,
            timeout: 300.0,
            looping: true,
            style_a: Default::default(),
            style_b: Default::default(),
            autoplay: true,
        }
    }
}

impl Widget for TransitionProps {}

/// A generic widget
/// You can consider this to kind behave like a div in html
/// Accepts: KStyle, OnEvent, and KChildren.
#[derive(Bundle)]
pub struct TransitionBundle {
    pub transition: TransitionProps,
    pub computed_styles: ComputedStyles,
    pub children: KChildren,
    pub widget_name: WidgetName,
}

impl Default for TransitionBundle {
    fn default() -> Self {
        Self {
            transition: Default::default(),
            computed_styles: ComputedStyles::default(),
            children: Default::default(),
            widget_name: TransitionProps::default().get_name(),
        }
    }
}

pub fn render(
    In(entity): In<Entity>,
    widget_context: Res<KayakWidgetContext>,
    mut commands: Commands,
    mut query: Query<(&TransitionProps, &KChildren)>,
    mut transition_state_query: Query<&mut TransitionState>,
) -> bool {
    if let Ok((transition, children)) = query.get_mut(entity) {
        let transition_entity = create_transition(
            &widget_context,
            &mut commands,
            entity,
            &Transition::new(transition),
        );
        if let Ok(mut transition_state) = transition_state_query.get_mut(transition_entity) {
            transition_state.transition.looping = transition.looping;
            transition_state.transition.easing = transition.easing;
            transition_state.transition.style_a = transition.style_a.clone();
            transition_state.transition.style_b = transition.style_b.clone();
            transition_state.transition.timeout = transition.timeout;
        }
        children.process(&widget_context, &mut commands, Some(entity));
    }
    true
}