ferroui_base/animation/
cross_fade.rs1use 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
16pub 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 pub fn new() -> Self {
31 Self::with_duration(TimeSpan::ZERO)
32 }
33
34 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 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 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 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 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 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}