ferroui_base/animation/transitions/
rotate_3d_transition.rs1use 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
18pub 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 pub fn new() -> Self {
42 Self { base: PageSlide::new(), depth: Cell::new(None) }
43 }
44
45 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 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}