aura-gui 0.3.5

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Reloj de animaciones: guarda cuándo empezó cada una y calcula su progreso.
//!
//! Una animación solo pide frames mientras corre; al terminar el bucle vuelve a dormir. Las que se repiten
//! para siempre ([`Repeat::Forever`]) mantienen la pantalla despierta: úsalas solo en lo que se está viendo.

use crate::core::color::Rgba;
use crate::core::geometry::{Bounds, Pixels, Point, Size};
use rustc_hash::FxHashMap;
use std::f32::consts::{PI, TAU};
use std::time::{Duration, Instant};

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Easing {
    Linear,
    EaseIn,
    EaseOut,
    EaseInOut,
    /// Curva de CSS `cubic-bezier(x1, y1, x2, y2)`.
    CubicBezier(f32, f32, f32, f32),
    /// Muelle amortiguado que se asienta en la duración. `bounce` 0 = sin rebote, 0.5 = rebota bastante.
    Spring {
        bounce: f32,
    },
}

impl Easing {
    /// La curva estándar de Material (`cubic-bezier(0.2, 0, 0, 1)`).
    pub const STANDARD: Easing = Easing::CubicBezier(0.2, 0.0, 0.0, 1.0);
    /// Muelle suave, como los de iOS.
    pub const SPRING: Easing = Easing::Spring { bounce: 0.15 };

    /// Aplica la curva a un progreso lineal de 0 a 1.
    pub fn apply(self, linear: f32) -> f32 {
        let t = linear.clamp(0.0, 1.0);
        match self {
            Easing::Linear => t,
            Easing::EaseIn => t.powi(3),
            Easing::EaseOut => 1.0 - (1.0 - t).powi(3),
            Easing::EaseInOut if t < 0.5 => 4.0 * t.powi(3),
            Easing::EaseInOut => 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0,
            Easing::CubicBezier(x1, y1, x2, y2) => cubic_bezier(x1, y1, x2, y2, t),
            Easing::Spring { bounce } => spring(bounce, t),
        }
    }
}

fn bezier(a: f32, b: f32, t: f32) -> f32 {
    3.0 * a * (1.0 - t).powi(2) * t + 3.0 * b * (1.0 - t) * t * t + t.powi(3)
}

/// Busca el `t` de la curva cuya x es `x` (Newton, con bisección si la pendiente se anula) y devuelve su y.
fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32, x: f32) -> f32 {
    let mut t = x;
    for _ in 0..8 {
        let error = bezier(x1, x2, t) - x;
        if error.abs() < 1.0e-5 {
            return bezier(y1, y2, t);
        }
        let slope = 3.0 * x1 * (1.0 - t).powi(2) + 6.0 * (x2 - x1) * (1.0 - t) * t + 3.0 * (1.0 - x2) * t * t;
        if slope.abs() < 1.0e-6 {
            break;
        }
        t = (t - error / slope).clamp(0.0, 1.0);
    }
    let (mut low, mut high) = (0.0, 1.0);
    for _ in 0..20 {
        t = (low + high) / 2.0;
        if bezier(x1, x2, t) < x { low = t } else { high = t }
    }
    bezier(y1, y2, t)
}

