use super::Color;
use crate::animation::{AnimationProperty, AnimationTarget};
use anyhow::Result;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct DynamicColor {
current: Color,
target: Color,
start_time: Option<Instant>,
duration: Duration,
easing_function: fn(f64) -> f64,
}
impl DynamicColor {
pub fn new(initial_color: Color) -> Self {
Self {
current: initial_color,
target: initial_color,
start_time: None,
duration: Duration::from_millis(300), easing_function: ease_in_out_cubic,
}
}
pub fn from_static(color: Color) -> Self {
Self::new(color)
}
pub fn animate_to(&mut self, target_color: Color, duration: Duration) {
self.target = target_color;
self.duration = duration;
self.start_time = Some(Instant::now());
}
pub fn set_target(&mut self, target_color: Color) {
self.animate_to(target_color, self.duration);
}
pub fn set_immediate(&mut self, color: Color) {
self.current = color;
self.target = color;
self.start_time = None;
}
pub fn with_easing(mut self, easing_fn: fn(f64) -> f64) -> Self {
self.easing_function = easing_fn;
self
}
pub fn with_duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
pub fn update(&mut self, _delta_time: f64) -> Color {
if let Some(start_time) = self.start_time {
let elapsed = start_time.elapsed();
if elapsed >= self.duration {
self.current = self.target;
self.start_time = None;
} else {
let progress = elapsed.as_secs_f64() / self.duration.as_secs_f64();
let eased_progress = (self.easing_function)(progress);
self.current = interpolate_color(self.current, self.target, eased_progress);
}
}
self.current
}
pub fn current(&self) -> Color {
self.current
}
pub fn is_animating(&self) -> bool {
self.start_time.is_some()
}
pub fn target(&self) -> Color {
self.target
}
}
pub trait ColorProperty {
fn color_mut(&mut self) -> &mut DynamicColor;
fn color(&self) -> &DynamicColor;
fn set_color(&mut self, color: Color) {
self.color_mut().set_immediate(color);
}
fn animate_color_to(&mut self, color: Color, duration: Duration) {
self.color_mut().animate_to(color, duration);
}
fn update_color(&mut self, delta_time: f64) -> bool {
let was_animating = self.color().is_animating();
self.color_mut().update(delta_time);
let still_animating = self.color().is_animating();
was_animating || still_animating }
}
fn interpolate_color(start: Color, end: Color, t: f64) -> Color {
let t = t.clamp(0.0, 1.0);
Color {
r: start.r + (end.r - start.r) * t,
g: start.g + (end.g - start.g) * t,
b: start.b + (end.b - start.b) * t,
a: start.a + (end.a - start.a) * t,
}
}
fn ease_in_out_cubic(t: f64) -> f64 {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
#[allow(dead_code)]
fn ease_linear(t: f64) -> f64 {
t
}
#[allow(dead_code)]
fn ease_in_out_sine(t: f64) -> f64 {
(-(std::f64::consts::PI * t).cos() + 1.0) / 2.0
}
impl AnimationTarget for DynamicColor {
fn set_property(&mut self, property: AnimationProperty) -> Result<()> {
match property {
AnimationProperty::Color { r, g, b } => {
let color = Color::rgba(
r as f64 / 255.0,
g as f64 / 255.0,
b as f64 / 255.0,
self.current.a,
);
self.set_immediate(color);
Ok(())
}
_ => anyhow::bail!("DynamicColor only supports Color properties"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_dynamic_color_creation() {
let color = Color::rgb(1.0, 0.0, 0.0);
let dynamic = DynamicColor::new(color);
assert_eq!(dynamic.current(), color);
assert_eq!(dynamic.target(), color);
assert!(!dynamic.is_animating());
}
#[test]
fn test_color_interpolation() {
let start = Color::rgb(0.0, 0.0, 0.0);
let end = Color::rgb(1.0, 1.0, 1.0);
let mid = interpolate_color(start, end, 0.5);
assert_eq!(mid.r, 0.5);
assert_eq!(mid.g, 0.5);
assert_eq!(mid.b, 0.5);
}
#[test]
fn test_color_interpolation_with_alpha() {
let start = Color::rgba(1.0, 0.0, 0.0, 0.0);
let end = Color::rgba(0.0, 1.0, 0.0, 1.0);
let result = interpolate_color(start, end, 0.25);
assert_eq!(result.r, 0.75);
assert_eq!(result.g, 0.25);
assert_eq!(result.b, 0.0);
assert_eq!(result.a, 0.25);
}
#[test]
fn test_color_interpolation_clamping() {
let start = Color::rgb(0.0, 0.0, 0.0);
let end = Color::rgb(1.0, 1.0, 1.0);
let below = interpolate_color(start, end, -0.5);
assert_eq!(below, start);
let above = interpolate_color(start, end, 1.5);
assert_eq!(above, end);
}
#[test]
fn test_immediate_color_set() {
let mut dynamic = DynamicColor::new(Color::rgb(0.0, 0.0, 0.0));
let new_color = Color::rgb(1.0, 0.0, 0.0);
dynamic.set_immediate(new_color);
assert_eq!(dynamic.current(), new_color);
assert_eq!(dynamic.target(), new_color);
assert!(!dynamic.is_animating());
}
#[test]
fn test_animation_start() {
let mut dynamic = DynamicColor::new(Color::rgb(1.0, 0.0, 0.0));
let target = Color::rgb(0.0, 1.0, 0.0);
assert!(!dynamic.is_animating());
dynamic.animate_to(target, Duration::from_millis(100));
assert!(dynamic.is_animating());
assert_eq!(dynamic.target(), target);
}
#[test]
fn test_animation_completion() {
let mut dynamic = DynamicColor::new(Color::rgb(1.0, 0.0, 0.0));
let target = Color::rgb(0.0, 1.0, 0.0);
dynamic.animate_to(target, Duration::from_millis(10));
assert!(dynamic.is_animating());
thread::sleep(Duration::from_millis(15));
let final_color = dynamic.update(0.016);
assert!(!dynamic.is_animating());
assert_eq!(final_color, target);
assert_eq!(dynamic.current(), target);
}
#[test]
fn test_zero_duration_animation() {
let mut dynamic = DynamicColor::new(Color::rgb(1.0, 0.0, 0.0));
let target = Color::rgb(0.0, 1.0, 0.0);
dynamic.animate_to(target, Duration::ZERO);
let result = dynamic.update(0.016);
assert!(!dynamic.is_animating());
assert_eq!(result, target);
}
#[test]
fn test_animation_chaining() {
let mut dynamic = DynamicColor::new(Color::rgb(1.0, 0.0, 0.0));
dynamic.animate_to(Color::rgb(0.0, 1.0, 0.0), Duration::from_millis(10));
thread::sleep(Duration::from_millis(5));
dynamic.animate_to(Color::rgb(0.0, 0.0, 1.0), Duration::from_millis(10));
assert!(dynamic.is_animating());
assert_eq!(dynamic.target(), Color::rgb(0.0, 0.0, 1.0));
}
#[test]
fn test_set_target_uses_default_duration() {
let mut dynamic =
DynamicColor::new(Color::rgb(1.0, 0.0, 0.0)).with_duration(Duration::from_millis(500));
dynamic.set_target(Color::rgb(0.0, 1.0, 0.0));
assert!(dynamic.is_animating());
assert_eq!(dynamic.target(), Color::rgb(0.0, 1.0, 0.0));
}
#[test]
fn test_builder_pattern() {
let dynamic = DynamicColor::new(Color::rgb(1.0, 0.0, 0.0))
.with_duration(Duration::from_millis(1000))
.with_easing(ease_linear);
assert_eq!(dynamic.current(), Color::rgb(1.0, 0.0, 0.0));
}
#[test]
fn test_from_static() {
let color = Color::rgb(0.5, 0.5, 0.5);
let dynamic = DynamicColor::from_static(color);
assert_eq!(dynamic.current(), color);
assert_eq!(dynamic.target(), color);
assert!(!dynamic.is_animating());
}
#[test]
fn test_easing_functions() {
for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
let cubic = ease_in_out_cubic(t);
let linear = ease_linear(t);
let sine = ease_in_out_sine(t);
assert!(
(0.0..=1.0).contains(&cubic),
"Cubic easing out of range at t={}",
t
);
assert!(
(0.0..=1.0).contains(&linear),
"Linear easing out of range at t={}",
t
);
assert!(
(0.0..=1.0).contains(&sine),
"Sine easing out of range at t={}",
t
);
}
assert_eq!(ease_in_out_cubic(0.0), 0.0);
assert_eq!(ease_in_out_cubic(1.0), 1.0);
assert_eq!(ease_linear(0.0), 0.0);
assert_eq!(ease_linear(1.0), 1.0);
assert!((ease_in_out_sine(0.0) - 0.0).abs() < 1e-10);
assert!((ease_in_out_sine(1.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_animation_target_integration() {
let mut dynamic = DynamicColor::new(Color::rgba(0.0, 0.0, 0.0, 0.5));
let property = AnimationProperty::Color {
r: 255,
g: 128,
b: 64,
};
dynamic.set_property(property).unwrap();
let expected = Color::rgba(1.0, 128.0 / 255.0, 64.0 / 255.0, 0.5); assert_eq!(dynamic.current(), expected);
}
#[test]
fn test_animation_target_invalid_property() {
let mut dynamic = DynamicColor::new(Color::rgb(0.0, 0.0, 0.0));
let property = AnimationProperty::Alpha(0.5);
let result = dynamic.set_property(property);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("DynamicColor only supports Color properties"));
}
#[test]
fn test_animation_progress_during_update() {
let mut dynamic = DynamicColor::new(Color::rgb(0.0, 0.0, 0.0));
let target = Color::rgb(1.0, 1.0, 1.0);
dynamic.animate_to(target, Duration::from_millis(100));
let start_time = std::time::Instant::now();
let mut colors = Vec::new();
while start_time.elapsed() < Duration::from_millis(120) {
colors.push(dynamic.update(0.016));
thread::sleep(Duration::from_millis(10));
}
assert!(colors.len() > 1);
let first = colors[0];
let last = colors[colors.len() - 1];
assert!(last.r >= first.r);
assert!(last.g >= first.g);
assert!(last.b >= first.b);
}
#[test]
fn test_multiple_animation_restarts() {
let mut dynamic = DynamicColor::new(Color::rgb(1.0, 0.0, 0.0));
for i in 0..5 {
let target = Color::rgb(i as f64 / 4.0, 1.0, 0.0);
dynamic.animate_to(target, Duration::from_millis(50));
assert!(dynamic.is_animating());
assert_eq!(dynamic.target(), target);
}
}
}