Skip to main content

ferroui_base/animation/
cross_fade.rs

1use crate::animation::easings::Easing;
2use crate::animation::{
3    Animation, Cue, FillMode, IPageTransition, IProgressPageTransition, KeyFrame, PageTransitionItem, TimeSpan,
4};
5use crate::animation::i_page_transition::{start_async, when_all};
6use crate::styling::Setter;
7use crate::threading::{CancellationToken, DispatcherTask};
8use crate::{Ref, Visual};
9
10const SIDE_PEEK_OPACITY: f64 = 0.72;
11const FAR_PEEK_OPACITY: f64 = 0.42;
12const OUTGOING_DIP: f64 = 0.22;
13const INCOMING_BOOST: f64 = 0.12;
14const PASSIVE_DIP: f64 = 0.05;
15
16/// Defines a cross-fade animation between two visuals.
17pub struct CrossFade {
18    fade_out_animation: Ref<Animation>,
19    fade_in_animation: Ref<Animation>,
20}
21
22impl Default for CrossFade {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl CrossFade {
29    /// Creates a cross-fade with a zero duration.
30    pub fn new() -> Self {
31        Self::with_duration(TimeSpan::ZERO)
32    }
33
34    /// Creates a cross-fade with the given duration.
35    pub fn with_duration(duration: TimeSpan) -> Self {
36        let opacity_animation = |from: f64, to: f64| {
37            let animation = Animation::new();
38            animation.set_fill_mode(FillMode::Forward);
39            animation
40                .children()
41                .add(KeyFrame::with_cue(Cue::new(0.0), [Setter::new(Visual::opacity_property(), from) as _]));
42            animation
43                .children()
44                .add(KeyFrame::with_cue(Cue::new(1.0), [Setter::new(Visual::opacity_property(), to) as _]));
45            animation.set_duration(duration);
46            animation
47        };
48
49        Self { fade_out_animation: opacity_animation(1.0, 0.0), fade_in_animation: opacity_animation(0.0, 1.0) }
50    }
51
52    /// The duration of the animation.
53    pub fn duration(&self) -> TimeSpan {
54        self.fade_out_animation.duration()
55    }
56
57    pub fn set_duration(&self, value: TimeSpan) {
58        self.fade_out_animation.set_duration(value);
59        self.fade_in_animation.set_duration(value);
60    }
61
62    /// The easing function used to fade in.
63    pub fn fade_in_easing(&self) -> Easing {
64        self.fade_in_animation.easing()
65    }
66
67    pub fn set_fade_in_easing(&self, value: impl Into<Easing>) {
68        self.fade_in_animation.set_easing(value)
69    }
70
71    /// The easing function used to fade out.
72    pub fn fade_out_easing(&self) -> Easing {
73        self.fade_out_animation.easing()
74    }
75
76    pub fn set_fade_out_easing(&self, value: impl Into<Easing>) {
77        self.fade_out_animation.set_easing(value)
78    }
79
80    /// The fill mode.
81    pub fn fill_mode(&self) -> FillMode {
82        self.fade_out_animation.fill_mode()
83    }
84
85    pub fn set_fill_mode(&self, value: FillMode) {
86        self.fade_out_animation.set_fill_mode(value);
87        self.fade_in_animation.set_fill_mode(value);
88    }
89
90    /// Starts the animation: fades `from` out and `to` in. When it has
91    /// ended without being cancelled, `from` is hidden.
92    pub fn start_cross_fade(
93        &self,
94        from: Option<&Ref<Visual>>,
95        to: Option<&Ref<Visual>>,
96        cancellation_token: CancellationToken,
97    ) -> DispatcherTask<()> {
98        let fade_out_animation = self.fade_out_animation.clone();
99        let fade_in_animation = self.fade_in_animation.clone();
100        let from = from.cloned();
101        let to = to.cloned();
102
103        start_async(async move {
104            if cancellation_token.is_cancellation_requested() {
105                return;
106            }
107
108            let mut tasks = Vec::new();
109
110            if let Some(from) = &from {
111                tasks.push(fade_out_animation.run_async_with_clock(from, None, cancellation_token.clone()));
112            }
113
114            if let Some(to) = &to {
115                to.set_is_visible(true);
116                tasks.push(fade_in_animation.run_async_with_clock(to, None, cancellation_token.clone()));
117            }
118
119            when_all(tasks).await;
120
121            if let Some(from) = &from {
122                if !cancellation_token.is_cancellation_requested() {
123                    from.set_is_visible(false);
124                }
125            }
126        })
127    }
128
129    fn update_visible_items(
130        progress: f64,
131        from: Option<&Ref<Visual>>,
132        to: Option<&Ref<Visual>>,
133        visible_items: &[PageTransitionItem],
134    ) {
135        let emphasis = (progress.clamp(0.0, 1.0) * std::f64::consts::PI).sin();
136        for item in visible_items {
137            item.visual.set_is_visible(true);
138            let mut opacity = Self::get_opacity_for_offset(item.viewport_center_offset);
139
140            if from.is_some_and(|from| item.visual.ptr_eq(from)) {
141                opacity = FAR_PEEK_OPACITY.max(opacity - (OUTGOING_DIP * emphasis));
142            } else if to.is_some_and(|to| item.visual.ptr_eq(to)) {
143                opacity = 1.0_f64.min(opacity + (INCOMING_BOOST * emphasis));
144            } else {
145                opacity = FAR_PEEK_OPACITY.max(opacity - (PASSIVE_DIP * emphasis));
146            }
147
148            item.visual.set_opacity(opacity);
149        }
150    }
151
152    fn get_opacity_for_offset(offset_from_center: f64) -> f64 {
153        let distance = offset_from_center.abs();
154
155        if distance <= 1.0 {
156            return lerp(1.0, SIDE_PEEK_OPACITY, distance);
157        }
158
159        if distance <= 2.0 {
160            return lerp(SIDE_PEEK_OPACITY, FAR_PEEK_OPACITY, distance - 1.0);
161        }
162
163        FAR_PEEK_OPACITY
164    }
165}
166
167fn lerp(from: f64, to: f64, t: f64) -> f64 {
168    let t = t.clamp(0.0, 1.0);
169    from + (to - from) * t
170}
171
172impl IPageTransition for CrossFade {
173    fn start(
174        &self,
175        from: Option<&Ref<Visual>>,
176        to: Option<&Ref<Visual>>,
177        _forward: bool,
178        cancellation_token: CancellationToken,
179    ) -> DispatcherTask<()> {
180        self.start_cross_fade(from, to, cancellation_token)
181    }
182
183    fn as_progress_page_transition(&self) -> Option<&dyn IProgressPageTransition> {
184        Some(self)
185    }
186}
187
188impl IProgressPageTransition for CrossFade {
189    fn update(
190        &self,
191        progress: f64,
192        from: Option<&Ref<Visual>>,
193        to: Option<&Ref<Visual>>,
194        _forward: bool,
195        _page_length: f64,
196        visible_items: &[PageTransitionItem],
197    ) {
198        if !visible_items.is_empty() {
199            Self::update_visible_items(progress, from, to, visible_items);
200            return;
201        }
202
203        if let Some(from) = from {
204            from.set_opacity(1.0 - progress);
205        }
206        if let Some(to) = to {
207            to.set_is_visible(true);
208            to.set_opacity(progress);
209        }
210    }
211
212    fn reset(&self, visual: &Ref<Visual>) {
213        visual.set_opacity(1.0);
214    }
215}