Skip to main content
Version: Next 🚧

Animations

Murali uses a Timeline to schedule animations. Animations are time-driven — they depend on elapsed time, not frame count. This makes them deterministic, reproducible, and easy to reason about.

Prefer concrete animation verbs first. Reach for callbacks like call_at(...) and call_during(...) only when the built-in authored verbs do not express the motion or state change you need.

Quick Decision Guide

Choose your animation based on what you want to achieve:

What do you want?Use thisWhen NOT to use it
Move an object.move_to(position)Object needs complex path (use updater or call_during)
Change size.scale_to(scale)Non-uniform scaling on specific tattva types
Rotate an object.rotate_to(quaternion)For 2D rotation, use Quat::from_rotation_z(angle)
Fade in/out.appear() / .fade_to(opacity)You want an immediate visibility change (use scene helpers like hide / show)
Draw a path progressively.draw() / .undraw()Not a path-like tattva (circles, squares don't draw)
Type text character-by-character.typewrite_text()Text should appear all at once (use .appear())
Reveal text with shift effect.reveal_text()Simple fade is enough (use .appear())
Build a table row-by-row.write_table()Table should appear instantly
Morph one shape into another.morph_from(source_id)Shapes are very different (may look strange)
Match another object's transform.match_transform(id)You want to animate to a specific position
Run custom logic.call_at() / .call_during()Simple property change (use animation verb)

Basic Setup

use murali::engine::timeline::Timeline;
use murali::frontend::animation::Ease;

let mut timeline = Timeline::new();

timeline
.animate(square_id)
.at(0.0) // Start time in seconds
.for_duration(2.0) // Duration in seconds
.ease(Ease::InOutQuad) // Easing function
.move_to(Vec3::new(3.0, 0.0, 0.0)) // Animation verb
.spawn(); // Add to timeline

scene.play(timeline); // Preferred API for single timeline

Key concepts:

  • .at(time) - When the animation starts (in seconds)
  • .for_duration(seconds) - How long it takes
  • .ease(...) - How the motion feels (linear, smooth, bouncy, etc.)
  • .animation_verb() - What actually changes
  • .spawn() - Adds the animation to the timeline

You can have multiple named timelines in a scene using scene.play_named("name", timeline), but they still share the same scene_time. Use them for organization, not for independent playback.

Animation Verbs

Transform Animations

move_to

Moves a tattva to a world-space position.

timeline
.animate(circle_id)
.at(0.0)
.for_duration(2.0)
.ease(Ease::OutCubic)
.move_to(Vec3::new(2.0, 1.0, 0.0))
.spawn();

Optionally specify a starting position (useful for objects that should move from somewhere other than their current position):

timeline
.animate(circle_id)
.at(0.0)
.for_duration(2.0)
.ease(Ease::OutCubic)
.move_to(Vec3::new(2.0, 1.0, 0.0))
.from_vec3(Vec3::new(-2.0, 0.0, 0.0))
.spawn();

When to use: Moving objects between positions, creating motion paths, layout transitions.

When NOT to use: Complex curved paths (use call_during with custom logic instead).

scale_to

Scales a tattva uniformly or non-uniformly.

// Uniform scaling
timeline
.animate(square_id)
.at(0.0)
.for_duration(1.5)
.ease(Ease::OutBack)
.scale_to(Vec3::splat(2.0)) // 2x in all directions
.spawn();

// Non-uniform scaling
timeline
.animate(rectangle_id)
.at(0.0)
.for_duration(1.5)
.ease(Ease::InOutQuad)
.scale_to(Vec3::new(3.0, 1.0, 1.0)) // Stretch horizontally
.spawn();

When to use: Growing/shrinking objects, emphasis effects, size transitions.

When NOT to use: Some tattvas may have specific sizing APIs that are more appropriate.

rotate_to

Rotates a tattva using quaternions.

use glam::Quat;

timeline
.animate(arrow_id)
.at(0.0)
.for_duration(2.0)
.ease(Ease::InOutQuad)
.rotate_to(Quat::from_rotation_z(std::f32::consts::PI))
.spawn();

When to use: Rotating objects in 3D space, orientation changes.

Tip: For simple 2D rotation around Z-axis, use:

.rotate_to(Quat::from_rotation_z(angle_in_radians))

Visibility Animations

appear

Reveals a tattva by animating its opacity from 0 to 1.

timeline
.animate(circle_id)
.at(0.0)
.for_duration(1.0)
.ease(Ease::OutCubic)
.appear()
.spawn();

When to use: Simple fade-in effects, revealing objects smoothly.

Note: The tattva should be staged (hidden initially) for this to work as expected. Use scene.hide(id) before the animation if needed.

Legacy alias: .create() still works but .appear() is clearer.

fade_to

Animates opacity to a specific value.

// Fade out
timeline
.animate(label_id)
.at(2.0)
.for_duration(1.0)
.ease(Ease::InCubic)
.fade_to(0.0)
.spawn();

// Fade to semi-transparent
timeline
.animate(background_id)
.at(0.0)
.for_duration(0.5)
.ease(Ease::Linear)
.fade_to(0.3)
.spawn();

When to use: Fade in/out effects, transparency transitions, layering effects.

When NOT to use: If you just want to hide something immediately, use scene.hide(id) instead.

Path Animations

draw / undraw

Progressively reveals or hides path-like geometry.

// Draw a line from start to end
timeline
.animate(line_id)
.at(0.0)
.for_duration(2.0)
.ease(Ease::OutCubic)
.draw()
.spawn();

// Undraw it later
timeline
.animate(line_id)
.at(4.0)
.for_duration(1.5)
.ease(Ease::InCubic)
.undraw()
.spawn();

When to use: Lines, arrows, paths, curves, vector graphics.

When NOT to use: Filled shapes like circles or squares (they don't have a natural "draw" progression).

Legacy aliases: .write() / .unwrite() still work.

Text Animations

typewrite_text / untypewrite_text

Reveals text character by character, like typing.

timeline
.animate(label_id)
.at(0.0)
.for_duration(2.0)
.ease(Ease::Linear) // Usually linear for typing effect
.typewrite_text()
.spawn();

When to use: Code snippets, terminal output, dramatic text reveals.

When NOT to use: Math equations (characters may not align properly), short labels (too slow).

reveal_text / hide_text

Reveals text with a centered shifting effect.

timeline
.animate(title_id)
.at(0.0)
.for_duration(1.5)
.ease(Ease::OutCubic)
.reveal_text()
.spawn();

When to use: Titles, headings, emphasis text.

When NOT to use: Long paragraphs (effect may be too busy), code blocks.

Structured Object Animations

write_table / unwrite_table

Builds a table row by row or cell by cell.

timeline
.animate(table_id)
.at(0.0)
.for_duration(3.0)
.ease(Ease::OutCubic)
.write_table()
.spawn();

When to use: Data tables, matrices, structured data reveals.

write_surface / unwrite_surface

Progressively reveals a parametric surface.

timeline
.animate(surface_id)
.at(0.0)
.for_duration(4.0)
.ease(Ease::InOutQuad)
.write_surface()
.spawn();

When to use: 3D surfaces, mathematical visualizations, terrain reveals.

Morphing Animations

morph_from

Morphs one tattva's shape into another.

// Hide the target initially
scene.hide(circle_id);

// Morph square into circle
timeline
.animate(circle_id)
.at(0.5)
.for_duration(2.0)
.ease(Ease::InOutQuad)
.morph_from(square_id)
.spawn();

When to use: Shape transitions, transformations, visual metaphors.

When NOT to use:

  • Shapes are very different (may look strange)
  • You need precise control over intermediate states
  • Shapes have different numbers of vertices (may need manual vertex matching)

Important: Always hide the target tattva before morphing, or it will be visible at its final state.

morph_matching_staged

Morphs one group of tattvas into another while automatically staging the target group.

timeline.morph_matching_staged(
source_ids,
target_ids,
&mut scene,
1.0, // start time
3.0, // duration
Ease::InOutCubic,
);

When to use: Transitioning between multiple objects, equation transformations, diagram transitions.

Note: This is a helper that handles staging for you. Use raw morph_matching(...) only when you need manual control.

match_transform

Snaps a tattva's transform to match another tattva's.

timeline
.animate(copy_id)
.at(0.0)
.for_duration(1.0)
.ease(Ease::OutCubic)
.match_transform(original_id)
.spawn();

When to use: Synchronizing positions, creating copies, alignment animations.

When NOT to use: You know the exact target position (use .move_to() instead).

Easing Functions

Easing functions control how an animation progresses over time. They affect the "feel" of motion.

EasingFeelBest for
Ease::LinearConstant speedMechanical motion, technical diagrams
Ease::InQuadSlow start, fast endFalling objects, gravity
Ease::OutQuadFast start, slow endComing to rest, deceleration
Ease::InOutQuadSlow start and endNatural motion, smooth transitions
Ease::InCubicStronger slow startDramatic acceleration
Ease::OutCubicStronger slow endSmooth landing
Ease::InOutCubicStronger smooth both endsElegant motion
Ease::InOutSmoothSmoothstep (C1 continuous)Very smooth, organic motion

Choosing an easing:

  • OutCubic - Default choice for most animations
  • InOutQuad - When you want symmetrical smoothness
  • Linear - Typing effects, technical motion
  • OutQuad - Quick but gentle stops
// Smooth, natural motion
.ease(Ease::OutCubic)

// Symmetrical, elegant
.ease(Ease::InOutQuad)

// Mechanical, constant speed
.ease(Ease::Linear)

Callbacks

Sometimes you need to run custom code at specific times or over a duration.

call_at

Run code once at a specific time:

timeline.call_at(2.0, |scene| {
// This runs once when scene_time reaches 2.0
println!("Halfway through!");

// You can modify the scene here
scene.hide(some_id);
});

When to use: Discrete events, state changes, logging, cleanup.

call_during

Run code continuously over a duration:

timeline.call_during(1.0, 2.0, |scene, t| {
// This runs every frame between t=1.0 and t=3.0
// t is normalized from 0.0 to 1.0

// Example: custom motion path
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);
});

When to use:

  • Complex motion paths (circles, spirals, custom curves)
  • Dependent motion (one object following another)
  • Custom interpolation logic
  • Procedural animations

When NOT to use: Simple property changes (use animation verbs instead).

Camera Animations

Animate the camera just like any other object:

use murali::frontend::animation::camera_animation_builder::CameraAnimationBuilder;

timeline
.animate_camera()
.at(0.0)
.for_duration(2.0)
.ease(Ease::InOutQuad)
.move_to(Vec3::new(0.0, 0.0, 5.0))
.spawn();

Available camera animations:

  • .move_to(position) - Move camera position
  • .look_at(target) - Point camera at target
  • .set_view_width(width) - Zoom in/out

Sequencing Animations

Sequential (one after another)

let mut timeline = Timeline::new();

// First animation: 0.0 to 2.0
timeline.animate(id1).at(0.0).for_duration(2.0).move_to(...).spawn();

// Second animation: 2.0 to 4.0 (starts when first ends)
timeline.animate(id2).at(2.0).for_duration(2.0).move_to(...).spawn();

// Third animation: 4.0 to 5.5
timeline.animate(id3).at(4.0).for_duration(1.5).move_to(...).spawn();

Parallel (at the same time)

let mut timeline = Timeline::new();

// All start at t=0.0
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 = 0.2; // 0.2 second delay between each

for (i, id) in tattva_ids.iter().enumerate() {
timeline
.animate(*id)
.at(i as f32 * stagger)
.for_duration(1.0)
.ease(Ease::OutCubic)
.appear()
.spawn();
}

Common Patterns

Fade In, Wait, Fade Out

let mut timeline = Timeline::new();

// Fade in
timeline.animate(id).at(0.0).for_duration(1.0).appear().spawn();

// (Visible from t=1.0 to t=4.0)

// Fade out
timeline.animate(id).at(4.0).for_duration(1.0).fade_to(0.0).spawn();

Move and Scale Together

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();

Reveal Text Then Draw Arrow

// Text appears first
timeline
.animate(label_id)
.at(0.0)
.for_duration(1.0)
.ease(Ease::OutCubic)
.appear()
.spawn();

// Arrow draws after text is visible
timeline
.animate(arrow_id)
.at(1.5)
.for_duration(1.5)
.ease(Ease::OutCubic)
.draw()
.spawn();

Best Practices

Do's

  • ✅ Use .appear() for simple fade-ins
  • ✅ Use OutCubic as your default easing
  • ✅ Keep animation durations between 0.5 and 2.0 seconds for most effects
  • ✅ Use .call_during() for complex motion paths
  • ✅ Stagger animations for visual interest
  • ✅ Hide target tattvas before morphing

Don'ts

  • ❌ Don't use .draw() on filled shapes (circles, squares)
  • ❌ Don't make animations too fast (< 0.3 seconds) or too slow (> 3 seconds)
  • ❌ Don't use Linear easing for organic motion
  • ❌ Don't forget to call .spawn() at the end
  • ❌ Don't animate properties that don't exist on a tattva type

Troubleshooting

Animation doesn't play:

  • Did you call .spawn() at the end?
  • Did you call scene.play(timeline)?
  • Is the start time (.at(...)) reasonable?

Animation happens instantly:

  • Check .for_duration(...) is > 0
  • Verify the timeline is actually being played

Object doesn't appear:

  • For .appear(), make sure the object is staged (hidden initially)
  • Check that the object is within camera view
  • Verify opacity is not 0.0

Morph looks weird:

  • Source and target shapes may be too different
  • Try hiding the target before morphing
  • Consider using .fade_to() transitions instead

What's Next?

  • Scene and App - Learn more about scene management
  • Tattvas - Explore all available objects to animate
  • Camera - Control camera movement and framing
  • Updaters - For frame-by-frame custom logic