Skip to main content

ranim_core/
lib.rs

1//! The core of ranim.
2
3#![warn(missing_docs)]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5#![allow(rustdoc::private_intra_doc_links)]
6#![doc(
7    html_logo_url = "https://raw.githubusercontent.com/AzurIce/ranim/refs/heads/main/assets/ranim.svg",
8    html_favicon_url = "https://raw.githubusercontent.com/AzurIce/ranim/refs/heads/main/assets/ranim.svg"
9)]
10
11/// Anchors and semantic bounds.
12pub mod anchor;
13pub mod animation;
14/// The audio plane of a scene.
15pub mod audio;
16/// Color utilities.
17pub mod color;
18/// Component data.
19pub mod components;
20/// Reusable typed output of scene evaluation.
21pub mod frame;
22/// Scene evaluation driver (lightweight session, no ECS).
23pub mod scene_evaluator;
24/// Time vocabulary for animation evaluation.
25pub mod time;
26/// Fundamental traits.
27pub mod traits;
28/// Generic values paired with typed external transforms.
29pub mod transformed;
30pub use scene_evaluator::SceneEvaluator;
31/// Utilities.
32pub mod utils;
33
34pub use glam;
35use std::fmt::Debug;
36use std::sync::Arc;
37
38use animation::{
39    build::IntoAnimNode,
40    compose::stack::AnimStack,
41    eval::OutputContext,
42    node::{AnimNode, AnimationInfo, bake_audio},
43};
44use frame::EvaluatedFrame;
45
46/// Commonly used ranim APIs.
47pub mod prelude {
48    pub use crate::color::prelude::*;
49    pub use crate::traits::*;
50
51    pub use crate::animation::build::{IntoAnimNode, PlaybackExt, Unplaced};
52    pub use crate::animation::compose::{
53        AnimIterExt,
54        lagged::{AnimLagged, LaggedFill},
55        sequence::AnimSequence,
56        stack::AnimStack,
57    };
58    pub use crate::animation::eval::{
59        Eval, EvalExt, Static, StaticAnim,
60        iterative::{Iterative, IterativeEval, IterativeFn},
61        pure::Pure,
62    };
63    pub use crate::animation::sound::Sound;
64    pub use crate::audio::{AudioClip, AudioTrack};
65    pub use crate::transformed::{Transformed, TransformedExt};
66    pub use crate::{RanimScene, TimeMark};
67}
68
69/// A marker attached to a time in a scene definition.
70#[derive(Debug, Clone)]
71pub enum TimeMark {
72    /// Capture a picture with a name.
73    Capture(String),
74}
75
76/// Animation definition builder passed to scene constructors.
77///
78/// The public [`RanimScene::root`] stack is the scene's animation composition
79/// root. Calling [`RanimScene::play`] is a convenience alias for pushing into
80/// that stack.
81#[derive(Default)]
82pub struct RanimScene {
83    /// Root animation stack. Modules pushed here share the same local origin.
84    ///
85    /// Audio leaves ([`Sound`](crate::animation::sound::Sound)) compose in the same tree beside visual
86    /// animations; [`RanimScene::seal`] bakes them through the same cell
87    /// remaps the visuals experience.
88    pub root: AnimStack,
89    time_marks: Vec<(f64, TimeMark)>,
90}
91
92impl RanimScene {
93    /// Create an empty scene definition.
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// Push an animation module into the root stack.
99    pub fn play<A: IntoAnimNode + 'static>(&mut self, animation: A) -> &mut Self {
100        self.root.push(animation);
101        self
102    }
103
104    /// Insert a time mark.
105    pub fn insert_time_mark(&mut self, sec: f64, time_mark: TimeMark) {
106        self.time_marks.push((sec, time_mark));
107    }
108
109    /// Finish the definition and produce an immutable, evaluable recipe.
110    ///
111    /// The tree is the only animation representation: visual cells are
112    /// point-walked per frame by [`SceneEvaluator::sample_at`], and sound
113    /// leaves are baked here — see [`bake_audio`](crate::animation) — both
114    /// experiencing the same cell remaps along the path.
115    pub fn seal(self) -> SealedRanimScene {
116        let total_secs = self.root.duration_secs();
117        let animations = self.root.into_built_animations();
118        let sample_rate = crate::audio::MASTER_SAMPLE_RATE as f64;
119        let audio: Arc<[f32]> = bake_audio(&animations, total_secs, sample_rate).into();
120        SealedRanimScene {
121            total_secs,
122            animations,
123            audio,
124            time_marks: self.time_marks,
125        }
126    }
127}
128
129impl Debug for RanimScene {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        f.debug_struct("RanimScene")
132            .field("animations", &self.root.built_animations().len())
133            .field("duration_secs", &self.root.duration_secs())
134            .finish()
135    }
136}
137
138/// Immutable animation recipe produced by [`RanimScene::seal`].
139pub struct SealedRanimScene {
140    total_secs: f64,
141    animations: Vec<AnimNode>,
142    /// The baked audio plane: interleaved stereo at the master sample rate
143    /// over `[0, total_secs]`, produced once at seal. Empty when the tree
144    /// has no sound leaves.
145    audio: Arc<[f32]>,
146    time_marks: Vec<(f64, TimeMark)>,
147}
148
149impl SealedRanimScene {
150    /// Consume this scene into a [`SceneEvaluator`] driving session.
151    ///
152    /// `logic_fps` is retained for call-site compatibility; it no longer drives
153    /// stepping (each iterative segment owns its own `sim_step`).
154    pub fn into_evaluator(self, logic_fps: f64) -> SceneEvaluator {
155        SceneEvaluator::new(self, logic_fps)
156    }
157
158    /// The baked audio plane: interleaved stereo at the master sample rate
159    /// over `[0, total_secs]` (shared, cheap to clone). Empty when the scene
160    /// has no sound leaves.
161    pub fn audio(&self) -> &Arc<[f32]> {
162        &self.audio
163    }
164
165    /// Total scene duration.
166    pub fn total_secs(&self) -> f64 {
167        self.total_secs
168    }
169
170    /// Scene time marks.
171    pub fn time_marks(&self) -> &[(f64, TimeMark)] {
172        &self.time_marks
173    }
174
175    /// Hierarchical runtime animation information for preview tooling.
176    pub fn get_animation_infos(&self) -> Vec<AnimationInfo> {
177        self.animations
178            .iter()
179            .map(AnimNode::animation_info)
180            .collect()
181    }
182
183    /// Sample all clips active at `target_sec` into typed CPU batches.
184    ///
185    /// Replaces the whole frame while reusing its batch storage. Each active
186    /// leaf contributes its complete output under its top-level animation source.
187    pub fn eval_at_sec(&self, target_sec: f64, out: &mut EvaluatedFrame) {
188        out.clear();
189        for (source_id, animation) in self.animations.iter().enumerate() {
190            animation.eval_at(target_sec, &mut OutputContext::new(out, source_id));
191        }
192    }
193
194    /// Sample by normalized scene progress, replacing `out`.
195    pub fn eval_at_alpha(&self, alpha: f64, out: &mut EvaluatedFrame) {
196        self.eval_at_sec(self.total_secs * alpha, out);
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::animation::{
204        build::{PlaybackExt, Unplaced},
205        compose::sequence::AnimSequence,
206        eval::Static,
207    };
208
209    fn leaf(duration: f64) -> impl Unplaced {
210        Static(0_u8).with_duration(duration)
211    }
212
213    #[test]
214    fn scene_play_places_sequences_on_the_root_timeline() {
215        // A reusable sequence module keeps its local gaps on the root stack,
216        // and later modules start at the root origin.
217        let mut reusable = AnimSequence::new();
218        reusable.push(leaf(2.0)).forward(1.0).push(leaf(1.0));
219        let mut scene = RanimScene::new();
220        scene.play(reusable);
221        scene.root.push(leaf(5.0));
222        let sealed = scene.seal();
223        assert_eq!(sealed.total_secs(), 5.0);
224        let infos = sealed.get_animation_infos();
225        assert_eq!(infos[0].range, 0.0..4.0);
226        assert_eq!(infos[0].children[1].range, 3.0..4.0);
227        assert_eq!(infos[1].range, 0.0..5.0);
228    }
229
230    #[test]
231    fn leaf_outputs_keep_containers_whole_while_children_share_source_parts() {
232        let mut scene = RanimScene::new();
233        scene.play(crate::stack![
234            Static(vec![1_u8, 2]).with_duration(1.0),
235            Static(3_u8).with_duration(1.0),
236            Static((42_u32, String::from("tuple"))).with_duration(1.0),
237        ]);
238        let sealed = scene.seal();
239        let mut frame = EvaluatedFrame::new();
240        sealed.eval_at_sec(0.5, &mut frame);
241
242        assert_eq!(frame.len(), 3);
243        let vector = &frame.items::<Vec<u8>>()[0];
244        assert_eq!(vector.value, [1, 2]);
245        assert_eq!((vector.id, vector.order), ((0, 0), 0));
246        let item = &frame.items::<u8>()[0];
247        assert_eq!(item.value, 3);
248        assert_eq!((item.id, item.order), ((0, 1), 1));
249        let tuple = &frame.items::<(u32, String)>()[0];
250        assert_eq!(tuple.value, (42, String::from("tuple")));
251        assert_eq!((tuple.id, tuple.order), ((0, 2), 2));
252        assert!(frame.items::<u32>().is_empty());
253    }
254}