use std::collections::HashMap;
use std::time::{Duration, Instant};
use llimphi_layout::{ComputedLayout, Rect};
use vello::kurbo::Affine;
#[derive(Clone, Copy, Debug)]
pub struct Hero {
pub key: u64,
pub duration: Duration,
pub easing: fn(f32) -> f32,
}
#[derive(Default)]
pub struct HeroRegistry {
last: HashMap<u64, Rect>,
tweens: HashMap<u64, Tween>,
}
struct Tween {
from_rect: Rect,
start: Instant,
duration: Duration,
easing: fn(f32) -> f32,
}
impl HeroRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn reconcile<Msg>(
&mut self,
mounted: &mut crate::Mounted<Msg>,
computed: &ComputedLayout,
now: Instant,
) -> bool {
let mut animating = false;
let mut seen: Vec<u64> = Vec::new();
for node in &mut mounted.nodes {
let Some(hero) = node.hero else { continue };
let Some(cur) = computed.get(node.id) else { continue };
seen.push(hero.key);
if let Some(last) = self.last.get(&hero.key).copied() {
if last != cur && !self.tweens.contains_key(&hero.key) {
self.tweens.insert(
hero.key,
Tween {
from_rect: last,
start: now,
duration: hero.duration,
easing: hero.easing,
},
);
}
}
if let Some(tw) = self.tweens.get(&hero.key) {
let elapsed = now.saturating_duration_since(tw.start).as_secs_f32();
let raw = (elapsed / tw.duration.as_secs_f32().max(1e-6)).clamp(0.0, 1.0);
if raw >= 1.0 {
node.transform = None;
self.tweens.remove(&hero.key);
} else {
let t = (tw.easing)(raw);
let back = back_transform(cur, tw.from_rect);
let xf = lerp_affine(back, Affine::IDENTITY, t);
node.transform = Some(xf);
animating = true;
}
}
self.last.insert(hero.key, cur);
}
if self.last.len() != seen.len() {
self.last.retain(|k, _| seen.contains(k));
self.tweens.retain(|k, _| seen.contains(k));
}
animating
}
}
fn back_transform(cur: Rect, from: Rect) -> Affine {
let sx = (from.w as f64) / (cur.w as f64).max(1e-6);
let sy = (from.h as f64) / (cur.h as f64).max(1e-6);
let cx_cur = (cur.x + cur.w * 0.5) as f64;
let cy_cur = (cur.y + cur.h * 0.5) as f64;
let cx_from = (from.x + from.w * 0.5) as f64;
let cy_from = (from.y + from.h * 0.5) as f64;
Affine::translate((cx_from - cx_cur, cy_from - cy_cur)) * Affine::scale_non_uniform(sx, sy)
}
fn lerp_affine(a: Affine, b: Affine, t: f32) -> Affine {
let p = a.as_coeffs();
let q = b.as_coeffs();
let ft = t as f64;
Affine::new([
p[0] + (q[0] - p[0]) * ft,
p[1] + (q[1] - p[1]) * ft,
p[2] + (q[2] - p[2]) * ft,
p[3] + (q[3] - p[3]) * ft,
p[4] + (q[4] - p[4]) * ft,
p[5] + (q[5] - p[5]) * ft,
])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{mount, View};
use llimphi_layout::{LayoutTree, Style};
use llimphi_layout::taffy::prelude::length;
use llimphi_layout::taffy::Size;
fn one(x: f32, y: f32, w: f32, h: f32) -> (crate::Mounted<()>, ComputedLayout) {
let v = View::<()>::new(Style {
size: Size { width: length(w), height: length(h) },
inset: llimphi_layout::taffy::Rect {
left: length(x),
top: length(y),
right: llimphi_layout::taffy::prelude::auto(),
bottom: llimphi_layout::taffy::prelude::auto(),
},
position: llimphi_layout::taffy::Position::Absolute,
..Default::default()
})
.hero(1, Duration::from_millis(200));
let mut layout = LayoutTree::new();
let mounted = mount(&mut layout, v);
let computed = layout
.compute(mounted.root, (1000.0_f32, 1000.0_f32))
.expect("layout");
(mounted, computed)
}
#[test]
fn primera_aparicion_no_anima() {
let mut reg = HeroRegistry::new();
let (mut m, c) = one(10.0, 10.0, 50.0, 50.0);
let animating = reg.reconcile(&mut m, &c, Instant::now());
assert!(!animating, "primera aparición no debe animar");
assert!(m.nodes[0].transform.is_none(), "sin xf en primer frame");
}
#[test]
fn cambio_de_rect_arranca_tween_y_aplica_xf() {
let mut reg = HeroRegistry::new();
let t0 = Instant::now();
let (mut m, c) = one(10.0, 10.0, 50.0, 50.0);
reg.reconcile(&mut m, &c, t0);
let (mut m, c) = one(200.0, 200.0, 100.0, 100.0);
let animating = reg.reconcile(&mut m, &c, t0 + Duration::from_millis(50));
assert!(animating, "cambio de rect → tween");
let xf = m.nodes[0].transform.expect("xf");
let c = xf.as_coeffs();
assert!(c[0] != 1.0 || c[3] != 1.0 || c[4] != 0.0 || c[5] != 0.0,
"xf no debe ser identidad a mitad del tween: {:?}", c);
}
#[test]
fn al_terminar_limpia_la_xf() {
let mut reg = HeroRegistry::new();
let t0 = Instant::now();
let (mut m, c) = one(10.0, 10.0, 50.0, 50.0);
reg.reconcile(&mut m, &c, t0);
let (mut m, c) = one(200.0, 200.0, 100.0, 100.0);
reg.reconcile(&mut m, &c, t0 + Duration::from_millis(10));
let (mut m, c) = one(200.0, 200.0, 100.0, 100.0);
let animating = reg.reconcile(&mut m, &c, t0 + Duration::from_millis(500));
assert!(!animating);
assert!(m.nodes[0].transform.is_none());
}
#[test]
fn back_transform_es_identidad_si_los_rects_coinciden() {
let r = Rect { x: 50.0, y: 50.0, w: 100.0, h: 100.0 };
let xf = back_transform(r, r);
let c = xf.as_coeffs();
assert!((c[0] - 1.0).abs() < 1e-9);
assert!((c[3] - 1.0).abs() < 1e-9);
assert!(c[4].abs() < 1e-9);
assert!(c[5].abs() < 1e-9);
}
}