use crate::easing::Easing;
use crate::traits::Update;
use crate::tween::Tween;
#[cfg(not(feature = "std"))]
use alloc::{collections::BTreeMap as HashMap, format, string::String, vec::Vec};
#[cfg(feature = "std")]
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl Rect {
pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn zero() -> Self {
Self {
x: 0.0,
y: 0.0,
width: 0.0,
height: 0.0,
}
}
#[cfg(feature = "wasm-dom")]
pub fn from_element(element: &web_sys::Element) -> Self {
let r = element.get_bounding_client_rect();
Self {
x: r.x() as f32,
y: r.y() as f32,
width: r.width() as f32,
height: r.height() as f32,
}
}
pub fn center(&self) -> [f32; 2] {
[self.x + self.width * 0.5, self.y + self.height * 0.5]
}
}
#[derive(Debug)]
pub struct LayoutAnimation {
pub translate_x: Tween<f32>,
pub translate_y: Tween<f32>,
pub scale_x: Tween<f32>,
pub scale_y: Tween<f32>,
}
impl LayoutAnimation {
fn from_rects(first: &Rect, last: &Rect, duration: f32, easing: Easing) -> Self {
let dx = first.x - last.x;
let dy = first.y - last.y;
let sx = if last.width > 0.0 {
first.width / last.width
} else {
1.0
};
let sy = if last.height > 0.0 {
first.height / last.height
} else {
1.0
};
Self {
translate_x: Tween::new(dx, 0.0)
.duration(duration)
.easing(easing.clone())
.build(),
translate_y: Tween::new(dy, 0.0)
.duration(duration)
.easing(easing.clone())
.build(),
scale_x: Tween::new(sx, 1.0)
.duration(duration)
.easing(easing.clone())
.build(),
scale_y: Tween::new(sy, 1.0)
.duration(duration)
.easing(easing)
.build(),
}
}
pub fn is_complete(&self) -> bool {
self.translate_x.is_complete()
&& self.translate_y.is_complete()
&& self.scale_x.is_complete()
&& self.scale_y.is_complete()
}
pub fn transform(&self) -> (f32, f32, f32, f32) {
(
self.translate_x.value(),
self.translate_y.value(),
self.scale_x.value(),
self.scale_y.value(),
)
}
pub fn css_transform(&self) -> String {
let (tx, ty, sx, sy) = self.transform();
format!("translate({tx}px, {ty}px) scale({sx}, {sy})")
}
}
impl Update for LayoutAnimation {
fn update(&mut self, dt: f32) -> bool {
let a = self.translate_x.update(dt);
let b = self.translate_y.update(dt);
let c = self.scale_x.update(dt);
let d = self.scale_y.update(dt);
a || b || c || d
}
}
#[derive(Debug)]
pub struct LayoutTransition {
pub id: String,
pub animation: LayoutAnimation,
}
#[derive(Debug)]
pub struct SharedElementTransition {
pub source_rect: Rect,
pub target_rect: Rect,
pub animation: LayoutAnimation,
}
impl SharedElementTransition {
pub fn new(source: Rect, target: Rect, duration: f32, easing: Easing) -> Self {
let animation = LayoutAnimation::from_rects(&source, &target, duration, easing);
Self {
source_rect: source,
target_rect: target,
animation,
}
}
pub fn update(&mut self, dt: f32) {
self.animation.update(dt);
}
pub fn css_transform(&self) -> String {
self.animation.css_transform()
}
pub fn is_complete(&self) -> bool {
self.animation.is_complete()
}
}
#[derive(Debug)]
pub struct LayoutAnimator {
tracked: HashMap<String, Rect>,
animations: HashMap<String, LayoutAnimation>,
}
impl LayoutAnimator {
pub fn new() -> Self {
Self {
tracked: HashMap::new(),
animations: HashMap::new(),
}
}
pub fn track(&mut self, id: &str, rect: Rect) {
self.tracked.insert(id.into(), rect);
}
#[cfg(feature = "wasm-dom")]
pub fn track_element(&mut self, element: &web_sys::Element, id: &str) {
self.track(id, Rect::from_element(element));
}
pub fn untrack(&mut self, id: &str) {
self.tracked.remove(id);
self.animations.remove(id);
}
pub fn compute_transitions(
&mut self,
new_rects: &[(&str, Rect)],
duration: f32,
easing: Easing,
) -> Vec<LayoutTransition> {
let mut transitions = Vec::new();
for &(id, new_rect) in new_rects {
if let Some(old_rect) = self.tracked.get(id) {
let dx = (old_rect.x - new_rect.x).abs();
let dy = (old_rect.y - new_rect.y).abs();
let dw = (old_rect.width - new_rect.width).abs();
let dh = (old_rect.height - new_rect.height).abs();
if dx > 0.5 || dy > 0.5 || dw > 0.5 || dh > 0.5 {
let anim =
LayoutAnimation::from_rects(old_rect, &new_rect, duration, easing.clone());
self.animations.insert(
id.into(),
LayoutAnimation::from_rects(old_rect, &new_rect, duration, easing.clone()),
);
transitions.push(LayoutTransition {
id: id.into(),
animation: anim,
});
}
}
self.tracked.insert(id.into(), new_rect);
}
transitions
}
pub fn animate_to_new_positions(
&mut self,
new_rects: &[(&str, Rect)],
duration: f32,
easing: Easing,
) {
let _ = self.compute_transitions(new_rects, duration, easing);
}
pub fn animate_reorder(
&mut self,
old_rects: &[(&str, Rect)],
new_rects: &[(&str, Rect)],
duration: f32,
easing: Easing,
) -> Vec<LayoutTransition> {
for &(id, rect) in old_rects {
self.tracked.insert(id.into(), rect);
}
self.compute_transitions(new_rects, duration, easing)
}
pub fn animate_enter(
&mut self,
id: &str,
target_rect: Rect,
duration: f32,
easing: Easing,
) -> LayoutAnimation {
let from = Rect::new(
target_rect.x + target_rect.width * 0.5,
target_rect.y + target_rect.height * 0.5,
0.0,
0.0,
);
let anim = LayoutAnimation::from_rects(&from, &target_rect, duration, easing.clone());
self.tracked.insert(id.into(), target_rect);
self.animations.insert(
id.into(),
LayoutAnimation::from_rects(&from, &target_rect, duration, easing),
);
anim
}
pub fn animate_exit(
&mut self,
id: &str,
duration: f32,
easing: Easing,
) -> Option<LayoutAnimation> {
let old_rect = self.tracked.remove(id)?;
let cx = old_rect.width * 0.5;
let cy = old_rect.height * 0.5;
let anim = LayoutAnimation {
translate_x: Tween::new(0.0, cx)
.duration(duration)
.easing(easing.clone())
.build(),
translate_y: Tween::new(0.0, cy)
.duration(duration)
.easing(easing.clone())
.build(),
scale_x: Tween::new(1.0, 0.0)
.duration(duration)
.easing(easing.clone())
.build(),
scale_y: Tween::new(1.0, 0.0)
.duration(duration)
.easing(easing)
.build(),
};
Some(anim)
}
pub fn update(&mut self, dt: f32) {
self.animations.retain(|_id, anim| anim.update(dt));
}
pub fn css_transform(&self, id: &str) -> Option<String> {
self.animations.get(id).map(|a| a.css_transform())
}
pub fn is_animating(&self) -> bool {
!self.animations.is_empty()
}
pub fn tracked_count(&self) -> usize {
self.tracked.len()
}
pub fn animation_count(&self) -> usize {
self.animations.len()
}
}
impl Default for LayoutAnimator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rect_center() {
let r = Rect::new(10.0, 20.0, 100.0, 50.0);
let c = r.center();
assert!((c[0] - 60.0).abs() < 1e-6);
assert!((c[1] - 45.0).abs() < 1e-6);
}
#[test]
fn layout_animation_from_rects() {
let first = Rect::new(0.0, 0.0, 100.0, 50.0);
let last = Rect::new(50.0, 100.0, 200.0, 100.0);
let anim = LayoutAnimation::from_rects(&first, &last, 0.3, Easing::Linear);
let (tx, ty, sx, sy) = anim.transform();
assert!((tx - (-50.0)).abs() < 1e-4, "tx={tx}");
assert!((ty - (-100.0)).abs() < 1e-4, "ty={ty}");
assert!((sx - 0.5).abs() < 1e-4, "sx={sx}");
assert!((sy - 0.5).abs() < 1e-4, "sy={sy}");
}
#[test]
fn layout_animation_completes() {
let first = Rect::new(0.0, 0.0, 100.0, 50.0);
let last = Rect::new(50.0, 100.0, 100.0, 50.0);
let mut anim = LayoutAnimation::from_rects(&first, &last, 0.3, Easing::Linear);
assert!(!anim.is_complete());
anim.update(0.3);
assert!(anim.is_complete());
let (tx, ty, sx, sy) = anim.transform();
assert!((tx).abs() < 1e-4);
assert!((ty).abs() < 1e-4);
assert!((sx - 1.0).abs() < 1e-4);
assert!((sy - 1.0).abs() < 1e-4);
}
#[test]
fn css_transform_format() {
let first = Rect::new(10.0, 20.0, 100.0, 50.0);
let last = Rect::new(10.0, 20.0, 100.0, 50.0);
let mut anim = LayoutAnimation::from_rects(&first, &last, 0.1, Easing::Linear);
anim.update(0.1);
let css = anim.css_transform();
assert!(css.contains("translate("));
assert!(css.contains("scale("));
}
#[test]
fn layout_animator_track_and_transition() {
let mut la = LayoutAnimator::new();
la.track("a", Rect::new(0.0, 0.0, 100.0, 50.0));
la.track("b", Rect::new(0.0, 60.0, 100.0, 50.0));
let transitions = la.compute_transitions(
&[
("a", Rect::new(0.0, 60.0, 100.0, 50.0)),
("b", Rect::new(0.0, 0.0, 100.0, 50.0)),
],
0.4,
Easing::EaseOutCubic,
);
assert_eq!(transitions.len(), 2);
assert!(la.is_animating());
}
#[test]
fn layout_animator_no_change() {
let mut la = LayoutAnimator::new();
la.track("a", Rect::new(0.0, 0.0, 100.0, 50.0));
let transitions = la.compute_transitions(
&[("a", Rect::new(0.0, 0.0, 100.0, 50.0))],
0.4,
Easing::Linear,
);
assert_eq!(transitions.len(), 0);
}
#[test]
fn layout_animator_update_completes() {
let mut la = LayoutAnimator::new();
la.track("a", Rect::new(0.0, 0.0, 100.0, 50.0));
la.compute_transitions(
&[("a", Rect::new(0.0, 100.0, 100.0, 50.0))],
0.3,
Easing::Linear,
);
assert!(la.is_animating());
la.update(0.3);
assert!(!la.is_animating());
}
#[test]
fn layout_animator_reorder() {
let mut la = LayoutAnimator::new();
let old = &[
("a", Rect::new(0.0, 0.0, 100.0, 50.0)),
("b", Rect::new(0.0, 60.0, 100.0, 50.0)),
("c", Rect::new(0.0, 120.0, 100.0, 50.0)),
];
let new = &[
("c", Rect::new(0.0, 0.0, 100.0, 50.0)),
("a", Rect::new(0.0, 60.0, 100.0, 50.0)),
("b", Rect::new(0.0, 120.0, 100.0, 50.0)),
];
let transitions = la.animate_reorder(old, new, 0.3, Easing::EaseOutCubic);
assert_eq!(transitions.len(), 3);
}
#[test]
fn layout_animator_enter() {
let mut la = LayoutAnimator::new();
let anim = la.animate_enter(
"new-el",
Rect::new(50.0, 50.0, 100.0, 100.0),
0.3,
Easing::Linear,
);
let (_, _, sx, sy) = anim.transform();
assert!((sx).abs() < 0.01, "sx={sx}");
assert!((sy).abs() < 0.01, "sy={sy}");
}
#[test]
fn layout_animator_exit() {
let mut la = LayoutAnimator::new();
la.track("bye", Rect::new(0.0, 0.0, 100.0, 50.0));
let anim = la.animate_exit("bye", 0.3, Easing::Linear);
assert!(anim.is_some());
assert_eq!(la.tracked_count(), 0);
}
#[test]
fn layout_animator_exit_unknown() {
let mut la = LayoutAnimator::new();
let anim = la.animate_exit("nonexistent", 0.3, Easing::Linear);
assert!(anim.is_none());
}
#[test]
fn shared_element_transition() {
let source = Rect::new(10.0, 10.0, 50.0, 50.0);
let target = Rect::new(100.0, 200.0, 200.0, 200.0);
let mut set = SharedElementTransition::new(source, target, 0.5, Easing::EaseOutCubic);
assert!(!set.is_complete());
set.update(0.5);
assert!(set.is_complete());
let css = set.css_transform();
assert!(css.contains("translate("));
}
}