use super::timeline::Animatable;
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Default)]
pub enum CompositeMode {
#[default]
Replace,
Add,
Accumulate,
}
#[inline]
pub fn blend<T: Animatable>(a: &T, b: &T, weight: f64) -> T {
a.lerp(b, weight.clamp(0.0, 1.0))
}
pub fn blend_weighted<T: Animatable>(values: &[(T, f64)]) -> Option<T> {
if values.is_empty() {
return None;
}
let mut result = values[0].0.clone();
let mut accumulated_weight = values[0].1;
for (value, weight) in &values[1..] {
if accumulated_weight > 0.0 {
let blend_factor = weight / (accumulated_weight + weight);
result = result.lerp(value, blend_factor);
accumulated_weight += weight;
} else {
result = value.clone();
accumulated_weight = *weight;
}
}
Some(result)
}
#[derive(Debug, Clone)]
pub struct AnimationLayer<T: Animatable> {
pub value: T,
pub weight: f64,
pub mode: CompositeMode,
}
impl<T: Animatable> AnimationLayer<T> {
pub fn new(value: T) -> Self {
Self {
value,
weight: 1.0,
mode: CompositeMode::Replace,
}
}
pub fn with_weight(mut self, weight: f64) -> Self {
self.weight = weight.clamp(0.0, 1.0);
self
}
pub fn with_mode(mut self, mode: CompositeMode) -> Self {
self.mode = mode;
self
}
}
pub fn resolve_layers<T: Animatable>(base: &T, layers: &[AnimationLayer<T>]) -> T {
if layers.is_empty() {
return base.clone();
}
let mut result = base.clone();
for layer in layers {
match layer.mode {
CompositeMode::Replace => {
result = result.lerp(&layer.value, layer.weight);
}
CompositeMode::Add | CompositeMode::Accumulate => {
result = result.lerp(&layer.value, layer.weight);
}
}
}
result
}
#[inline]
pub fn additive_blend_f64(base: f64, additive_value: f64, weight: f64) -> f64 {
base + additive_value * weight
}
#[inline]
pub fn additive_blend_f32(base: f32, additive_value: f32, weight: f32) -> f32 {
base + additive_value * weight
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum InterruptionStrategy {
Instant,
Blend { duration_secs: f64 },
InheritVelocity,
Queue,
}
impl Default for InterruptionStrategy {
fn default() -> Self {
InterruptionStrategy::Blend { duration_secs: 0.2 }
}
}
#[derive(Debug, Clone)]
pub struct AnimationTransition<T: Animatable> {
pub from_value: T,
pub from_velocity: Option<f64>,
pub blend_duration: f64,
pub elapsed: f64,
}
impl<T: Animatable> AnimationTransition<T> {
pub fn new(current_value: T, strategy: InterruptionStrategy) -> Self {
let blend_duration = match strategy {
InterruptionStrategy::Blend { duration_secs } => duration_secs,
InterruptionStrategy::Instant => 0.0,
InterruptionStrategy::InheritVelocity => 0.2, InterruptionStrategy::Queue => 0.0,
};
Self {
from_value: current_value,
from_velocity: None,
blend_duration,
elapsed: 0.0,
}
}
pub fn blend_weight(&self) -> f64 {
if self.blend_duration <= 0.0 {
return 1.0;
}
(self.elapsed / self.blend_duration).clamp(0.0, 1.0)
}
pub fn tick(&mut self, dt: f64) {
self.elapsed += dt;
}
pub fn is_complete(&self) -> bool {
self.elapsed >= self.blend_duration
}
pub fn apply(&self, new_value: &T) -> T {
self.from_value.lerp(new_value, self.blend_weight())
}
}
#[derive(Debug, Clone)]
pub struct AnimationSlot<T: Animatable> {
current: T,
transition: Option<AnimationTransition<T>>,
pub strategy: InterruptionStrategy,
}
impl<T: Animatable> AnimationSlot<T> {
pub fn new(initial: T) -> Self {
Self {
current: initial,
transition: None,
strategy: InterruptionStrategy::default(),
}
}
pub fn with_strategy(mut self, strategy: InterruptionStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn set(&mut self, new_target_value: T) {
match self.strategy {
InterruptionStrategy::Instant => {
self.current = new_target_value;
self.transition = None;
}
InterruptionStrategy::Blend { .. } => {
self.transition = Some(AnimationTransition::new(
self.current.clone(),
self.strategy,
));
}
InterruptionStrategy::InheritVelocity => {
self.transition = Some(AnimationTransition::new(
self.current.clone(),
self.strategy,
));
}
InterruptionStrategy::Queue => {
self.current = new_target_value;
self.transition = None;
}
}
}
pub fn update(&mut self, animated_value: T, dt: f64) -> T {
if let Some(ref mut transition) = self.transition {
transition.tick(dt);
if transition.is_complete() {
self.current = animated_value.clone();
self.transition = None;
self.current.clone()
} else {
let blended = transition.apply(&animated_value);
self.current = blended.clone();
blended
}
} else {
self.current = animated_value.clone();
self.current.clone()
}
}
pub fn value(&self) -> &T {
&self.current
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blend_50_50() {
let a = 0.0_f64;
let b = 10.0_f64;
let result = blend(&a, &b, 0.5);
assert!((result - 5.0_f64).abs() < 1e-10);
}
#[test]
fn test_blend_weighted_three_values() {
let values = vec![
(0.0_f64, 0.5), (10.0_f64, 0.3), (20.0_f64, 0.2), ];
let result = blend_weighted(&values).unwrap();
assert!(result >= 0.0 && result <= 20.0);
}
#[test]
fn test_resolve_layers_replace_mode() {
let base = 0.0_f64;
let layers = vec![
AnimationLayer::new(10.0_f64).with_weight(0.5),
AnimationLayer::new(20.0_f64).with_weight(0.3),
];
let result = resolve_layers(&base, &layers);
assert!((result - 9.5_f64).abs() < 1e-10);
}
#[test]
fn test_additive_blend_f64() {
let base = 10.0_f64;
let additive = 5.0_f64;
let weight = 0.5_f64;
let result = additive_blend_f64(base, additive, weight);
assert!((result - 12.5_f64).abs() < 1e-10);
}
#[test]
fn test_animation_transition_crossfade() {
let initial_value = 0.0;
let strategy = InterruptionStrategy::Blend { duration_secs: 1.0 };
let mut transition = AnimationTransition::new(initial_value, strategy);
assert_eq!(transition.blend_weight(), 0.0);
transition.tick(0.5);
assert!((transition.blend_weight() - 0.5).abs() < 1e-10);
transition.tick(0.5);
assert_eq!(transition.blend_weight(), 1.0);
assert!(transition.is_complete());
}
#[test]
fn test_animation_transition_apply() {
let initial_value = 0.0;
let strategy = InterruptionStrategy::Blend { duration_secs: 1.0 };
let mut transition = AnimationTransition::new(initial_value, strategy);
let new_value = 10.0;
let result = transition.apply(&new_value);
assert_eq!(result, 0.0);
transition.tick(0.5);
let result = transition.apply(&new_value);
assert!((result - 5.0_f64).abs() < 1e-10);
transition.tick(0.5);
let result = transition.apply(&new_value);
assert_eq!(result, 10.0);
}
#[test]
fn test_animation_slot_instant_strategy() {
let mut slot = AnimationSlot::new(0.0)
.with_strategy(InterruptionStrategy::Instant);
slot.set(10.0);
let result = slot.update(10.0, 0.0);
assert_eq!(result, 10.0);
assert_eq!(*slot.value(), 10.0);
}
#[test]
fn test_animation_slot_blend_strategy() {
let mut slot = AnimationSlot::new(0.0)
.with_strategy(InterruptionStrategy::Blend { duration_secs: 1.0 });
slot.set(10.0);
let result = slot.update(10.0, 0.0);
assert_eq!(result, 0.0);
let result = slot.update(10.0, 0.5);
assert!((result - 5.0_f64).abs() < 1e-10);
let result = slot.update(10.0, 0.5);
assert_eq!(result, 10.0);
assert_eq!(*slot.value(), 10.0);
}
}