Skip to main content

ranim_core/animation/
build.rs

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