Timelines
A Timeline is how you schedule when animations happen in Murali. It's the conductor that orchestrates all the changes in your scene over time.
What is a Timeline?β
Think of a timeline as a musical score:
- It tells each "instrument" (tattva) when to play (start time)
- It specifies how long each note lasts (duration)
- It controls how the note is played (easing)
- It defines what happens (animation verb)
Key insight: Timelines don't store stateβthey schedule mutations. The scene holds the actual state, and the timeline tells it when and how to change.
Creating a Timelineβ
use murali::engine::timeline::Timeline;
let mut timeline = Timeline::new();
That's it! A timeline starts empty, and you add animations to it.
Adding Animationsβ
The most common way to add animations is with the fluent builder API:
timeline
.animate(tattva_id) // What to animate
.at(0.0) // When to start (seconds)
.for_duration(2.0) // How long it takes
.ease(Ease::OutCubic) // How it moves
.move_to(Vec3::new(3.0, 0.0, 0.0)) // What changes
.spawn(); // Add to timeline
Important: Don't forget .spawn() at the end! Without it, the animation isn't added to the timeline.
Timeline Lifecycleβ
1. Build Phaseβ
You create animations and add them to the timeline:
let mut timeline = Timeline::new();
timeline.animate(id1).at(0.0).for_duration(2.0).move_to(...).spawn();
timeline.animate(id2).at(1.5).for_duration(1.5).scale_to(...).spawn();
timeline.animate(id3).at(2.0).for_duration(1.0).fade_to(...).spawn();
2. Play Phaseβ
You give the timeline to the scene:
scene.play(timeline);
3. Runtime Phaseβ
Each frame:
- Scene time advances by
dt(e.g., 1/60 second) - Timeline checks which animations should be active
- Active animations apply their changes to tattvas
- Scene state is updated
- Renderer draws the new state
One Timeline vs Many Timelinesβ
Single Timeline (Recommended)β
Most scenes use a single timeline:
let mut timeline = Timeline::new();
// Add all your animations
timeline.animate(id1).at(0.0).for_duration(2.0).move_to(...).spawn();
timeline.animate(id2).at(1.0).for_duration(1.5).scale_to(...).spawn();
timeline.animate(id3).at(2.0).for_duration(1.0).appear().spawn();
// Play it
scene.play(timeline); // Internally uses the name "main"
This is the preferred API for most use cases.
Multiple Named Timelinesβ
You can have multiple timelines for organization:
let mut main_timeline = Timeline::new();
let mut background_timeline = Timeline::new();
let mut ui_timeline = Timeline::new();
// Add animations to each...
scene.play_named("main", main_timeline);
scene.play_named("background", background_timeline);
scene.play_named("ui", ui_timeline);
Important limitations:
- All timelines share the same
scene_time - They progress together, not independently
- You can't pause one timeline while others continue
- You can't play timelines at different speeds
When to use multiple timelines:
- Organizing complex scenes by layer (foreground, background, UI)
- Separating concerns (content animations vs camera movements)
- Managing different "tracks" that you want to edit separately
- Code organization in large projects
Current status: This is an advanced feature that works but has limitations. Treat multiple timelines as organizational lanes for one shared scene clock, not as independent playback systems. Future versions may add more explicit control.
Timeline Time vs Scene Timeβ
Scene time is the authoritative clock. It's a single f32 that represents "where we are" in the animation.
Timeline time is just how animations are scheduled relative to scene time.
// At scene_time = 1.5:
timeline.animate(id).at(0.0).for_duration(2.0).move_to(...).spawn();
// This animation is active (started at 0.0, ends at 2.0)
timeline.animate(id).at(2.0).for_duration(1.0).scale_to(...).spawn();
// This animation hasn't started yet (starts at 2.0)
All timelines advance with scene time. There's no separate "timeline time" that can drift or be controlled independently.
If you are choosing between one timeline and many, use one timeline by default. Reach for multiple named timelines when separating concerns makes a large scene easier to edit and reason about.
Callbacksβ
Sometimes you need to run custom code at specific times.
call_atβ
Run code once at a specific time:
timeline.call_at(2.0, |scene| {
println!("Reached t=2.0!");
scene.hide(some_id);
});
Use cases:
- Discrete events (show/hide objects)
- State changes
- Logging or debugging
- Cleanup
call_duringβ
Run code continuously over a duration:
timeline.call_during(1.0, 2.0, |scene, t| {
// t goes from 0.0 to 1.0 over the duration
// This runs every frame between scene_time 1.0 and 3.0
let angle = t * std::f32::consts::TAU;
let position = Vec3::new(
angle.cos() * 3.0,
angle.sin() * 3.0,
0.0
);
scene.set_position_3d(circle_id, position);
});
Use cases:
- Complex motion paths (circles, spirals, custom curves)
- Dependent motion (one object following another)
- Procedural animations
- Custom interpolation logic
Note: The t parameter is normalized from 0.0 to 1.0, regardless of the actual duration.
Advanced Timeline Featuresβ
Morphing Groupsβ
Morph multiple tattvas at once:
timeline.morph_matching_staged(
source_ids, // Vec<TattvaId>
target_ids, // Vec<TattvaId>
&mut scene,
1.0, // start time
2.0, // duration
Ease::InOutCubic,
);
This automatically stages (hides) the target tattvas and morphs them from the sources.
Signal Playbackβ
For procedural or signal-driven animations:
use murali::engine::timeline::SignalPlayback;
// Play once
let playback = SignalPlayback::once(0.0, 2.0, Ease::OutCubic);
timeline.play_signal(tattva_id, playback);
// Round trip (there and back)
let playback = SignalPlayback::round_trip(0.0, 2.0, Ease::InOutQuad);
timeline.play_signal(tattva_id, playback);
// Loop multiple times
let playback = SignalPlayback::looped(0.0, 1.0, 5, Ease::Linear);
timeline.play_signal(tattva_id, playback);
Wait Untilβ
Ensure the scene runs until a specific time, even if all animations finish earlier:
timeline.wait_until(10.0);
This is useful for adding a pause at the end of your animation before it loops or exits.
End Timeβ
Get when the timeline finishes:
let end = timeline.end_time();
println!("Animation ends at t={}", end);
This considers all scheduled animations and any wait_until calls.
Sequencing Patternsβ
Sequential (One After Another)β
let mut timeline = Timeline::new();
let mut current_time = 0.0;
// Animation 1
timeline.animate(id1).at(current_time).for_duration(2.0).move_to(...).spawn();
current_time += 2.0;
// Animation 2 (starts when 1 ends)
timeline.animate(id2).at(current_time).for_duration(1.5).scale_to(...).spawn();
current_time += 1.5;
// Animation 3 (starts when 2 ends)
timeline.animate(id3).at(current_time).for_duration(1.0).appear().spawn();
Parallel (All at Once)β
let mut timeline = Timeline::new();
// All start at the same time
timeline.animate(id1).at(0.0).for_duration(2.0).move_to(...).spawn();
timeline.animate(id2).at(0.0).for_duration(2.0).scale_to(...).spawn();
timeline.animate(id3).at(0.0).for_duration(2.0).fade_to(...).spawn();
Staggered (Overlapping)β
let mut timeline = Timeline::new();
let stagger_delay = 0.2;
for (i, id) in tattva_ids.iter().enumerate() {
timeline
.animate(*id)
.at(i as f32 * stagger_delay)
.for_duration(1.0)
.ease(Ease::OutCubic)
.appear()
.spawn();
}
Overlapping (Start Before Previous Ends)β
let mut timeline = Timeline::new();
// Animation 1: 0.0 to 2.0
timeline.animate(id1).at(0.0).for_duration(2.0).move_to(...).spawn();
// Animation 2: 1.5 to 3.0 (overlaps with 1)
timeline.animate(id2).at(1.5).for_duration(1.5).scale_to(...).spawn();
// Animation 3: 2.5 to 3.5 (overlaps with 2)
timeline.animate(id3).at(2.5).for_duration(1.0).appear().spawn();
Common Patternsβ
Intro β Content β Outroβ
let mut timeline = Timeline::new();
// Intro: Title appears
timeline.animate(title_id).at(0.0).for_duration(1.0).appear().spawn();
// Content: Main animation
timeline.animate(content_id).at(1.5).for_duration(3.0).draw().spawn();
// Outro: Everything fades out
timeline.animate(title_id).at(5.0).for_duration(1.0).fade_to(0.0).spawn();
timeline.animate(content_id).at(5.0).for_duration(1.0).fade_to(0.0).spawn();
Build Up Then Transformβ
let mut timeline = Timeline::new();
// Build: Reveal all pieces
for (i, id) in piece_ids.iter().enumerate() {
timeline
.animate(*id)
.at(i as f32 * 0.3)
.for_duration(0.8)
.ease(Ease::OutCubic)
.appear()
.spawn();
}
// Transform: Move pieces into final positions
let transform_start = piece_ids.len() as f32 * 0.3 + 1.0;
for (i, id) in piece_ids.iter().enumerate() {
timeline
.animate(*id)
.at(transform_start)
.for_duration(2.0)
.ease(Ease::InOutQuad)
.move_to(final_positions[i])
.spawn();
}
Synchronized Multi-Property Animationβ
let mut timeline = Timeline::new();
// Move and scale at the same time
timeline
.animate(id)
.at(0.0)
.for_duration(2.0)
.ease(Ease::OutCubic)
.move_to(Vec3::new(3.0, 0.0, 0.0))
.spawn();
timeline
.animate(id)
.at(0.0)
.for_duration(2.0)
.ease(Ease::OutCubic)
.scale_to(Vec3::splat(2.0))
.spawn();
// Fade out while moving back
timeline
.animate(id)
.at(3.0)
.for_duration(1.5)
.ease(Ease::InCubic)
.move_to(Vec3::ZERO)
.spawn();
timeline
.animate(id)
.at(3.0)
.for_duration(1.5)
.ease(Ease::InCubic)
.fade_to(0.0)
.spawn();
Timeline Best Practicesβ
Do'sβ
β Use descriptive timing constants
const INTRO_START: f32 = 0.0;
const INTRO_DURATION: f32 = 1.5;
const CONTENT_START: f32 = INTRO_START + INTRO_DURATION + 0.5;
const CONTENT_DURATION: f32 = 3.0;
β Group related animations
// Title animations
timeline.animate(title_id).at(0.0).for_duration(1.0).appear().spawn();
timeline.animate(title_id).at(5.0).for_duration(1.0).fade_to(0.0).spawn();
// Content animations
timeline.animate(content_id).at(1.5).for_duration(2.0).draw().spawn();
timeline.animate(content_id).at(5.0).for_duration(1.0).undraw().spawn();
β Use staggering for visual interest
for (i, id) in ids.iter().enumerate() {
timeline
.animate(*id)
.at(i as f32 * 0.2)
.for_duration(1.0)
.appear()
.spawn();
}
β Add pauses between sections
// Section 1: 0.0 to 3.0
// Pause: 3.0 to 3.5
// Section 2: 3.5 to 6.0
Don'tsβ
β Don't forget .spawn()
// This does nothing!
timeline.animate(id).at(0.0).for_duration(2.0).move_to(...);
// Missing .spawn()
β Don't use magic numbers
// Bad
timeline.animate(id).at(2.347).for_duration(1.823).move_to(...).spawn();
// Good
const REVEAL_TIME: f32 = 2.35;
const REVEAL_DURATION: f32 = 1.8;
timeline.animate(id).at(REVEAL_TIME).for_duration(REVEAL_DURATION).move_to(...).spawn();
β Don't make timings too tight
// Bad - no breathing room
timeline.animate(id1).at(0.0).for_duration(1.0).appear().spawn();
timeline.animate(id2).at(1.0).for_duration(1.0).appear().spawn();
// Better - add small gaps
timeline.animate(id1).at(0.0).for_duration(1.0).appear().spawn();
timeline.animate(id2).at(1.3).for_duration(1.0).appear().spawn();
β Don't try to control timeline playback speed
// This doesn't exist (yet)
// timeline.set_speed(0.5); // β Not supported
Debugging Timelinesβ
Print Timeline Infoβ
let end_time = timeline.end_time();
println!("Timeline ends at: {:.2}s", end_time);
Add Debug Callbacksβ
timeline.call_at(1.0, |_scene| {
println!("Checkpoint 1 reached");
});
timeline.call_at(2.5, |_scene| {
println!("Checkpoint 2 reached");
});
timeline.call_at(5.0, |_scene| {
println!("Animation complete");
});
Visualize Timingβ
// Print a simple timeline visualization
println!("Timeline:");
println!("0.0s: Title appears");
println!("1.5s: Content draws");
println!("3.0s: Transform begins");
println!("5.0s: Fade out");
println!("6.0s: End");
Troubleshootingβ
Animations don't play:
- Did you call
scene.play(timeline)? - Did you forget
.spawn()at the end of animations? - Are start times reasonable (not negative, not too large)?
Animations happen in wrong order:
- Check your
.at(time)values - Make sure you're not reusing the same time for sequential animations
- Print
timeline.end_time()to verify total duration
Animation feels wrong:
- Try different easing functions
- Adjust duration (too fast or too slow?)
- Add small delays between animations for breathing room
Multiple timelines don't work as expected:
- Remember: all timelines share scene_time
- They can't run at different speeds
- Consider using a single timeline with organized sections instead
What's Next?β
- Animations - Learn all available animation verbs
- Scene and App - Understand scene management
- Updaters - For frame-by-frame custom logic
- Camera - Animate the camera