Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ Update it whenever you learn something new about the project's patterns, convent
- When implementing newtypes with `derive_more`, include `Copy` and `Clone` derives when the wrapped type supports them.
- When generating state transition events or similar sequences, include cumulative/complete state in each event rather than just the delta. This provides full context to event handlers.
- In data structures with paired values (like min/max bounds), group them logically: prefer `x: [f32; 2], y: [f32; 2]` over `min_x, min_y, max_x, max_y` for clarity.
- Prefer using standard library traits (`Add`, `Mul`, etc.) over creating custom traits when possible.
- When operations can be expressed using simpler primitives (e.g., subtraction as `a + (b * -1.0)`), avoid adding extra trait requirements.
- Use `Self::Variant` consistently in enum match statements rather than fully qualifying the enum name.
- Types that are private to a module don't need constructor functions - use struct literals directly.

## Safety & Quality
- Avoid unsafe or experimental APIs unless required.
Expand Down
20 changes: 13 additions & 7 deletions animation/src/animated.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::time::Duration;
use std::{
ops::{Add, Mul},
time::Duration,
};

use parking_lot::Mutex;

Expand All @@ -9,7 +12,7 @@ use crate::{AnimationCoordinator, BlendedAnimation, Interpolatable, Interpolatio
/// `Animated` implicitly supports animation blending. New animations added are combined with the
/// trajectory of previous animations.
#[derive(Debug)]
pub struct Animated<T: Send> {
pub struct Animated<T: Interpolatable + Send> {
coordinator: AnimationCoordinator,
/// The current value and the current state of the animation.
///
Expand Down Expand Up @@ -38,7 +41,7 @@ impl<T: Interpolatable + Send> Animated<T> {
duration: Duration,
interpolation: Interpolation,
) where
T: 'static + PartialEq,
T: 'static + PartialEq + Add<Output = T> + Mul<f64, Output = T>,
{
let mut inner = self.inner.lock();
if *inner.final_value() == target_value {
Expand All @@ -59,7 +62,7 @@ impl<T: Interpolatable + Send> Animated<T> {
/// current value, if it is currently not animating.
pub fn animate(&mut self, target_value: T, duration: Duration, interpolation: Interpolation)
where
T: 'static,
T: 'static + Add<Output = T> + Mul<f64, Output = T>,
{
let instant = self.coordinator.allocate_animation_time(duration);

Expand Down Expand Up @@ -93,7 +96,10 @@ impl<T: Interpolatable + Send> Animated<T> {
/// The current value of this animated value.
///
/// If an animation is active, this computes the current value from the animation.
pub fn value(&self) -> T {
pub fn value(&self) -> T
where
T: Add<Output = T> + Mul<f64, Output = T>,
{
let mut inner = self.inner.lock();
if inner.animation.is_active() {
let instant = self.coordinator.current_cycle_time();
Expand Down Expand Up @@ -134,15 +140,15 @@ impl<T: Interpolatable + Send> Animated<T> {
#[derive(Debug)]
struct AnimatedInner<T>
where
T: Send,
T: Interpolatable + Send,
{
/// The current value.
value: T,
/// The currently running animations.
animation: BlendedAnimation<T>,
}

impl<T: Send> AnimatedInner<T> {
impl<T: Interpolatable + Send> AnimatedInner<T> {
pub fn final_value(&self) -> &T {
self.animation.final_value().unwrap_or(&self.value)
}
Expand Down
87 changes: 87 additions & 0 deletions animation/src/blended.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::{
ops::{Add, Mul},
time::Duration,
};

use crate::{time::Instant, Interpolatable, Interpolation};

mod hermite;
mod linear;

pub use hermite::Hermite;
pub use linear::Linear;

#[derive(Debug)]
pub enum BlendedAnimation<T: Interpolatable> {
Linear(Linear<T>),
Hermite(Hermite<T>),
}

impl<T: Interpolatable> Default for BlendedAnimation<T> {
fn default() -> Self {
Self::Linear(Linear::default())
}
}

impl<T: Interpolatable> BlendedAnimation<T> {
pub fn animate_to(
&mut self,
current_value: T,
current_time: Instant,
to: T,
duration: Duration,
interpolation: Interpolation,
) where
T: Add<Output = T> + Mul<f64, Output = T>,
{
match self {
Self::Linear(inner) => {
inner.animate_to(current_value, current_time, to, duration, interpolation)
}
Self::Hermite(inner) => {
// Hermite ignores the interpolation parameter - it uses velocity continuity
inner.animate_to(current_value, current_time, to, duration)
}
}
}

pub fn is_active(&self) -> bool {
match self {
Self::Linear(inner) => inner.is_active(),
Self::Hermite(inner) => inner.is_active(),
}
}

pub fn final_value(&self) -> Option<&T> {
match self {
Self::Linear(inner) => inner.final_value(),
Self::Hermite(inner) => inner.final_value(),
}
}

pub fn end(&mut self) -> Option<T> {
match self {
Self::Linear(inner) => inner.end(),
Self::Hermite(inner) => inner.end(),
}
}

pub fn count(&self) -> usize {
match self {
Self::Linear(inner) => inner.count(),
Self::Hermite(inner) => inner.count(),
}
}
}

impl<T> BlendedAnimation<T>
where
T: Interpolatable + Add<Output = T> + Mul<f64, Output = T>,
{
pub fn proceed(&mut self, instant: Instant) -> Option<T> {
match self {
Self::Linear(inner) => inner.proceed(instant),
Self::Hermite(inner) => inner.proceed(instant),
}
}
}
Loading