Skip to main content

ferroui_base/rendering/composition/animations/
natural_motion_animation.rs

1//! WinUI's natural motion animations: `NaturalMotionAnimation`, the scalar and
2//! vector natural motion animations, and the spring animations
3//! (`SpringScalarNaturalMotionAnimation`, `SpringVector2NaturalMotionAnimation`,
4//! `SpringVector3NaturalMotionAnimation`) with their `Compositor` factories.
5//!
6//! A natural motion animation has no duration and no key frames: it moves the
7//! property by a physical model until the model comes to rest. The spring
8//! animations move each component on a damped harmonic oscillator
9//! ([`spring_state`](super::spring_state)) from the initial value (or the
10//! current value of the property) with the initial velocity to the final value
11//! (or the new value of the property when the animation is implicit). They end
12//! by themselves, so a scoped batch waits for them; a spring without damping
13//! never comes to rest and counts as an infinite animation.
14//!
15//! The classes "derive" from each other by embedding and dereferencing, as the
16//! key frame animations do: `SpringScalarNaturalMotionAnimation` dereferences
17//! to `ScalarNaturalMotionAnimation`, which dereferences to
18//! `NaturalMotionAnimation`, which dereferences to `CompositionAnimation`.
19
20use super::{
21    AnimationDelayBehavior, AnimationInstanceFactory, AnimationStopBehavior, CompositionAnimation,
22    ICompositionAnimation, ICompositionAnimationBase, SpringNaturalMotionAnimationInstance,
23    SpringNaturalMotionParameters,
24};
25use crate::numerics::{Vector2, Vector3};
26use crate::rendering::composition::expressions::ExpressionVariant;
27use crate::rendering::composition::server::ServerObjectId;
28use crate::rendering::composition::{AsCompositionObject, CompositionObject, Compositor};
29use std::cell::Cell;
30use std::ops::Deref;
31use std::rc::Rc;
32use std::time::Duration;
33
34/// The damping ratio of a new spring animation: critically damped.
35//
36// Deviation (winui-composition.md): the defaults of `DampingRatio` and
37// `Period` are not documented in the reference the port reads; it chose a
38// critically damped spring with a period of 50 ms.
39pub const DEFAULT_SPRING_DAMPING_RATIO: f32 = 1.0;
40
41/// The period of a new spring animation.
42pub const DEFAULT_SPRING_PERIOD: Duration = Duration::from_millis(50);
43
44/// The base of the natural motion animations (WinUI `NaturalMotionAnimation`):
45/// the delay and the stop behavior.
46pub struct NaturalMotionAnimation {
47    base: CompositionAnimation,
48    delay_behavior: Cell<AnimationDelayBehavior>,
49    delay_time: Cell<Duration>,
50    stop_behavior: Cell<AnimationStopBehavior>,
51}
52
53impl Deref for NaturalMotionAnimation {
54    type Target = CompositionAnimation;
55
56    fn deref(&self) -> &CompositionAnimation {
57        &self.base
58    }
59}
60
61impl NaturalMotionAnimation {
62    fn new(compositor: &Rc<Compositor>) -> NaturalMotionAnimation {
63        NaturalMotionAnimation {
64            base: CompositionAnimation::new(compositor),
65            delay_behavior: Cell::new(AnimationDelayBehavior::default()),
66            delay_time: Cell::new(Duration::ZERO),
67            stop_behavior: Cell::new(AnimationStopBehavior::default()),
68        }
69    }
70
71    /// Whether the property takes the initial value before the delay
72    /// (`SetInitialValueBeforeDelay`) or when the motion starts.
73    pub fn delay_behavior(&self) -> AnimationDelayBehavior {
74        self.delay_behavior.get()
75    }
76
77    pub fn set_delay_behavior(&self, value: AnimationDelayBehavior) {
78        self.delay_behavior.set(value)
79    }
80
81    /// The delay between the start of the animation and the start of the
82    /// motion.
83    pub fn delay_time(&self) -> Duration {
84        self.delay_time.get()
85    }
86
87    pub fn set_delay_time(&self, value: Duration) {
88        self.delay_time.set(value)
89    }
90
91    /// The value the property is set to when the animation is stopped with
92    /// `StopAnimation`.
93    pub fn stop_behavior(&self) -> AnimationStopBehavior {
94        self.stop_behavior.get()
95    }
96
97    pub fn set_stop_behavior(&self, value: AnimationStopBehavior) {
98        self.stop_behavior.set(value)
99    }
100}
101
102/// Expands the natural motion animation of a value type and its spring
103/// animation.
104macro_rules! natural_motion_classes {
105    ($(($motion:ident, $spring:ident, $create:ident, $ty:ty, $type_name:literal)),* $(,)?) => {$(
106        #[doc = concat!(
107            "A natural motion animation of ", $type_name, " values (WinUI `", stringify!($motion), "`): ",
108            "where the motion starts and ends, and its initial velocity."
109        )]
110        pub struct $motion {
111            base: NaturalMotionAnimation,
112            final_value: Cell<Option<$ty>>,
113            initial_value: Cell<Option<$ty>>,
114            initial_velocity: Cell<$ty>,
115        }
116
117        impl Deref for $motion {
118            type Target = NaturalMotionAnimation;
119
120            fn deref(&self) -> &NaturalMotionAnimation {
121                &self.base
122            }
123        }
124
125        impl $motion {
126            fn new(compositor: &Rc<Compositor>) -> $motion {
127                $motion {
128                    base: NaturalMotionAnimation::new(compositor),
129                    final_value: Cell::new(None),
130                    initial_value: Cell::new(None),
131                    initial_velocity: Cell::new(<$ty>::default()),
132                }
133            }
134
135            /// The value the motion comes to rest at. When `None`, an
136            /// implicit animation moves to the new value of the property
137            /// (`this.FinalValue`), and an explicit one to the current value.
138            pub fn final_value(&self) -> Option<$ty> {
139                self.final_value.get()
140            }
141
142            pub fn set_final_value(&self, value: Option<$ty>) {
143                self.final_value.set(value)
144            }
145
146            /// The value the motion starts from; the current value of the
147            /// property when `None`.
148            pub fn initial_value(&self) -> Option<$ty> {
149                self.initial_value.get()
150            }
151
152            pub fn set_initial_value(&self, value: Option<$ty>) {
153                self.initial_value.set(value)
154            }
155
156            /// The velocity the motion starts with, in units of the property
157            /// per second.
158            pub fn initial_velocity(&self) -> $ty {
159                self.initial_velocity.get()
160            }
161
162            pub fn set_initial_velocity(&self, value: $ty) {
163                self.initial_velocity.set(value)
164            }
165        }
166
167        #[doc = concat!(
168            "A spring animation of ", $type_name, " values (WinUI `", stringify!($spring), "`): ",
169            "each component moves on a damped harmonic oscillator of the given damping ratio and period."
170        )]
171        pub struct $spring {
172            base: $motion,
173            damping_ratio: Cell<f32>,
174            period: Cell<Duration>,
175        }
176
177        impl Deref for $spring {
178            type Target = $motion;
179
180            fn deref(&self) -> &$motion {
181                &self.base
182            }
183        }
184
185        impl $spring {
186            pub fn new(compositor: &Rc<Compositor>) -> Rc<$spring> {
187                Rc::new($spring {
188                    base: $motion::new(compositor),
189                    damping_ratio: Cell::new(DEFAULT_SPRING_DAMPING_RATIO),
190                    period: Cell::new(DEFAULT_SPRING_PERIOD),
191                })
192            }
193
194            /// The damping ratio of the spring: 1 (the default) comes to rest
195            /// as fast as possible without overshooting, less overshoots and
196            /// oscillates, more comes to rest slower; 0 oscillates forever.
197            pub fn damping_ratio(&self) -> f32 {
198                self.damping_ratio.get()
199            }
200
201            /// Sets the damping ratio. Panics when it is negative or not a
202            /// number.
203            pub fn set_damping_ratio(&self, value: f32) {
204                if value.is_nan() || value < 0.0 || value.is_infinite() {
205                    panic!("The damping ratio must be a non-negative number");
206                }
207                self.damping_ratio.set(value)
208            }
209
210            /// The period of the oscillation of the spring without damping;
211            /// a shorter period is a stiffer spring.
212            pub fn period(&self) -> Duration {
213                self.period.get()
214            }
215
216            /// Sets the period. Panics when it is zero.
217            pub fn set_period(&self, value: Duration) {
218                if value.is_zero() {
219                    panic!("The period of a spring must be positive");
220                }
221                self.period.set(value)
222            }
223        }
224
225        impl ICompositionAnimationBase for $spring {
226            fn as_composition_animation(&self) -> Option<&dyn ICompositionAnimation> {
227                Some(self)
228            }
229        }
230
231        impl ICompositionAnimation for $spring {
232            fn target(&self) -> Option<String> {
233                self.base.target()
234            }
235
236            fn create_instance(
237                &self,
238                target_object: ServerObjectId,
239                implicit_final_value: Option<ExpressionVariant>,
240            ) -> AnimationInstanceFactory {
241                let parameters = self.create_snapshot_source();
242                // Parsed now: an expression that cannot be parsed fails the
243                // start, as in WinUI.
244                let initial_values = match self.initial_value_expressions().parse_all() {
245                    Ok(initial_values) => initial_values,
246                    Err(error) => panic!("{error}"),
247                };
248                let spring = SpringNaturalMotionParameters::<$ty> {
249                    final_value: self
250                        .base
251                        .final_value()
252                        .or_else(|| implicit_final_value.and_then(|value| value.try_cast::<$ty>())),
253                    initial_value: self.base.initial_value(),
254                    initial_velocity: self.base.initial_velocity(),
255                    damping_ratio: self.damping_ratio(),
256                    period: self.period(),
257                    delay_behavior: self.delay_behavior(),
258                    delay_time: self.delay_time(),
259                    stop_behavior: self.stop_behavior(),
260                };
261                // The animation belongs to the batches that are open now; a
262                // spring without damping never ends.
263                let completion = self.compositor().acquire_animation_completion(spring.damping_ratio <= 0.0);
264                AnimationInstanceFactory::new(move || {
265                    let instance = SpringNaturalMotionAnimationInstance::<$ty>::new(
266                        spring,
267                        target_object,
268                        Rc::new(parameters.build()),
269                    );
270                    instance.set_completion(completion);
271                    instance.set_initial_value_expressions(initial_values);
272                    instance
273                })
274            }
275        }
276
277        impl AsCompositionObject for $spring {
278            fn as_composition_object(&self) -> &CompositionObject {
279                self.object()
280            }
281
282            fn composition_type_name(&self) -> &'static str {
283                stringify!($spring)
284            }
285        }
286
287        impl Compositor {
288            #[doc = concat!("Creates a `", stringify!($spring), "`.")]
289            pub fn $create(&self) -> Rc<$spring> {
290                $spring::new(&self.this_handle())
291            }
292        }
293    )*};
294}
295
296natural_motion_classes! {
297    (ScalarNaturalMotionAnimation, SpringScalarNaturalMotionAnimation, create_spring_scalar_animation, f32, "scalar"),
298    (Vector2NaturalMotionAnimation, SpringVector2NaturalMotionAnimation, create_spring_vector2_animation, Vector2, "`Vector2`"),
299    (Vector3NaturalMotionAnimation, SpringVector3NaturalMotionAnimation, create_spring_vector3_animation, Vector3, "`Vector3`"),
300}