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}