1use std::collections::HashMap;
48use std::time::{Duration, Instant};
49
50use vello::kurbo::{Affine, Rect};
51use vello::peniko::{Color, Fill, Mix};
52use vello::Scene;
53
54use crate::Mounted;
55
56#[derive(Clone, Copy, Debug)]
60pub struct Anim {
61 pub key: u64,
62 pub duration: Duration,
63 pub easing: fn(f32) -> f32,
66 pub enter: bool,
72 pub exit: bool,
79 pub enter_from_xf: Option<Affine>,
85 pub switch: Option<u64>,
93}
94
95pub fn ease_out_cubic(t: f32) -> f32 {
99 let u = 1.0 - t.clamp(0.0, 1.0);
100 1.0 - u * u * u
101}
102
103#[derive(Clone, Copy, Debug)]
121pub struct SizeAnim {
122 pub key: u64,
123 pub duration: Duration,
124 pub easing: fn(f32) -> f32,
125}
126
127#[derive(Clone, Copy)]
128struct SizeAnimEntry {
129 from: (f32, f32),
130 to: (f32, f32),
131 start: Instant,
132 duration: Duration,
133 easing: fn(f32) -> f32,
134}
135
136impl SizeAnimEntry {
137 fn settled(target: (f32, f32), now: Instant, _dur: Duration, easing: fn(f32) -> f32) -> Self {
143 Self {
144 from: target,
145 to: target,
146 start: now,
147 duration: Duration::ZERO,
148 easing,
149 }
150 }
151
152 fn t(&self, now: Instant) -> f32 {
153 if self.duration.is_zero() {
154 return 1.0;
155 }
156 let elapsed = now.saturating_duration_since(self.start).as_secs_f32();
157 let raw = (elapsed / self.duration.as_secs_f32()).clamp(0.0, 1.0);
158 (self.easing)(raw)
159 }
160
161 fn value(&self, now: Instant) -> (f32, f32) {
162 let t = self.t(now);
163 let (fw, fh) = self.from;
164 let (tw, th) = self.to;
165 (fw + (tw - fw) * t, fh + (th - fh) * t)
166 }
167
168 fn done(&self, now: Instant) -> bool {
169 now.saturating_duration_since(self.start) >= self.duration
170 }
171}
172
173#[derive(Default)]
177pub struct SizeAnimRegistry {
178 entries: HashMap<u64, SizeAnimEntry>,
179}
180
181impl SizeAnimRegistry {
182 pub fn new() -> Self {
183 Self::default()
184 }
185
186 pub fn clear(&mut self) {
187 self.entries.clear();
188 }
189
190 pub fn is_animating(&self, key: u64, now: Instant) -> bool {
192 self.entries.get(&key).map(|e| !e.done(now)).unwrap_or(false)
193 }
194}
195
196fn try_extract_length_size(
201 style: &llimphi_layout::Style,
202) -> Option<(f32, f32)> {
203 use llimphi_layout::taffy::CompactLength;
204 let w = style.size.width;
205 let h = style.size.height;
206 if w.tag() == CompactLength::LENGTH_TAG && h.tag() == CompactLength::LENGTH_TAG {
207 Some((w.value(), h.value()))
208 } else {
209 None
210 }
211}
212
213fn patch_length_size(style: &mut llimphi_layout::Style, size: (f32, f32)) {
214 use llimphi_layout::taffy::Dimension;
215 style.size.width = Dimension::length(size.0);
216 style.size.height = Dimension::length(size.1);
217}
218
219pub fn reconcile_size_anim<Msg>(
233 view: &mut crate::View<Msg>,
234 reg: &mut SizeAnimRegistry,
235 now: Instant,
236) -> bool {
237 let mut seen: Vec<u64> = Vec::new();
238 let animating = reconcile_size_anim_inner(view, reg, now, &mut seen);
239 if reg.entries.len() != seen.len() {
240 reg.entries.retain(|k, _| seen.contains(k));
241 }
242 animating
243}
244
245fn reconcile_size_anim_inner<Msg>(
246 view: &mut crate::View<Msg>,
247 reg: &mut SizeAnimRegistry,
248 now: Instant,
249 seen: &mut Vec<u64>,
250) -> bool {
251 let mut animating = false;
252 if let Some(sa) = view.animated_size {
253 if let Some(target) = try_extract_length_size(&view.style) {
254 seen.push(sa.key);
255 let entry = reg
256 .entries
257 .entry(sa.key)
258 .or_insert_with(|| SizeAnimEntry::settled(target, now, sa.duration, sa.easing));
259 if entry.to != target {
260 entry.from = entry.value(now);
264 entry.to = target;
265 entry.start = now;
266 entry.duration = sa.duration;
267 entry.easing = sa.easing;
268 }
269 let interp = if entry.done(now) { entry.to } else { entry.value(now) };
270 patch_length_size(&mut view.style, interp);
271 if !entry.done(now) {
272 animating = true;
273 }
274 }
275 }
276 for child in view.children.iter_mut() {
277 if reconcile_size_anim_inner(child, reg, now, seen) {
278 animating = true;
279 }
280 }
281 animating
282}
283
284#[derive(Clone, Copy, PartialEq)]
290struct AnimSnapshot {
291 fill: Option<Color>,
292 radius: f64,
293 alpha: Option<f32>,
294 transform: Option<Affine>,
295}
296
297#[inline]
298fn lerp_f64(a: f64, b: f64, t: f32) -> f64 {
299 a + (b - a) * t as f64
300}
301
302#[inline]
303fn lerp_color(a: Color, b: Color, t: f32) -> Color {
304 let p = a.components;
305 let q = b.components;
306 Color {
307 components: [
308 p[0] + (q[0] - p[0]) * t,
309 p[1] + (q[1] - p[1]) * t,
310 p[2] + (q[2] - p[2]) * t,
311 p[3] + (q[3] - p[3]) * t,
312 ],
313 ..a
314 }
315}
316
317#[inline]
322fn lerp_affine(a: Affine, b: Affine, t: f32) -> Affine {
323 let p = a.as_coeffs();
324 let q = b.as_coeffs();
325 let ft = t as f64;
326 Affine::new([
327 p[0] + (q[0] - p[0]) * ft,
328 p[1] + (q[1] - p[1]) * ft,
329 p[2] + (q[2] - p[2]) * ft,
330 p[3] + (q[3] - p[3]) * ft,
331 p[4] + (q[4] - p[4]) * ft,
332 p[5] + (q[5] - p[5]) * ft,
333 ])
334}
335
336impl AnimSnapshot {
337 fn lerp(self, to: AnimSnapshot, t: f32) -> AnimSnapshot {
341 let fill = match (self.fill, to.fill) {
342 (Some(a), Some(b)) => Some(lerp_color(a, b, t)),
343 _ => to.fill,
344 };
345 let alpha = match (self.alpha, to.alpha) {
349 (None, None) => None,
350 (a, b) => {
351 let from = a.unwrap_or(1.0);
352 let dst = b.unwrap_or(1.0);
353 Some(from + (dst - from) * t)
354 }
355 };
356 let transform = match (self.transform, to.transform) {
361 (None, None) => None,
362 (a, b) => {
363 let from = a.unwrap_or(Affine::IDENTITY);
364 let dst = b.unwrap_or(Affine::IDENTITY);
365 Some(lerp_affine(from, dst, t))
366 }
367 };
368 AnimSnapshot {
369 fill,
370 radius: lerp_f64(self.radius, to.radius, t),
371 alpha,
372 transform,
373 }
374 }
375}
376
377struct AnimEntry {
379 from: AnimSnapshot,
380 to: AnimSnapshot,
381 start: Instant,
382 duration: Duration,
383 easing: fn(f32) -> f32,
384}
385
386impl AnimEntry {
387 fn settled(snap: AnimSnapshot, now: Instant) -> Self {
389 Self {
390 from: snap,
391 to: snap,
392 start: now,
393 duration: Duration::ZERO,
394 easing: |t| t,
395 }
396 }
397
398 fn t(&self, now: Instant) -> f32 {
400 if self.duration.is_zero() {
401 return 1.0;
402 }
403 let elapsed = now.saturating_duration_since(self.start).as_secs_f32();
404 let raw = (elapsed / self.duration.as_secs_f32()).clamp(0.0, 1.0);
405 (self.easing)(raw)
406 }
407
408 fn value(&self, now: Instant) -> AnimSnapshot {
409 self.from.lerp(self.to, self.t(now))
410 }
411
412 fn done(&self, now: Instant) -> bool {
413 now.saturating_duration_since(self.start) >= self.duration
414 }
415}
416
417struct LiveExit {
421 scene: Scene,
422 duration: Duration,
423 easing: fn(f32) -> f32,
424}
425
426struct Ghost {
429 scene: Scene,
430 start: Instant,
431 duration: Duration,
432 easing: fn(f32) -> f32,
433}
434
435impl Ghost {
436 fn alpha(&self, now: Instant) -> f32 {
438 if self.duration.is_zero() {
439 return 0.0;
440 }
441 let elapsed = now.saturating_duration_since(self.start).as_secs_f32();
442 let raw = (elapsed / self.duration.as_secs_f32()).clamp(0.0, 1.0);
443 1.0 - (self.easing)(raw)
444 }
445
446 fn done(&self, now: Instant) -> bool {
447 now.saturating_duration_since(self.start) >= self.duration
448 }
449}
450
451#[derive(Default)]
454pub struct AnimRegistry {
455 entries: HashMap<u64, AnimEntry>,
456 live: HashMap<u64, LiveExit>,
460 ghosts: HashMap<u64, Ghost>,
463 variants: HashMap<u64, u64>,
466}
467
468impl AnimRegistry {
469 pub fn new() -> Self {
470 Self::default()
471 }
472
473 pub fn reconcile<Msg>(&mut self, mounted: &mut Mounted<Msg>, now: Instant) -> bool {
483 let mut animating = false;
484 let mut seen: Vec<u64> = Vec::new();
485 let mut present_live: Vec<u64> = Vec::new();
488 let mut present_exit_only: Vec<u64> = Vec::new();
492 for node in &mut mounted.nodes {
493 let Some(anim) = node.anim else { continue };
494 seen.push(anim.key);
495 let target = AnimSnapshot {
496 fill: node.fill,
497 radius: node.radius,
498 alpha: node.alpha,
499 transform: node.transform,
500 };
501 let mut switched = false;
506 if anim.exit {
507 present_live.push(anim.key);
508 present_exit_only.push(anim.key);
509 } else if let Some(variant) = anim.switch {
510 present_live.push(anim.key);
511 if let Some(prev) = self.variants.insert(anim.key, variant) {
512 if prev != variant {
513 switched = true;
514 if let Some(le) = self.live.remove(&anim.key) {
515 self.ghosts.insert(
516 anim.key,
517 Ghost {
518 scene: le.scene,
519 start: now,
520 duration: le.duration,
521 easing: le.easing,
522 },
523 );
524 }
525 }
526 }
527 }
528 let entry = self.entries.entry(anim.key).or_insert_with(|| {
529 if anim.enter {
533 let from = AnimSnapshot {
534 alpha: Some(0.0),
535 transform: anim.enter_from_xf.or(target.transform),
536 ..target
537 };
538 AnimEntry {
539 from,
540 to: target,
541 start: now,
542 duration: anim.duration,
543 easing: anim.easing,
544 }
545 } else {
546 AnimEntry::settled(target, now)
547 }
548 });
549 if switched {
550 entry.from = AnimSnapshot {
553 alpha: Some(0.0),
554 ..target
555 };
556 entry.to = target;
557 entry.start = now;
558 entry.duration = anim.duration;
559 entry.easing = anim.easing;
560 } else if entry.to != target {
561 entry.from = entry.value(now);
564 entry.to = target;
565 entry.start = now;
566 entry.duration = anim.duration;
567 entry.easing = anim.easing;
568 }
569 let v = if entry.done(now) { entry.to } else { entry.value(now) };
573 node.fill = v.fill;
574 node.radius = v.radius;
575 node.alpha = v.alpha;
576 node.transform = v.transform;
577 if !entry.done(now) {
578 animating = true;
579 }
580 }
581 if self.entries.len() != seen.len() {
582 self.entries.retain(|k, _| seen.contains(k));
583 }
584 if self.variants.len() != seen.len() {
587 self.variants.retain(|k, _| seen.contains(k));
588 }
589
590 let vanished: Vec<u64> = self
597 .live
598 .keys()
599 .filter(|k| !present_live.contains(k))
600 .copied()
601 .collect();
602 for key in vanished {
603 if let Some(le) = self.live.remove(&key) {
604 self.ghosts.insert(
605 key,
606 Ghost {
607 scene: le.scene,
608 start: now,
609 duration: le.duration,
610 easing: le.easing,
611 },
612 );
613 }
614 }
615 for key in &present_exit_only {
616 self.ghosts.remove(key);
617 }
618 self.ghosts.retain(|_, g| !g.done(now));
619 animating || !self.ghosts.is_empty()
620 }
621
622 pub fn live_exit_nodes<Msg>(&self, mounted: &Mounted<Msg>) -> Vec<(usize, usize, u64)> {
628 mounted
629 .nodes
630 .iter()
631 .enumerate()
632 .filter_map(|(idx, n)| {
633 n.anim
634 .filter(|a| a.exit || a.switch.is_some())
635 .map(|a| (idx, n.subtree_end, a.key))
636 })
637 .collect()
638 }
639
640 pub fn store_live_exit(
644 &mut self,
645 key: u64,
646 scene: Scene,
647 duration: Duration,
648 easing: fn(f32) -> f32,
649 ) {
650 self.live.insert(key, LiveExit { scene, duration, easing });
651 }
652
653 pub fn replay_ghosts(&mut self, scene: &mut Scene, now: Instant, w: f32, h: f32) -> bool {
658 if self.ghosts.is_empty() {
659 return false;
660 }
661 let clip = Rect::new(0.0, 0.0, w as f64, h as f64);
662 for g in self.ghosts.values() {
663 let a = g.alpha(now);
664 if a <= 0.0 {
665 continue;
666 }
667 scene.push_layer(Fill::NonZero, Mix::Normal, a, Affine::IDENTITY, &clip);
668 scene.append(&g.scene, None);
669 scene.pop_layer();
670 }
671 true
672 }
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678 use crate::{mount, View};
679 use llimphi_layout::{LayoutTree, Style};
680
681 fn rgba(r: u8, g: u8, b: u8) -> Color {
682 Color::from_rgba8(r, g, b, 255)
683 }
684
685 fn one(fill: Color) -> Mounted<()> {
687 let v = View::<()>::new(Style::default())
688 .fill(fill)
689 .animated(1, Duration::from_millis(200));
690 let mut layout = LayoutTree::new();
691 mount(&mut layout, v)
692 }
693
694 #[test]
695 fn primera_aparicion_no_anima() {
696 let mut reg = AnimRegistry::new();
697 let mut m = one(rgba(255, 0, 0));
698 let now = Instant::now();
699 let animating = reg.reconcile(&mut m, now);
700 assert!(!animating, "la primera vez no debe animar");
701 assert_eq!(m.nodes[0].fill, Some(rgba(255, 0, 0)));
702 }
703
704 #[test]
705 fn cambio_de_color_interpola_y_pide_frames() {
706 let mut reg = AnimRegistry::new();
707 let t0 = Instant::now();
708 let mut m = one(rgba(255, 0, 0));
710 reg.reconcile(&mut m, t0);
711 let mut m = one(rgba(0, 0, 255));
715 let animating = reg.reconcile(&mut m, t0 + Duration::from_millis(100));
716 assert!(animating, "al detectar el cambio debe pedir frames");
717 let mut m = one(rgba(0, 0, 255));
720 let animating = reg.reconcile(&mut m, t0 + Duration::from_millis(200));
721 assert!(animating, "a mitad del tween debe seguir animando");
722 let c = m.nodes[0].fill.expect("fill").components;
723 assert!(c[0] < 1.0 && c[0] > 0.0, "rojo intermedio: {}", c[0]);
724 assert!(c[2] > 0.0 && c[2] < 1.0, "azul intermedio: {}", c[2]);
725 }
726
727 #[test]
728 fn al_terminar_llega_al_objetivo_y_deja_de_pedir_frames() {
729 let mut reg = AnimRegistry::new();
730 let t0 = Instant::now();
731 let mut m = one(rgba(255, 0, 0));
732 reg.reconcile(&mut m, t0);
733 let mut m = one(rgba(0, 0, 255));
734 reg.reconcile(&mut m, t0 + Duration::from_millis(100)); let mut m = one(rgba(0, 0, 255));
737 let animating = reg.reconcile(&mut m, t0 + Duration::from_millis(400));
738 assert!(!animating);
739 assert_eq!(m.nodes[0].fill, Some(rgba(0, 0, 255)));
740 }
741
742 fn one_alpha(alpha: f32) -> Mounted<()> {
744 let v = View::<()>::new(Style::default())
745 .alpha(alpha)
746 .animated(1, Duration::from_millis(200));
747 let mut layout = LayoutTree::new();
748 mount(&mut layout, v)
749 }
750
751 fn one_enter() -> Mounted<()> {
753 let v = View::<()>::new(Style::default())
754 .fill(rgba(10, 20, 30))
755 .animated_enter(1, Duration::from_millis(200));
756 let mut layout = LayoutTree::new();
757 mount(&mut layout, v)
758 }
759
760 #[test]
761 fn fade_in_de_entrada_arranca_transparente_y_llega_a_opaco() {
762 let mut reg = AnimRegistry::new();
763 let t0 = Instant::now();
764 let mut m = one_enter();
767 let animating = reg.reconcile(&mut m, t0);
768 assert!(animating, "la entrada debe animar desde el primer frame");
769 assert_eq!(m.nodes[0].alpha, Some(0.0), "arranca transparente");
770 let mut m = one_enter();
772 reg.reconcile(&mut m, t0 + Duration::from_millis(100));
773 let a = m.nodes[0].alpha.expect("alpha");
774 assert!(a > 0.0 && a < 1.0, "alpha intermedio: {a}");
775 let mut m = one_enter();
777 let animating = reg.reconcile(&mut m, t0 + Duration::from_millis(400));
778 assert!(!animating);
779 assert_eq!(m.nodes[0].alpha, None, "aterriza en opaco sin capa");
780 }
781
782 fn one_exit() -> Mounted<()> {
784 let v = View::<()>::new(Style::default())
785 .fill(rgba(10, 20, 30))
786 .animated_exit(7, Duration::from_millis(200));
787 let mut layout = LayoutTree::new();
788 mount(&mut layout, v)
789 }
790
791 fn empty() -> Mounted<()> {
793 let v = View::<()>::new(Style::default()).fill(rgba(9, 9, 9));
794 let mut layout = LayoutTree::new();
795 mount(&mut layout, v)
796 }
797
798 #[test]
799 fn fade_out_de_salida_promueve_fantasma_y_lo_descarta_al_terminar() {
800 let mut reg = AnimRegistry::new();
801 let t0 = Instant::now();
802 let mut m = one_exit();
805 let animating = reg.reconcile(&mut m, t0);
806 assert!(!animating, "presente y quieto no anima");
807 reg.store_live_exit(7, Scene::new(), Duration::from_millis(200), ease_out_cubic);
808 let mut m = empty();
810 let animating = reg.reconcile(&mut m, t0 + Duration::from_millis(10));
811 assert!(animating, "un fantasma vivo mantiene el ticker");
812 assert!(reg.replay_ghosts(&mut Scene::new(), t0 + Duration::from_millis(10), 100.0, 100.0));
813 let mut m = empty();
815 let animating = reg.reconcile(&mut m, t0 + Duration::from_millis(300));
816 assert!(!animating, "fantasma agotado → sin más frames");
817 assert!(!reg.replay_ghosts(&mut Scene::new(), t0 + Duration::from_millis(300), 100.0, 100.0));
818 }
819
820 fn one_switch(variant: u64) -> Mounted<()> {
822 let v = View::<()>::new(Style::default())
823 .fill(rgba(10, 20, 30))
824 .animated_switch(5, variant, Duration::from_millis(200));
825 let mut layout = LayoutTree::new();
826 mount(&mut layout, v)
827 }
828
829 #[test]
830 fn switch_de_variante_cruza_contenido() {
831 let mut reg = AnimRegistry::new();
832 let t0 = Instant::now();
833 let mut m = one_switch(1);
835 assert!(!reg.reconcile(&mut m, t0), "primera aparición no cruza");
836 reg.store_live_exit(5, Scene::new(), Duration::from_millis(200), ease_out_cubic);
838 let mut m = one_switch(2);
841 let animating = reg.reconcile(&mut m, t0 + Duration::from_millis(10));
842 assert!(animating, "el cross-fade pide frames");
843 let a = m.nodes[0].alpha.expect("alpha de fade-in");
844 assert!(a < 0.3, "el contenido nuevo arranca casi transparente: {a}");
845 assert!(
846 reg.replay_ghosts(&mut Scene::new(), t0 + Duration::from_millis(10), 100.0, 100.0),
847 "hay un fantasma del contenido viejo"
848 );
849 reg.store_live_exit(5, Scene::new(), Duration::from_millis(200), ease_out_cubic);
851 let mut m = one_switch(2);
853 let animating = reg.reconcile(&mut m, t0 + Duration::from_millis(400));
854 assert!(!animating, "asentado tras la duración");
855 assert_eq!(m.nodes[0].alpha, None, "opaco exacto sin capa residual");
856 assert!(
857 !reg.replay_ghosts(&mut Scene::new(), t0 + Duration::from_millis(400), 100.0, 100.0),
858 "fantasma agotado"
859 );
860 }
861
862 #[test]
863 fn switch_misma_variante_no_cruza() {
864 let mut reg = AnimRegistry::new();
865 let t0 = Instant::now();
866 let mut m = one_switch(1);
867 reg.reconcile(&mut m, t0);
868 reg.store_live_exit(5, Scene::new(), Duration::from_millis(200), ease_out_cubic);
869 let mut m = one_switch(1);
871 let animating = reg.reconcile(&mut m, t0 + Duration::from_millis(10));
872 assert!(!animating, "sin cambio de variante no cruza");
873 assert_eq!(m.nodes[0].alpha, None, "el contenido sigue opaco");
874 assert!(!reg.replay_ghosts(&mut Scene::new(), t0 + Duration::from_millis(10), 100.0, 100.0));
875 }
876
877 #[test]
878 fn reaparecer_cancela_el_fantasma() {
879 let mut reg = AnimRegistry::new();
880 let t0 = Instant::now();
881 let mut m = one_exit();
882 reg.reconcile(&mut m, t0);
883 reg.store_live_exit(7, Scene::new(), Duration::from_millis(200), ease_out_cubic);
884 let mut m = empty();
886 assert!(reg.reconcile(&mut m, t0 + Duration::from_millis(10)));
887 let mut m = one_exit();
889 let animating = reg.reconcile(&mut m, t0 + Duration::from_millis(100));
890 assert!(!animating, "al reaparecer no queda fantasma");
891 assert!(!reg.replay_ghosts(&mut Scene::new(), t0 + Duration::from_millis(100), 100.0, 100.0));
892 }
893
894 fn one_xf(xf: Affine) -> Mounted<()> {
896 let v = View::<()>::new(Style::default())
897 .transform(xf)
898 .animated(1, Duration::from_millis(200));
899 let mut layout = LayoutTree::new();
900 mount(&mut layout, v)
901 }
902
903 fn one_pop_in() -> Mounted<()> {
905 let v = View::<()>::new(Style::default())
906 .fill(rgba(1, 2, 3))
907 .animated_enter_from(2, Duration::from_millis(200), Affine::scale(0.5));
908 let mut layout = LayoutTree::new();
909 mount(&mut layout, v)
910 }
911
912 #[test]
913 fn cambio_de_transform_interpola_y_pide_frames() {
914 let mut reg = AnimRegistry::new();
915 let t0 = Instant::now();
916 let mut m = one_xf(Affine::IDENTITY);
918 assert!(!reg.reconcile(&mut m, t0), "primera aparición no anima");
919 let mut m = one_xf(Affine::scale(2.0));
921 let animating = reg.reconcile(&mut m, t0 + Duration::from_millis(50));
922 assert!(animating, "al cambiar la xf debe pedir frames");
923 let mut m = one_xf(Affine::scale(2.0));
925 reg.reconcile(&mut m, t0 + Duration::from_millis(150));
926 let c = m.nodes[0].transform.expect("transform").as_coeffs();
927 assert!(c[0] > 1.0 && c[0] < 2.0, "m00 intermedio: {}", c[0]);
928 assert!(c[3] > 1.0 && c[3] < 2.0, "m11 intermedio: {}", c[3]);
929 }
930
931 #[test]
932 fn transform_al_terminar_llega_exacto() {
933 let mut reg = AnimRegistry::new();
934 let t0 = Instant::now();
935 let mut m = one_xf(Affine::IDENTITY);
936 reg.reconcile(&mut m, t0);
937 let mut m = one_xf(Affine::translate((10.0, 20.0)));
938 reg.reconcile(&mut m, t0 + Duration::from_millis(50));
939 let mut m = one_xf(Affine::translate((10.0, 20.0)));
941 let animating = reg.reconcile(&mut m, t0 + Duration::from_millis(400));
942 assert!(!animating);
943 let c = m.nodes[0].transform.expect("xf").as_coeffs();
944 assert!((c[4] - 10.0).abs() < 1e-9, "tx exacto: {}", c[4]);
945 assert!((c[5] - 20.0).abs() < 1e-9, "ty exacto: {}", c[5]);
946 }
947
948 #[test]
949 fn pop_in_arranca_desde_la_xf_inicial_y_aterriza_sin_xf() {
950 let mut reg = AnimRegistry::new();
951 let t0 = Instant::now();
952 let mut m = one_pop_in();
956 let animating = reg.reconcile(&mut m, t0);
957 assert!(animating, "pop-in anima desde el primer frame");
958 let c = m.nodes[0].transform.expect("xf inicial").as_coeffs();
959 assert!((c[0] - 0.5).abs() < 1e-9, "arranca en scale 0.5: {}", c[0]);
960 let mut m = one_pop_in();
962 reg.reconcile(&mut m, t0 + Duration::from_millis(100));
963 let c = m.nodes[0].transform.expect("xf medio").as_coeffs();
964 assert!(c[0] > 0.5 && c[0] < 1.0, "scale intermedio: {}", c[0]);
965 let mut m = one_pop_in();
967 let animating = reg.reconcile(&mut m, t0 + Duration::from_millis(400));
968 assert!(!animating, "asentado");
969 assert_eq!(m.nodes[0].transform, None, "sin xf residual al asentarse");
970 }
971
972 #[test]
973 fn cambio_de_alpha_interpola() {
974 let mut reg = AnimRegistry::new();
975 let t0 = Instant::now();
976 let mut m = one_alpha(1.0);
978 let animating = reg.reconcile(&mut m, t0);
979 assert!(!animating, "primera aparición sin enter no anima");
980 let mut m = one_alpha(0.0);
982 reg.reconcile(&mut m, t0 + Duration::from_millis(50));
983 let mut m = one_alpha(0.0);
985 reg.reconcile(&mut m, t0 + Duration::from_millis(150));
986 let a = m.nodes[0].alpha.expect("alpha");
987 assert!(a > 0.0 && a < 1.0, "alpha intermedio: {a}");
988 }
989
990 #[test]
991 fn keys_que_se_van_se_descartan() {
992 let mut reg = AnimRegistry::new();
993 let now = Instant::now();
994 let mut m = one(rgba(1, 2, 3));
995 reg.reconcile(&mut m, now);
996 assert_eq!(reg.entries.len(), 1);
997 let v = View::<()>::new(Style::default()).fill(rgba(9, 9, 9));
999 let mut layout = LayoutTree::new();
1000 let mut m2 = mount(&mut layout, v);
1001 reg.reconcile(&mut m2, now);
1002 assert_eq!(reg.entries.len(), 0);
1003 }
1004
1005 fn sized_view(key: u64, w: f32, h: f32, dur_ms: u64) -> View<()> {
1008 use llimphi_layout::taffy::prelude::{length, Size};
1009 let mut style = Style::default();
1010 style.size = Size { width: length(w), height: length(h) };
1011 View::<()>::new(style).animated_size(key, Duration::from_millis(dur_ms))
1012 }
1013
1014 #[test]
1015 fn size_anim_primera_aparicion_no_anima() {
1016 let mut reg = SizeAnimRegistry::new();
1017 let mut v = sized_view(1, 100.0, 80.0, 200);
1018 let now = Instant::now();
1019 let animating = reconcile_size_anim(&mut v, &mut reg, now);
1020 assert!(!animating, "primera vez: sin animación");
1021 let (w, h) = (v.style.size.width.value(), v.style.size.height.value());
1023 assert_eq!((w, h), (100.0, 80.0));
1024 }
1025
1026 #[test]
1027 fn size_anim_cambia_target_interpola() {
1028 let mut reg = SizeAnimRegistry::new();
1029 let t0 = Instant::now();
1030 let mut v = sized_view(1, 100.0, 80.0, 200);
1032 reconcile_size_anim(&mut v, &mut reg, t0);
1033 let mut v = sized_view(1, 200.0, 160.0, 200);
1036 let animating = reconcile_size_anim(&mut v, &mut reg, t0);
1037 assert!(animating, "cambio de target: pide frames");
1038 let (w, h) = (v.style.size.width.value(), v.style.size.height.value());
1039 assert!(w < 200.0 && w >= 100.0, "ancho intermedio: {w}");
1040 assert!(h < 160.0 && h >= 80.0, "alto intermedio: {h}");
1041 let mut v = sized_view(1, 200.0, 160.0, 200);
1043 let animating = reconcile_size_anim(&mut v, &mut reg, t0 + Duration::from_millis(100));
1044 assert!(animating, "a mitad del tween sigue animando");
1045 let (w, h) = (v.style.size.width.value(), v.style.size.height.value());
1046 assert!(w > 100.0 && w < 200.0, "ancho mitad-tween: {w}");
1047 assert!(h > 80.0 && h < 160.0, "alto mitad-tween: {h}");
1048 }
1049
1050 #[test]
1051 fn size_anim_termina_y_se_detiene() {
1052 let mut reg = SizeAnimRegistry::new();
1053 let t0 = Instant::now();
1054 let mut v = sized_view(1, 100.0, 80.0, 200);
1055 reconcile_size_anim(&mut v, &mut reg, t0);
1056 let mut v = sized_view(1, 200.0, 160.0, 200);
1057 reconcile_size_anim(&mut v, &mut reg, t0); let mut v = sized_view(1, 200.0, 160.0, 200);
1060 let animating = reconcile_size_anim(&mut v, &mut reg, t0 + Duration::from_millis(400));
1061 assert!(!animating);
1062 assert_eq!(
1063 (v.style.size.width.value(), v.style.size.height.value()),
1064 (200.0, 160.0),
1065 );
1066 }
1067
1068 #[test]
1069 fn size_anim_no_animable_si_tamano_no_es_length() {
1070 use llimphi_layout::taffy::prelude::{percent, Dimension, Size};
1073 let mut reg = SizeAnimRegistry::new();
1074 let mut style = Style::default();
1075 style.size = Size { width: percent(0.5), height: Dimension::auto() };
1076 let mut v = View::<()>::new(style).animated_size(1, Duration::from_millis(200));
1077 let animating = reconcile_size_anim(&mut v, &mut reg, Instant::now());
1078 assert!(!animating);
1079 use llimphi_layout::taffy::CompactLength;
1081 assert_ne!(v.style.size.width.tag(), CompactLength::LENGTH_TAG);
1082 }
1083
1084 #[test]
1085 fn size_anim_descarta_keys_no_vistas() {
1086 let mut reg = SizeAnimRegistry::new();
1087 let now = Instant::now();
1088 let mut v = sized_view(42, 50.0, 50.0, 200);
1089 reconcile_size_anim(&mut v, &mut reg, now);
1090 assert_eq!(reg.entries.len(), 1);
1091 let mut v: View<()> = View::<()>::new(Style::default());
1093 reconcile_size_anim(&mut v, &mut reg, now);
1094 assert_eq!(reg.entries.len(), 0);
1095 }
1096}