use crate::AnimatedStyle;
use animato_core::Easing;
use animato_spring::SpringConfig;
use dioxus::prelude::*;
#[derive(Clone, Debug)]
pub struct PresenceAnimation {
pub duration: f32,
pub easing: Easing,
pub from: AnimatedStyle,
pub to: AnimatedStyle,
pub spring: Option<SpringConfig>,
}
impl PartialEq for PresenceAnimation {
fn eq(&self, other: &Self) -> bool {
self.duration == other.duration
&& self.easing == other.easing
&& self.from == other.from
&& self.to == other.to
&& spring_eq(self.spring.as_ref(), other.spring.as_ref())
}
}
impl PresenceAnimation {
pub fn fade() -> Self {
Self::new(
AnimatedStyle::new().opacity(0.0),
AnimatedStyle::new().opacity(1.0),
)
}
pub fn slide_up() -> Self {
Self::new(
AnimatedStyle::new().opacity(0.0).translate(0.0, 20.0),
AnimatedStyle::new().opacity(1.0).translate(0.0, 0.0),
)
}
pub fn slide_down() -> Self {
Self::new(
AnimatedStyle::new().opacity(0.0).translate(0.0, -20.0),
AnimatedStyle::new().opacity(1.0).translate(0.0, 0.0),
)
}
pub fn slide_left() -> Self {
Self::new(
AnimatedStyle::new().opacity(0.0).translate(-20.0, 0.0),
AnimatedStyle::new().opacity(1.0).translate(0.0, 0.0),
)
}
pub fn slide_right() -> Self {
Self::new(
AnimatedStyle::new().opacity(0.0).translate(20.0, 0.0),
AnimatedStyle::new().opacity(1.0).translate(0.0, 0.0),
)
}
pub fn zoom_in() -> Self {
Self::new(
AnimatedStyle::new().opacity(0.0).scale(0.8),
AnimatedStyle::new().opacity(1.0).scale(1.0),
)
}
pub fn zoom_out() -> Self {
Self::new(
AnimatedStyle::new().opacity(0.0).scale(1.2),
AnimatedStyle::new().opacity(1.0).scale(1.0),
)
}
pub fn flip_x() -> Self {
Self::new(
AnimatedStyle::new()
.opacity(0.0)
.transform("rotateX(90deg)"),
AnimatedStyle::new().opacity(1.0).transform("rotateX(0deg)"),
)
}
pub fn flip_y() -> Self {
Self::new(
AnimatedStyle::new()
.opacity(0.0)
.transform("rotateY(90deg)"),
AnimatedStyle::new().opacity(1.0).transform("rotateY(0deg)"),
)
}
pub fn blur_in() -> Self {
Self::new(
AnimatedStyle::new().opacity(0.0).blur(10.0),
AnimatedStyle::new().opacity(1.0).blur(0.0),
)
}
pub fn spring(config: SpringConfig) -> Self {
let mut animation = Self::zoom_in();
animation.spring = Some(config);
animation
}
pub fn new(from: AnimatedStyle, to: AnimatedStyle) -> Self {
Self {
duration: 0.25,
easing: Easing::EaseOutCubic,
from,
to,
spring: None,
}
}
pub fn reversed(&self) -> Self {
Self {
duration: self.duration,
easing: self.easing.clone(),
from: self.to.clone(),
to: self.from.clone(),
spring: self.spring.clone(),
}
}
}
#[component]
pub fn AnimatePresence(
show: Signal<bool>,
enter: Option<PresenceAnimation>,
exit: Option<PresenceAnimation>,
wait_exit: Option<bool>,
children: Element,
) -> Element {
let enter = enter.unwrap_or_else(PresenceAnimation::fade);
let exit = exit.unwrap_or_else(|| enter.reversed());
let wait_exit = wait_exit.unwrap_or(true);
let transition = transition_css(enter.duration.max(exit.duration));
let enter_style = format!("{}{}", enter.to.to_css(), transition);
let exit_style = format!("{}{}", exit.to.to_css(), transition);
let exit_display = if wait_exit { "" } else { "display:none;" };
let style = if crate::read_signal(show) {
enter_style
} else {
format!("{exit_style}{exit_display}")
};
rsx! {
div {
style: "{style}",
{children}
}
}
}
pub(crate) fn transition_css(duration: f32) -> String {
let duration = duration.max(0.0);
format!(
"transition:opacity {duration:.3}s ease, transform {duration:.3}s ease, filter {duration:.3}s ease; will-change:opacity,transform,filter;"
)
}
fn spring_eq(a: Option<&SpringConfig>, b: Option<&SpringConfig>) -> bool {
match (a, b) {
(Some(a), Some(b)) => {
a.stiffness == b.stiffness
&& a.damping == b.damping
&& a.mass == b.mass
&& a.epsilon == b.epsilon
}
(None, None) => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn presets_have_expected_styles() {
let fade = PresenceAnimation::fade();
assert_eq!(fade.from.opacity, Some(0.0));
assert_eq!(fade.to.opacity, Some(1.0));
let slide = PresenceAnimation::slide_up();
assert_eq!(slide.from.translate_y, Some(20.0));
assert_eq!(slide.to.translate_y, Some(0.0));
}
#[test]
fn spring_presence_keeps_config_and_reverses() {
let config = SpringConfig::wobbly();
let spring = PresenceAnimation::spring(config.clone());
let stored = spring.spring.as_ref().expect("spring config");
assert_eq!(stored.stiffness, config.stiffness);
assert_eq!(spring.from.scale, Some(0.8));
let reversed = spring.reversed();
assert_eq!(reversed.from.scale, Some(1.0));
assert_eq!(reversed.to.scale, Some(0.8));
}
#[test]
fn all_presence_presets_are_well_formed() {
let presets = [
PresenceAnimation::fade(),
PresenceAnimation::slide_down(),
PresenceAnimation::slide_left(),
PresenceAnimation::slide_right(),
PresenceAnimation::zoom_in(),
PresenceAnimation::zoom_out(),
PresenceAnimation::flip_x(),
PresenceAnimation::flip_y(),
PresenceAnimation::blur_in(),
];
for preset in presets {
assert!(preset.duration > 0.0);
assert_eq!(preset.easing, Easing::EaseOutCubic);
assert_eq!(preset.from.opacity, Some(0.0));
assert_eq!(preset.to.opacity, Some(1.0));
assert!(preset.from.to_css().contains("opacity:0;"));
assert!(preset.to.to_css().contains("opacity:1;"));
}
}
#[test]
fn equality_and_transition_css_cover_edge_cases() {
assert_eq!(PresenceAnimation::fade(), PresenceAnimation::fade());
assert_ne!(
PresenceAnimation::spring(SpringConfig::snappy()),
PresenceAnimation::spring(SpringConfig::wobbly())
);
assert_ne!(PresenceAnimation::fade(), PresenceAnimation::slide_up());
let css = transition_css(-1.0);
assert!(css.contains("0.000s"));
assert!(css.contains("will-change:opacity,transform,filter;"));
}
}