Skip to main content

ranim_core/animation/
build.rs

1//! Authoring protocol and playback wrappers for animation definitions.
2//!
3//! [`IntoAnimNode`] is the lowering protocol: it turns an authoring definition
4//! into an [`AnimNode`]. It is intentionally
5//! separate from [`Eval`], the typed visual-leaf
6//! protocol. An `Eval` whose output is `Clone + Send + Sync + 'static` gets an
7//! [`IntoAnimNode`] implementation by blanket impl; the built-in animation
8//! containers implement it directly.
9//!
10//! [`Unplaced`] marks definitions that have not been fixed to a parent time
11//! coordinate yet, so containers and [`Unplaced::at`] may schedule them.
12
13use std::any::type_name;
14
15use super::{
16    eval::Eval,
17    node::{AnimNode, LeafContent, NodeContent},
18};
19
20/// A definition that can be lowered into the runtime animation tree.
21pub trait IntoAnimNode: Sized {
22    /// Lower this definition into its local runtime representation.
23    fn into_anim_node(self) -> AnimNode;
24}
25
26impl IntoAnimNode for AnimNode {
27    fn into_anim_node(self) -> AnimNode {
28        self
29    }
30}
31
32/// Capability for animation definitions that have not been fixed in parent
33/// time coordinates.
34///
35/// This trait is used to constrain the anims that can be inserted into
36/// [`AnimSequence`](crate::animation::compose::sequence::AnimSequence). Only
37/// anims that are not placed can be inserted into it.
38pub trait Unplaced: IntoAnimNode {
39    /// Place this definition at an offset in its parent's local time coordinates.
40    fn at(self, offset_sec: f64) -> At<Self> {
41        At {
42            inner: self,
43            offset_sec,
44        }
45    }
46}
47
48/// Playback parameter builders for animations that have not been placed yet.
49pub trait PlaybackExt: Unplaced {
50    /// Change the animation's rate function.
51    fn with_rate_func(self, rate_func: fn(f64) -> f64) -> Paramed<Self> {
52        Paramed::new(self).with_rate_func(rate_func)
53    }
54
55    /// Change the animation's duration.
56    fn with_duration(self, duration_secs: f64) -> Paramed<Self> {
57        Paramed::new(self).with_duration(duration_secs)
58    }
59
60    /// Enable or disable this animation's output.
61    fn with_enabled(self, enabled: bool) -> Paramed<Self> {
62        Paramed::new(self).with_enabled(enabled)
63    }
64}
65
66impl<A: Unplaced> PlaybackExt for A {}
67
68impl<E> Unplaced for E
69where
70    E: Eval + 'static,
71    E::Output: Clone + Send + Sync + 'static,
72{
73}
74
75impl<E> IntoAnimNode for E
76where
77    E: Eval + 'static,
78    E::Output: Clone + Send + Sync + 'static,
79{
80    fn into_anim_node(self) -> AnimNode {
81        AnimNode {
82            content: NodeContent::Leaf(LeafContent::Anim(Box::new(self))),
83            internal_time_secs: 1.0,
84            anim_name: type_name::<E>(),
85            rate_func: None,
86            time_range: 0.0..1.0,
87            end_inclusive: true,
88            enabled: true,
89        }
90    }
91}
92
93/// Playback parameters applied to an animation definition.
94#[derive(Debug, Clone)]
95pub(crate) struct AnimationParam {
96    /// Time remapping function; `None` is the identity (linear) rate, kept
97    /// structural so the mixing descent can compose affine maps.
98    pub rate_func: Option<fn(f64) -> f64>,
99    /// Optional duration override in seconds.
100    pub duration_secs: Option<f64>,
101    /// Whether this animation contributes a value.
102    pub enabled: bool,
103}
104
105impl Default for AnimationParam {
106    fn default() -> Self {
107        Self {
108            rate_func: None,
109            duration_secs: None,
110            enabled: true,
111        }
112    }
113}
114
115/// An animation definition with overridden playback parameters.
116pub struct Paramed<A> {
117    inner: A,
118    param: AnimationParam,
119}
120
121impl<A> Paramed<A> {
122    /// Wrap an animation without overriding its duration.
123    pub(crate) fn new(inner: A) -> Self {
124        Self {
125            inner,
126            param: AnimationParam::default(),
127        }
128    }
129
130    /// Change the animation's rate function.
131    pub fn with_rate_func(mut self, rate_func: fn(f64) -> f64) -> Self {
132        self.param.rate_func = Some(rate_func);
133        self
134    }
135
136    /// Change the animation's duration.
137    ///
138    /// For a [`Sound`](crate::animation::sound::Sound) this resamples the
139    /// audio linearly: playing a clip faster also shifts its pitch up, since
140    /// the whole content span is warped onto the new window.
141    pub fn with_duration(mut self, duration_secs: f64) -> Self {
142        assert_valid_duration(duration_secs);
143        self.param.duration_secs = Some(duration_secs);
144        self
145    }
146
147    /// Enable or disable this animation's output.
148    pub fn with_enabled(mut self, enabled: bool) -> Self {
149        self.param.enabled = enabled;
150        self
151    }
152}
153
154impl<A: Unplaced + 'static> Unplaced for Paramed<A> {}
155impl<A: Unplaced + 'static> IntoAnimNode for Paramed<A> {
156    fn into_anim_node(self) -> AnimNode {
157        let mut cell = self.inner.into_anim_node();
158        if let Some(duration_secs) = self.param.duration_secs {
159            cell.time_range = 0.0..duration_secs;
160        }
161        cell.rate_func = self.param.rate_func;
162        cell.enabled = self.param.enabled;
163        cell.anim_name = type_name::<A>();
164        cell
165    }
166}
167
168/// An animation fixed at an offset in its parent's time coordinates.
169///
170/// This is a terminal placement entry: it implements
171/// [`IntoAnimNode`] but not [`Unplaced`], so playback parameters must be
172/// configured before calling [`Unplaced::at`].
173pub struct At<A> {
174    inner: A,
175    offset_sec: f64,
176}
177
178impl<A: IntoAnimNode> IntoAnimNode for At<A> {
179    fn into_anim_node(self) -> AnimNode {
180        let mut animation = self.inner.into_anim_node();
181        animation.shift_by(self.offset_sec);
182        animation
183    }
184}
185
186fn assert_valid_duration(duration_secs: f64) {
187    assert!(
188        duration_secs.is_finite() && duration_secs >= 0.0,
189        "animation duration must be finite and non-negative"
190    );
191}