Skip to main content

blinc_layout/
animated.rs

1//! Animation integration for the fluent element API
2//!
3//! Provides multiple approaches for animating element properties:
4//!
5//! ## 1. Direct AnimatedValue in properties
6//!
7//! Pass animated values directly to property methods:
8//!
9//! ```ignore
10//! let opacity = AnimatedValue::new(ctx.animation_handle(), 1.0, SpringConfig::stiff());
11//! opacity.set_target(0.5);
12//!
13//! div()
14//!     .opacity(opacity.get())
15//!     .scale(scale.get())
16//! ```
17//!
18//! ## 2. Animate builder block
19//!
20//! Use the `animate()` method for declarative transitions:
21//!
22//! ```ignore
23//! div()
24//!     .animate(|a| a
25//!         .opacity(0.0, 1.0)  // from 0 to 1
26//!         .scale(0.8, 1.0)    // from 0.8 to 1
27//!         .with_spring(SpringConfig::wobbly())
28//!     )
29//! ```
30//!
31//! ## 3. With animated value binding
32//!
33//! Bind an animated value to update any property:
34//!
35//! ```ignore
36//! div()
37//!     .with_animated(&opacity_anim, |d, v| d.opacity(v))
38//!     .with_animated(&scale_anim, |d, v| d.scale(v))
39//! ```
40
41use blinc_animation::{AnimatedValue, Easing, SchedulerHandle, SpringConfig};
42
43// ============================================================================
44// Animation Builder
45// ============================================================================
46
47/// A builder for declarative animation transitions
48///
49/// Created via the `animate()` method on elements. Allows specifying
50/// from/to values for various properties with spring or easing configuration.
51pub struct AnimationBuilder {
52    handle: Option<SchedulerHandle>,
53    opacity: Option<(f32, f32)>,
54    scale: Option<(f32, f32)>,
55    translate_x: Option<(f32, f32)>,
56    translate_y: Option<(f32, f32)>,
57    rotate: Option<(f32, f32)>,
58    spring_config: SpringConfig,
59    duration_ms: Option<u32>,
60    easing: Easing,
61    auto_start: bool,
62}
63
64impl Default for AnimationBuilder {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl AnimationBuilder {
71    /// Create a new animation builder
72    pub fn new() -> Self {
73        Self {
74            handle: None,
75            opacity: None,
76            scale: None,
77            translate_x: None,
78            translate_y: None,
79            rotate: None,
80            spring_config: SpringConfig::stiff(),
81            duration_ms: None,
82            easing: Easing::EaseInOut,
83            auto_start: true,
84        }
85    }
86
87    /// Set the scheduler handle for spring animations
88    pub fn with_handle(mut self, handle: SchedulerHandle) -> Self {
89        self.handle = Some(handle);
90        self
91    }
92
93    /// Animate opacity from `from` to `to`
94    pub fn opacity(mut self, from: f32, to: f32) -> Self {
95        self.opacity = Some((from, to));
96        self
97    }
98
99    /// Animate uniform scale from `from` to `to`
100    pub fn scale(mut self, from: f32, to: f32) -> Self {
101        self.scale = Some((from, to));
102        self
103    }
104
105    /// Animate X translation from `from` to `to`
106    pub fn translate_x(mut self, from: f32, to: f32) -> Self {
107        self.translate_x = Some((from, to));
108        self
109    }
110
111    /// Animate Y translation from `from` to `to`
112    pub fn translate_y(mut self, from: f32, to: f32) -> Self {
113        self.translate_y = Some((from, to));
114        self
115    }
116
117    /// Animate rotation from `from` to `to` (in radians)
118    pub fn rotate(mut self, from: f32, to: f32) -> Self {
119        self.rotate = Some((from, to));
120        self
121    }
122
123    /// Animate rotation from `from` to `to` (in degrees)
124    pub fn rotate_deg(self, from: f32, to: f32) -> Self {
125        let to_rad = |deg: f32| deg * std::f32::consts::PI / 180.0;
126        self.rotate(to_rad(from), to_rad(to))
127    }
128
129    /// Use spring physics with the given configuration
130    pub fn with_spring(mut self, config: SpringConfig) -> Self {
131        self.spring_config = config;
132        self.duration_ms = None; // Spring overrides duration
133        self
134    }
135
136    /// Use a gentle spring (good for page transitions)
137    pub fn gentle(self) -> Self {
138        self.with_spring(SpringConfig::gentle())
139    }
140
141    /// Use a wobbly spring (good for playful UI)
142    pub fn wobbly(self) -> Self {
143        self.with_spring(SpringConfig::wobbly())
144    }
145
146    /// Use a stiff spring (good for buttons)
147    pub fn stiff(self) -> Self {
148        self.with_spring(SpringConfig::stiff())
149    }
150
151    /// Use a snappy spring (good for quick responses)
152    pub fn snappy(self) -> Self {
153        self.with_spring(SpringConfig::snappy())
154    }
155
156    /// Use keyframe animation with the given duration and easing
157    pub fn with_duration(mut self, duration_ms: u32) -> Self {
158        self.duration_ms = Some(duration_ms);
159        self
160    }
161
162    /// Set the easing function (for keyframe animations)
163    pub fn with_easing(mut self, easing: Easing) -> Self {
164        self.easing = easing;
165        self
166    }
167
168    /// Don't auto-start the animation
169    pub fn paused(mut self) -> Self {
170        self.auto_start = false;
171        self
172    }
173
174    /// Get the current opacity value (for use in element building)
175    ///
176    /// Returns the `from` value initially, then animates to `to`.
177    pub fn get_opacity(&self) -> Option<f32> {
178        self.opacity.map(|(from, _)| from)
179    }
180
181    /// Get the current scale value
182    pub fn get_scale(&self) -> Option<f32> {
183        self.scale.map(|(from, _)| from)
184    }
185
186    /// Get the current translate_x value
187    pub fn get_translate_x(&self) -> Option<f32> {
188        self.translate_x.map(|(from, _)| from)
189    }
190
191    /// Get the current translate_y value
192    pub fn get_translate_y(&self) -> Option<f32> {
193        self.translate_y.map(|(from, _)| from)
194    }
195
196    /// Get the current rotation value
197    pub fn get_rotate(&self) -> Option<f32> {
198        self.rotate.map(|(from, _)| from)
199    }
200}
201
202// ============================================================================
203// Animated Property Holder
204// ============================================================================
205
206/// Holds animated values for element properties
207///
208/// This is returned by `animate()` and can be stored to access
209/// the current animated values during rebuilds.
210#[derive(Clone)]
211pub struct AnimatedProperties {
212    pub opacity: Option<AnimatedValue>,
213    pub scale: Option<AnimatedValue>,
214    pub translate_x: Option<AnimatedValue>,
215    pub translate_y: Option<AnimatedValue>,
216    pub rotate: Option<AnimatedValue>,
217}
218
219impl AnimatedProperties {
220    /// Create animated properties from an animation builder
221    pub fn from_builder(builder: &AnimationBuilder, handle: SchedulerHandle) -> Self {
222        let config = builder.spring_config;
223
224        let opacity = builder.opacity.map(|(from, to)| {
225            let mut anim = AnimatedValue::new(handle.clone(), from, config);
226            if builder.auto_start {
227                anim.set_target(to);
228            }
229            anim
230        });
231
232        let scale = builder.scale.map(|(from, to)| {
233            let mut anim = AnimatedValue::new(handle.clone(), from, config);
234            if builder.auto_start {
235                anim.set_target(to);
236            }
237            anim
238        });
239
240        let translate_x = builder.translate_x.map(|(from, to)| {
241            let mut anim = AnimatedValue::new(handle.clone(), from, config);
242            if builder.auto_start {
243                anim.set_target(to);
244            }
245            anim
246        });
247
248        let translate_y = builder.translate_y.map(|(from, to)| {
249            let mut anim = AnimatedValue::new(handle.clone(), from, config);
250            if builder.auto_start {
251                anim.set_target(to);
252            }
253            anim
254        });
255
256        let rotate = builder.rotate.map(|(from, to)| {
257            let mut anim = AnimatedValue::new(handle.clone(), from, config);
258            if builder.auto_start {
259                anim.set_target(to);
260            }
261            anim
262        });
263
264        Self {
265            opacity,
266            scale,
267            translate_x,
268            translate_y,
269            rotate,
270        }
271    }
272
273    /// Get current opacity (or 1.0 if not animated)
274    pub fn opacity(&self) -> f32 {
275        self.opacity.as_ref().map(|a| a.get()).unwrap_or(1.0)
276    }
277
278    /// Get current scale (or 1.0 if not animated)
279    pub fn scale(&self) -> f32 {
280        self.scale.as_ref().map(|a| a.get()).unwrap_or(1.0)
281    }
282
283    /// Get current translate_x (or 0.0 if not animated)
284    pub fn translate_x(&self) -> f32 {
285        self.translate_x.as_ref().map(|a| a.get()).unwrap_or(0.0)
286    }
287
288    /// Get current translate_y (or 0.0 if not animated)
289    pub fn translate_y(&self) -> f32 {
290        self.translate_y.as_ref().map(|a| a.get()).unwrap_or(0.0)
291    }
292
293    /// Get current rotation (or 0.0 if not animated)
294    pub fn rotate(&self) -> f32 {
295        self.rotate.as_ref().map(|a| a.get()).unwrap_or(0.0)
296    }
297
298    /// Check if any animations are still running
299    pub fn is_animating(&self) -> bool {
300        self.opacity
301            .as_ref()
302            .map(|a| a.is_animating())
303            .unwrap_or(false)
304            || self
305                .scale
306                .as_ref()
307                .map(|a| a.is_animating())
308                .unwrap_or(false)
309            || self
310                .translate_x
311                .as_ref()
312                .map(|a| a.is_animating())
313                .unwrap_or(false)
314            || self
315                .translate_y
316                .as_ref()
317                .map(|a| a.is_animating())
318                .unwrap_or(false)
319            || self
320                .rotate
321                .as_ref()
322                .map(|a| a.is_animating())
323                .unwrap_or(false)
324    }
325}
326
327// ============================================================================
328// Div Extension for Animations
329// ============================================================================
330
331use crate::div::Div;
332
333impl Div {
334    /// Apply an animation builder to this element
335    ///
336    /// The closure receives an `AnimationBuilder` and should configure
337    /// the desired animations. The initial values are applied immediately.
338    ///
339    /// Note: For the animations to actually run, you need to store the
340    /// `AnimatedProperties` and use their values in subsequent rebuilds.
341    /// For simpler usage, consider using `with_animated()` instead.
342    ///
343    /// # Example
344    ///
345    /// ```ignore
346    /// // In your UI builder:
347    /// let props = ctx.use_animation(|a| a
348    ///     .opacity(0.0, 1.0)
349    ///     .scale(0.8, 1.0)
350    ///     .wobbly()
351    /// );
352    ///
353    /// div()
354    ///     .opacity(props.opacity())
355    ///     .scale(props.scale())
356    /// ```
357    pub fn animate<F>(self, f: F) -> Self
358    where
359        F: FnOnce(AnimationBuilder) -> AnimationBuilder,
360    {
361        let builder = f(AnimationBuilder::new());
362
363        // Apply initial values from the builder
364        let mut result = self;
365
366        if let Some(opacity) = builder.get_opacity() {
367            result = result.opacity(opacity);
368        }
369
370        if let Some(scale) = builder.get_scale() {
371            result = result.scale(scale);
372        }
373
374        // Apply translation transform
375        let tx = builder.get_translate_x().unwrap_or(0.0);
376        let ty = builder.get_translate_y().unwrap_or(0.0);
377        if tx != 0.0 || ty != 0.0 {
378            result = result.translate(tx, ty);
379        }
380
381        if let Some(rot) = builder.get_rotate() {
382            result = result.rotate(rot);
383        }
384
385        result
386    }
387
388    /// Apply an animated value to update this element
389    ///
390    /// The closure receives the current Div and the animated value,
391    /// and should return the modified Div.
392    ///
393    /// # Example
394    ///
395    /// ```ignore
396    /// let opacity = AnimatedValue::new(ctx.animation_handle(), 0.0, SpringConfig::stiff());
397    /// opacity.set_target(1.0);
398    ///
399    /// div()
400    ///     .with_animated(&opacity, |d, v| d.opacity(v))
401    /// ```
402    pub fn with_animated<F>(self, anim: &AnimatedValue, f: F) -> Self
403    where
404        F: FnOnce(Self, f32) -> Self,
405    {
406        f(self, anim.get())
407    }
408
409    /// Apply animated properties to this element
410    ///
411    /// Applies all animated values (opacity, scale, translate, rotate)
412    /// from the `AnimatedProperties` to this element.
413    ///
414    /// # Example
415    ///
416    /// ```ignore
417    /// let props = AnimatedProperties::from_builder(&builder, handle);
418    ///
419    /// div()
420    ///     .apply_animations(&props)
421    /// ```
422    pub fn apply_animations(self, props: &AnimatedProperties) -> Self {
423        let mut result = self.opacity(props.opacity());
424
425        let scale = props.scale();
426        if (scale - 1.0).abs() > 0.001 {
427            result = result.scale(scale);
428        }
429
430        let tx = props.translate_x();
431        let ty = props.translate_y();
432        if tx.abs() > 0.001 || ty.abs() > 0.001 {
433            result = result.translate(tx, ty);
434        }
435
436        let rot = props.rotate();
437        if rot.abs() > 0.001 {
438            result = result.rotate(rot);
439        }
440
441        result
442    }
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448
449    #[test]
450    fn test_animation_builder() {
451        let builder = AnimationBuilder::new()
452            .opacity(0.0, 1.0)
453            .scale(0.5, 1.0)
454            .wobbly();
455
456        assert_eq!(builder.get_opacity(), Some(0.0));
457        assert_eq!(builder.get_scale(), Some(0.5));
458    }
459
460    #[test]
461    fn test_div_animate() {
462        use crate::div::div;
463
464        let d = div()
465            .w(100.0)
466            .animate(|a| a.opacity(0.5, 1.0).scale(0.8, 1.0));
467
468        // The initial values should be applied
469        // (We can't easily test this without accessing private fields)
470        assert!(true);
471    }
472}