/// Oscilador armónico amortiguado que va de 0 a 1; la frecuencia hace que se asiente cerca de `t = 1`.
fn spring(bounce: f32, t: f32) -> f32 {
    if t >= 1.0 {
        return 1.0;
    }
    let damping = (1.0 - bounce).clamp(0.05, 1.0);
    let omega = 3.0 * PI;
    if damping >= 1.0 {
        return 1.0 - (1.0 + omega * t) * (-omega * t).exp();
    }
    let damped = omega * (1.0 - damping * damping).sqrt();
    let envelope = (-damping * omega * t).exp();
    1.0 - envelope * ((damped * t).cos() + damping * omega / damped * (damped * t).sin())
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Repeat {
    Once,
    Times(u32),
    Forever,
}

/// Duración en milisegundos, para escribir `Animation::new(ms(300))` sin importar `Duration`.
pub const fn ms(millis: u64) -> Duration {
    Duration::from_millis(millis)
}

#[derive(Clone, Copy, Debug)]
pub struct Animation {
    pub duration: Duration,
    pub easing: Easing,
    pub delay: Duration,
    pub repeat: Repeat,
    /// Al repetir, va y vuelve en lugar de saltar al principio.
    pub alternate: bool,
}

impl Animation {
    pub fn new(duration: Duration) -> Self {
        Self { duration, easing: Easing::EaseOut, delay: Duration::ZERO, repeat: Repeat::Once, alternate: false }
    }

    pub fn easing(mut self, easing: Easing) -> Self {
        self.easing = easing;
        self
    }

    pub fn delay(mut self, delay: Duration) -> Self {
        self.delay = delay;
        self
    }

    pub fn repeat(mut self, repeat: Repeat) -> Self {
        self.repeat = repeat;
        self
    }

    /// Repite para siempre yendo y volviendo (pulsos, indicadores de carga).
    pub fn ping_pong(mut self) -> Self {
        self.repeat = Repeat::Forever;
        self.alternate = true;
        self
    }

    /// Progreso con la curva aplicada tras `elapsed` y si sigue en curso.
    pub fn sample(&self, elapsed: Duration) -> (f32, bool) {
        let Some(active) = elapsed.checked_sub(self.delay) else { return (self.easing.apply(0.0), true) };
        let total = self.duration.as_secs_f32().max(f32::EPSILON);
        let cycles = active.as_secs_f32() / total;
        let limit = match self.repeat {
            Repeat::Once => 1.0,
            Repeat::Times(times) => times.max(1) as f32,
            Repeat::Forever => f32::INFINITY,
        };
        let running = cycles < limit;
        let cycles = cycles.min(limit);
        let cycle = cycles.floor();
        let mut linear = if running { cycles - cycle } else { 1.0 };
        let backwards = self.alternate && if running { cycle % 2.0 == 1.0 } else { (limit - 1.0) % 2.0 == 1.0 };
        if backwards {
            linear = 1.0 - linear;
        }
        (self.easing.apply(linear), running)
    }
}

/// Onda suave entre 0 y 1 con `period` segundos y un desfase (0 a 1) para escalonar varias piezas
/// (indicadores de carga, iconos vivos). Se alimenta con el tiempo de `ViewContext::phase`.
pub fn wave(seconds: f32, period: f32, offset: f32) -> f32 {
    0.5 + 0.5 * ((seconds / period + offset) * TAU).sin()
}

/// Interpolación lineal entre dos valores, para animar cualquier propiedad con el progreso de una animación.
pub trait Lerp: Copy {
    fn lerp(self, to: Self, t: f32) -> Self;
}

impl Lerp for f32 {
    fn lerp(self, to: Self, t: f32) -> Self {
        self + (to - self) * t
    }
}

impl Lerp for Pixels {
    fn lerp(self, to: Self, t: f32) -> Self {
        Pixels(self.0.lerp(to.0, t))
    }
}

impl Lerp for Point {
    fn lerp(self, to: Self, t: f32) -> Self {
        Point::new(self.x.lerp(to.x, t), self.y.lerp(to.y, t))
    }
}

impl Lerp for Size {
    fn lerp(self, to: Self, t: f32) -> Self {
        Size::new(self.width.lerp(to.width, t), self.height.lerp(to.height, t))
    }
}

impl Lerp for Bounds {
    fn lerp(self, to: Self, t: f32) -> Self {
        Bounds::new(self.origin.lerp(to.origin, t), self.size.lerp(to.size, t))
    }
}

impl Lerp for Rgba {
    fn lerp(self, to: Self, t: f32) -> Self {
        Rgba { r: self.r.lerp(to.r, t), g: self.g.lerp(to.g, t), b: self.b.lerp(to.b, t), a: self.a.lerp(to.a, t) }
    }
}

/// Transición en curso de un valor hacia su objetivo.
#[derive(Clone, Copy)]
struct Transition {
    from: f32,
    to: f32,
    started: Instant,
}

#[derive(Default)]
pub struct AnimationClock {
    started: FxHashMap<String, Instant>,
    transitions: FxHashMap<String, Transition>,
}

impl AnimationClock {
    /// Progreso de 0 a 1 (con la curva aplicada) y si sigue en curso.
    pub fn progress(&mut self, id: &str, animation: Animation, now: Instant) -> (f32, bool) {
        let started = *self.started.entry(id.to_owned()).or_insert(now);
        animation.sample(now.saturating_duration_since(started))
    }

    /// Vuelve a empezar desde cero la próxima vez que se consulte.
    pub fn restart(&mut self, id: &str) {
        self.started.remove(id);
    }

    /// Valor que se desliza hacia `target` cada vez que este cambia, desde donde iba (aunque estuviera a mitad).
    /// La primera vez arranca ya en `target`. Devuelve el valor actual y si sigue moviéndose.
    pub fn transition(&mut self, id: &str, target: f32, animation: Animation, now: Instant) -> (f32, bool) {
        let state =
            self.transitions.entry(id.to_owned()).or_insert(Transition { from: target, to: target, started: now });
        let (eased, running) = animation.sample(now.saturating_duration_since(state.started));
        let current = state.from.lerp(state.to, eased);
        if state.to != target {
            *state = Transition { from: current, to: target, started: now };
            return (current, true);
        }
        (current, running && state.from != state.to)
    }
}

#[cfg(test)]
#[path = "animation_tests.rs"]
mod tests;