1use crate::AnimatedStyle;
4use animato_core::Easing;
5use animato_spring::SpringConfig;
6use dioxus::prelude::*;
7
8#[derive(Clone, Debug)]
10pub struct PresenceAnimation {
11 pub duration: f32,
13 pub easing: Easing,
15 pub from: AnimatedStyle,
17 pub to: AnimatedStyle,
19 pub spring: Option<SpringConfig>,
21}
22
23impl PartialEq for PresenceAnimation {
24 fn eq(&self, other: &Self) -> bool {
25 self.duration == other.duration
26 && self.easing == other.easing
27 && self.from == other.from
28 && self.to == other.to
29 && spring_eq(self.spring.as_ref(), other.spring.as_ref())
30 }
31}
32
33impl PresenceAnimation {
34 pub fn fade() -> Self {
36 Self::new(
37 AnimatedStyle::new().opacity(0.0),
38 AnimatedStyle::new().opacity(1.0),
39 )
40 }
41
42 pub fn slide_up() -> Self {
44 Self::new(
45 AnimatedStyle::new().opacity(0.0).translate(0.0, 20.0),
46 AnimatedStyle::new().opacity(1.0).translate(0.0, 0.0),
47 )
48 }
49
50 pub fn slide_down() -> Self {
52 Self::new(
53 AnimatedStyle::new().opacity(0.0).translate(0.0, -20.0),
54 AnimatedStyle::new().opacity(1.0).translate(0.0, 0.0),
55 )
56 }
57
58 pub fn slide_left() -> Self {
60 Self::new(
61 AnimatedStyle::new().opacity(0.0).translate(-20.0, 0.0),
62 AnimatedStyle::new().opacity(1.0).translate(0.0, 0.0),
63 )
64 }
65
66 pub fn slide_right() -> Self {
68 Self::new(
69 AnimatedStyle::new().opacity(0.0).translate(20.0, 0.0),
70 AnimatedStyle::new().opacity(1.0).translate(0.0, 0.0),
71 )
72 }
73
74 pub fn zoom_in() -> Self {
76 Self::new(
77 AnimatedStyle::new().opacity(0.0).scale(0.8),
78 AnimatedStyle::new().opacity(1.0).scale(1.0),
79 )
80 }
81
82 pub fn zoom_out() -> Self {
84 Self::new(
85 AnimatedStyle::new().opacity(0.0).scale(1.2),
86 AnimatedStyle::new().opacity(1.0).scale(1.0),
87 )
88 }
89
90 pub fn flip_x() -> Self {
92 Self::new(
93 AnimatedStyle::new()
94 .opacity(0.0)
95 .transform("rotateX(90deg)"),
96 AnimatedStyle::new().opacity(1.0).transform("rotateX(0deg)"),
97 )
98 }
99
100 pub fn flip_y() -> Self {
102 Self::new(
103 AnimatedStyle::new()
104 .opacity(0.0)
105 .transform("rotateY(90deg)"),
106 AnimatedStyle::new().opacity(1.0).transform("rotateY(0deg)"),
107 )
108 }
109
110 pub fn blur_in() -> Self {
112 Self::new(
113 AnimatedStyle::new().opacity(0.0).blur(10.0),
114 AnimatedStyle::new().opacity(1.0).blur(0.0),
115 )
116 }
117
118 pub fn spring(config: SpringConfig) -> Self {
120 let mut animation = Self::zoom_in();
121 animation.spring = Some(config);
122 animation
123 }
124
125 pub fn new(from: AnimatedStyle, to: AnimatedStyle) -> Self {
127 Self {
128 duration: 0.25,
129 easing: Easing::EaseOutCubic,
130 from,
131 to,
132 spring: None,
133 }
134 }
135
136 pub fn reversed(&self) -> Self {
138 Self {
139 duration: self.duration,
140 easing: self.easing.clone(),
141 from: self.to.clone(),
142 to: self.from.clone(),
143 spring: self.spring.clone(),
144 }
145 }
146}
147
148#[component]
150pub fn AnimatePresence(
151 show: Signal<bool>,
153 enter: Option<PresenceAnimation>,
155 exit: Option<PresenceAnimation>,
157 wait_exit: Option<bool>,
159 children: Element,
161) -> Element {
162 let enter = enter.unwrap_or_else(PresenceAnimation::fade);
163 let exit = exit.unwrap_or_else(|| enter.reversed());
164 let wait_exit = wait_exit.unwrap_or(true);
165 let transition = transition_css(enter.duration.max(exit.duration));
166 let enter_style = format!("{}{}", enter.to.to_css(), transition);
167 let exit_style = format!("{}{}", exit.to.to_css(), transition);
168 let exit_display = if wait_exit { "" } else { "display:none;" };
169 let style = if crate::read_signal(show) {
170 enter_style
171 } else {
172 format!("{exit_style}{exit_display}")
173 };
174
175 rsx! {
176 div {
177 style: "{style}",
178 {children}
179 }
180 }
181}
182
183pub(crate) fn transition_css(duration: f32) -> String {
184 let duration = duration.max(0.0);
185 format!(
186 "transition:opacity {duration:.3}s ease, transform {duration:.3}s ease, filter {duration:.3}s ease; will-change:opacity,transform,filter;"
187 )
188}
189
190fn spring_eq(a: Option<&SpringConfig>, b: Option<&SpringConfig>) -> bool {
191 match (a, b) {
192 (Some(a), Some(b)) => {
193 a.stiffness == b.stiffness
194 && a.damping == b.damping
195 && a.mass == b.mass
196 && a.epsilon == b.epsilon
197 }
198 (None, None) => true,
199 _ => false,
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn presets_have_expected_styles() {
209 let fade = PresenceAnimation::fade();
210 assert_eq!(fade.from.opacity, Some(0.0));
211 assert_eq!(fade.to.opacity, Some(1.0));
212
213 let slide = PresenceAnimation::slide_up();
214 assert_eq!(slide.from.translate_y, Some(20.0));
215 assert_eq!(slide.to.translate_y, Some(0.0));
216 }
217
218 #[test]
219 fn spring_presence_keeps_config_and_reverses() {
220 let config = SpringConfig::wobbly();
221 let spring = PresenceAnimation::spring(config.clone());
222 let stored = spring.spring.as_ref().expect("spring config");
223 assert_eq!(stored.stiffness, config.stiffness);
224 assert_eq!(spring.from.scale, Some(0.8));
225
226 let reversed = spring.reversed();
227 assert_eq!(reversed.from.scale, Some(1.0));
228 assert_eq!(reversed.to.scale, Some(0.8));
229 }
230
231 #[test]
232 fn all_presence_presets_are_well_formed() {
233 let presets = [
234 PresenceAnimation::fade(),
235 PresenceAnimation::slide_down(),
236 PresenceAnimation::slide_left(),
237 PresenceAnimation::slide_right(),
238 PresenceAnimation::zoom_in(),
239 PresenceAnimation::zoom_out(),
240 PresenceAnimation::flip_x(),
241 PresenceAnimation::flip_y(),
242 PresenceAnimation::blur_in(),
243 ];
244
245 for preset in presets {
246 assert!(preset.duration > 0.0);
247 assert_eq!(preset.easing, Easing::EaseOutCubic);
248 assert_eq!(preset.from.opacity, Some(0.0));
249 assert_eq!(preset.to.opacity, Some(1.0));
250 assert!(preset.from.to_css().contains("opacity:0;"));
251 assert!(preset.to.to_css().contains("opacity:1;"));
252 }
253 }
254
255 #[test]
256 fn equality_and_transition_css_cover_edge_cases() {
257 assert_eq!(PresenceAnimation::fade(), PresenceAnimation::fade());
258 assert_ne!(
259 PresenceAnimation::spring(SpringConfig::snappy()),
260 PresenceAnimation::spring(SpringConfig::wobbly())
261 );
262 assert_ne!(PresenceAnimation::fade(), PresenceAnimation::slide_up());
263
264 let css = transition_css(-1.0);
265 assert!(css.contains("0.000s"));
266 assert!(css.contains("will-change:opacity,transform,filter;"));
267 }
268}