Skip to main content

ferroui_base/animation/transitions/
rotate_3d_transition.rs

1use crate::animation::easings::Easing;
2use crate::animation::{
3    Animation, Cue, IAnimationSetter, IPageTransition, IProgressPageTransition, KeyFrame,
4    PageSlide, PageTransitionItem, SlideAxis, TimeSpan,
5};
6use crate::media::{ITransform, Rotate3DTransform};
7use crate::styling::Setter;
8use crate::animation::i_page_transition::{start_async, when_all};
9use crate::threading::{CancellationToken, DispatcherTask};
10use crate::{Ref, Visual};
11use std::cell::Cell;
12use std::ops::Deref;
13use std::rc::Rc;
14
15const SIDE_PEEK_ANGLE: f64 = 24.0;
16const FAR_PEEK_ANGLE: f64 = 38.0;
17
18/// Transitions between two pages by rotating them around a shared axis in
19/// 3D. It is a [`PageSlide`] whose members it derefs to.
20pub struct Rotate3DTransition {
21    base: PageSlide,
22    depth: Cell<Option<f64>>,
23}
24
25impl Default for Rotate3DTransition {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl Deref for Rotate3DTransition {
32    type Target = PageSlide;
33
34    fn deref(&self) -> &PageSlide {
35        &self.base
36    }
37}
38
39impl Rotate3DTransition {
40    /// Creates a horizontal rotation with a zero duration.
41    pub fn new() -> Self {
42        Self { base: PageSlide::new(), depth: Cell::new(None) }
43    }
44
45    /// Creates a rotation with the given duration and axis. `depth`
46    /// defines the depth of the 3D effect; when `None` it is calculated
47    /// automatically.
48    pub fn with_duration(duration: TimeSpan, orientation: SlideAxis, depth: Option<f64>) -> Self {
49        Self { base: PageSlide::with_duration(duration, orientation), depth: Cell::new(depth) }
50    }
51
52    /// Defines the depth of the 3D effect. If `None`, the depth is
53    /// calculated automatically.
54    pub fn depth(&self) -> Option<f64> {
55        self.depth.get()
56    }
57
58    pub fn set_depth(&self, value: Option<f64>) {
59        self.depth.set(value)
60    }
61
62    fn rotate_transform(visual: &Ref<Visual>) -> Ref<Rotate3DTransform> {
63        let existing = visual
64            .render_transform()
65            .and_then(|transform| transform.as_object().and_then(|o| o.to_ref().cast::<Rotate3DTransform>()));
66        match existing {
67            Some(transform) => transform,
68            None => {
69                let transform = Rotate3DTransform::new();
70                let handle: Rc<dyn ITransform> = (&transform).into();
71                visual.set_render_transform(Some(handle));
72                transform
73            }
74        }
75    }
76
77    fn update_visible_items(
78        &self,
79        progress: f64,
80        from: Option<&Ref<Visual>>,
81        to: Option<&Ref<Visual>>,
82        page_length: f64,
83        visible_items: &[PageTransitionItem],
84    ) {
85        let anchor = from.or(to).unwrap_or(&visible_items[0].visual);
86        let Some(parent) = anchor.visual_parent() else { return };
87
88        let horizontal = self.orientation() == SlideAxis::Horizontal;
89        let center = if page_length > 0.0 {
90            page_length
91        } else if horizontal {
92            parent.bounds().width
93        } else {
94            parent.bounds().height
95        };
96        let depth = self.depth.get().unwrap_or(center);
97        let angle_strength = (progress.clamp(0.0, 1.0) * std::f64::consts::PI).sin();
98
99        for item in visible_items {
100            let visual = &item.visual;
101            visual.set_is_visible(true);
102            visual.set_z_index(Self::get_z_index(item.viewport_center_offset));
103
104            let transform = Self::rotate_transform(visual);
105
106            transform.set_depth(depth);
107            transform.set_center_z(-center / 2.0);
108
109            let angle = Self::get_angle_for_offset(item.viewport_center_offset) * angle_strength;
110            if horizontal {
111                transform.set_angle_y(angle);
112                transform.set_angle_x(0.0);
113            } else {
114                transform.set_angle_x(angle);
115                transform.set_angle_y(0.0);
116            }
117        }
118    }
119
120    fn get_angle_for_offset(offset_from_center: f64) -> f64 {
121        if offset_from_center == 0.0 || offset_from_center.is_nan() {
122            return 0.0;
123        }
124        let sign = offset_from_center.signum();
125
126        let distance = offset_from_center.abs();
127        if distance <= 1.0 {
128            return sign * lerp(0.0, SIDE_PEEK_ANGLE, distance);
129        }
130
131        if distance <= 2.0 {
132            return sign * lerp(SIDE_PEEK_ANGLE, FAR_PEEK_ANGLE, distance - 1.0);
133        }
134
135        sign * FAR_PEEK_ANGLE
136    }
137
138    fn get_z_index(offset_from_center: f64) -> i32 {
139        let distance = offset_from_center.abs();
140
141        if distance < 0.5 {
142            return 3;
143        }
144        if distance < 1.5 {
145            return 2;
146        }
147        1
148    }
149}
150
151fn lerp(from: f64, to: f64, t: f64) -> f64 {
152    let t = t.clamp(0.0, 1.0);
153    from + (to - from) * t
154}
155
156impl IPageTransition for Rotate3DTransition {
157    fn start(
158        &self,
159        from: Option<&Ref<Visual>>,
160        to: Option<&Ref<Visual>>,
161        forward: bool,
162        cancellation_token: CancellationToken,
163    ) -> DispatcherTask<()> {
164        let orientation = self.orientation();
165        let depth = self.depth.get();
166        let duration = self.duration();
167        let fill_mode = self.fill_mode();
168        let slide_in_easing = self.slide_in_easing();
169        let slide_out_easing = self.slide_out_easing();
170        let from = from.cloned();
171        let to = to.cloned();
172
173        start_async(async move {
174            if cancellation_token.is_cancellation_requested() {
175                return;
176            }
177
178            let mut tasks = Vec::with_capacity(if from.is_some() && to.is_some() { 2 } else { 1 });
179            let parent = PageSlide::get_visual_parent(from.as_ref(), to.as_ref());
180            let (rotate_property, center) = match orientation {
181                SlideAxis::Vertical => (Rotate3DTransform::angle_x_property(), parent.bounds().height),
182                SlideAxis::Horizontal => (Rotate3DTransform::angle_y_property(), parent.bounds().width),
183            };
184
185            let depth_setter: Rc<dyn IAnimationSetter> =
186                Setter::new(Rotate3DTransform::depth_property(), depth.unwrap_or(center));
187            let center_z_setter: Rc<dyn IAnimationSetter> =
188                Setter::new(Rotate3DTransform::center_z_property(), -center / 2.0);
189
190            let create_key_frame = |cue: f64, rotation: f64, z_index: i32, is_visible: bool| {
191                let setters: [Rc<dyn IAnimationSetter>; 5] = [
192                    Setter::new(rotate_property, rotation),
193                    Setter::new(Visual::z_index_property(), z_index),
194                    Setter::new(Visual::is_visible_property(), is_visible),
195                    center_z_setter.clone(),
196                    depth_setter.clone(),
197                ];
198                KeyFrame::with_cue(Cue::new(cue), setters)
199            };
200
201            let create_animation = |easing: Easing, key_frames: [Ref<KeyFrame>; 3]| {
202                let animation = Animation::new();
203                animation.set_easing(easing);
204                animation.set_duration(duration);
205                animation.set_fill_mode(fill_mode);
206                animation.children().add_range(key_frames);
207                animation
208            };
209
210            let direction = if forward { -1.0 } else { 1.0 };
211
212            if let Some(from) = &from {
213                let animation = create_animation(
214                    slide_out_easing,
215                    [
216                        create_key_frame(0.0, 0.0, 2, true),
217                        create_key_frame(0.5, 45.0 * direction, 1, true),
218                        create_key_frame(1.0, 90.0 * direction, 1, false),
219                    ],
220                );
221
222                tasks.push(animation.run_async_with_clock(from, None, cancellation_token.clone()));
223            }
224
225            if let Some(to) = &to {
226                to.set_is_visible(true);
227                let animation = create_animation(
228                    slide_in_easing,
229                    [
230                        create_key_frame(0.0, 90.0 * -direction, 1, true),
231                        create_key_frame(0.5, 45.0 * -direction, 1, true),
232                        create_key_frame(1.0, 0.0, 2, true),
233                    ],
234                );
235
236                tasks.push(animation.run_async_with_clock(to, None, cancellation_token.clone()));
237            }
238
239            when_all(tasks).await;
240
241            if !cancellation_token.is_cancellation_requested() {
242                if let Some(to) = &to {
243                    to.set_z_index(2);
244                }
245
246                if let Some(from) = &from {
247                    from.set_is_visible(false);
248                    from.set_z_index(1);
249                }
250            }
251        })
252    }
253
254    fn as_progress_page_transition(&self) -> Option<&dyn IProgressPageTransition> {
255        Some(self)
256    }
257
258    fn as_page_slide(&self) -> Option<&PageSlide> {
259        Some(&self.base)
260    }
261}
262
263impl IProgressPageTransition for Rotate3DTransition {
264    fn update(
265        &self,
266        progress: f64,
267        from: Option<&Ref<Visual>>,
268        to: Option<&Ref<Visual>>,
269        forward: bool,
270        page_length: f64,
271        visible_items: &[PageTransitionItem],
272    ) {
273        if !visible_items.is_empty() {
274            self.update_visible_items(progress, from, to, page_length, visible_items);
275            return;
276        }
277
278        if from.is_none() && to.is_none() {
279            return;
280        }
281
282        let parent = PageSlide::get_visual_parent(from, to);
283        let horizontal = self.orientation() == SlideAxis::Horizontal;
284        let center = if page_length > 0.0 {
285            page_length
286        } else if horizontal {
287            parent.bounds().width
288        } else {
289            parent.bounds().height
290        };
291        let depth = self.depth.get().unwrap_or(center);
292        let sign = if forward { 1.0 } else { -1.0 };
293
294        if let Some(from) = from {
295            let ft = Self::rotate_transform(from);
296            ft.set_depth(depth);
297            ft.set_center_z(-center / 2.0);
298            from.set_z_index(if progress < 0.5 { 2 } else { 1 });
299            if horizontal {
300                ft.set_angle_y(-sign * 90.0 * progress);
301            } else {
302                ft.set_angle_x(-sign * 90.0 * progress);
303            }
304        }
305
306        if let Some(to) = to {
307            to.set_is_visible(true);
308            let tt = Self::rotate_transform(to);
309            tt.set_depth(depth);
310            tt.set_center_z(-center / 2.0);
311            to.set_z_index(if progress < 0.5 { 1 } else { 2 });
312            if horizontal {
313                tt.set_angle_y(sign * 90.0 * (1.0 - progress));
314            } else {
315                tt.set_angle_x(sign * 90.0 * (1.0 - progress));
316            }
317        }
318    }
319
320    fn reset(&self, visual: &Ref<Visual>) {
321        visual.set_render_transform(None);
322        visual.set_z_index(0);
323    }
324}