Skip to main content
Version: 0.1.0

Which API Should I Use?

Murali offers multiple ways to accomplish similar tasks. This guide helps you choose the right API for your situation.

As a general rule, prefer the highest-level authored API that clearly expresses your intent. Reach for lower-level APIs only when the authored surface stops being expressive enough.

Scene: play() vs play_named() vs set_timeline()​

Use scene.play(timeline)​

When: You have a single timeline (most common case)

let mut timeline = Timeline::new();
// ... add animations
scene.play(timeline);

Why: Clearest intent, simplest API, recommended for 95% of scenes.

Use scene.play_named(name, timeline)​

When: You have multiple timelines and want to organize them by name

scene.play_named("main", main_timeline);
scene.play_named("background", background_timeline);

Why: Better organization for complex scenes with multiple animation layers.

Important: Named timelines are separate scheduling lanes, not separate clocks. They still share the same scene_time.

Use scene.set_timeline(name, timeline)​

When: You're working with internal systems or need the lower-level API

scene.set_timeline("main", timeline);

Why: This is the underlying implementation. play() and play_named() are wrappers around this. Use it only if you need direct control or are wiring lower-level systems.

Recommendation: Start with play(), move to play_named() if you need multiple timelines, and only use set_timeline() when the wrapper APIs stop being expressive enough.


Adding Tattvas: add_tattva() vs add()​

Use scene.add_tattva(state, position)​

When: Adding any visual object (99% of cases)

let id = scene.add_tattva(
Circle::new(1.0, 48, color),
Vec3::new(0.0, 0.0, 0.0),
);

Why: Type-safe, clear intent, handles wrapping automatically.

Use scene.add(tattva)​

When: Working with already-wrapped tattvas or internal systems

let tattva = state.into_tattva();
let id = scene.add(tattva);

Why: Lower-level API for advanced scenarios.

Recommendation: Always use add_tattva() in application code.


Modifying Tattvas: Intent Helpers vs Typed Access​

Use Intent Helpers​

When: Changing common properties (position, scale, opacity, visibility)

scene.set_position_2d(id, Vec2::new(1.0, 2.0)); // For 2D scenes
scene.set_position_3d(id, Vec3::new(1.0, 2.0, 0.0)); // For 3D scenes
scene.set_scale(id, Vec3::splat(2.0));
scene.set_opacity(id, 0.5);
scene.hide(id);
scene.show(id);

Why: Clear, works on any tattva type, easy to read.

Use Typed Access​

When: Changing type-specific properties

if let Some(circle) = scene.get_tattva_typed_mut::<Circle>(id) {
circle.radius = 2.0;
circle.segments = 64;
}

Why: Access to type-specific state when the shared scene helpers are not enough.

Recommendation: Prefer intent helpers when possible, use typed access for type-specific needs.


Visibility: hide() vs set_opacity(0.0) vs fade_to(0.0)​

Use scene.hide(id)​

When: You want to immediately hide something (no animation)

scene.hide(id);

Why: Instant, clear intent, no animation overhead.

Use scene.set_opacity(id, 0.0)​

When: You want to set opacity immediately (no animation) through the shared styling surface

scene.set_opacity(id, 0.0);

Why: This is the low-level opacity control. In most authored code, scene.hide(id) is the clearer choice when you mean "make this disappear now."

Use .fade_to(0.0) animation​

When: You want to animate the fade out

timeline
.animate(id)
.at(2.0)
.for_duration(1.0)
.ease(Ease::InCubic)
.fade_to(0.0)
.spawn();

Why: Smooth animated transition.

Recommendation:

  • Staging objects before reveal → scene.hide(id)
  • Instant hide → scene.hide(id)
  • Immediate opacity adjustment → scene.set_opacity(id, value)
  • Animated fade → .fade_to(0.0)

Appearance: appear() vs fade_to(1.0) vs show()​

Use .appear() animation​

When: Animating an object from hidden to visible

scene.hide(id); // Stage it first

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

Why: Clear intent, handles staging automatically if object was hidden.

Use .fade_to(1.0) animation​

When: Animating opacity to full (object is already partially visible)

timeline
.animate(id)
.at(0.0)
.for_duration(1.0)
.ease(Ease::OutCubic)
.fade_to(1.0)
.spawn();

Why: Explicit about target opacity value.

Use scene.show(id)​

When: Immediately showing a hidden object (no animation)

scene.show(id);

Why: Instant, no animation overhead.

Recommendation:

  • Animated reveal → .appear()
  • Instant show → scene.show(id)
  • Specific opacity target → .fade_to(value)

Text: Label vs Typst vs Latex vs CodeBlock​

Use Label​

When: Simple text, titles, labels, UI text

Label::new("Hello World", 0.32)

Why: Fast, simple, no external dependencies, good for most text.

Limitations: Basic styling only, no math, no complex formatting.

Use Typst​

When: Rich text, formatted documents, modern typesetting

Typst::new("*Bold* and _italic_ text", 0.5) // source, world_height

Why: Modern, fast, good formatting, easier than LaTeX.

Limitations: Newer ecosystem, fewer legacy packages than LaTeX.

Use Latex​

When: Mathematical equations, academic content, complex formulas

Latex::new(r"\frac{a}{b} + \sqrt{c}", 0.5) // source, world_height

Why: Industry standard for math, extensive symbol support.

Limitations: Requires LaTeX installation, slower rendering, complex syntax.

Use CodeBlock​

When: Displaying code with syntax highlighting

CodeBlock::new("fn main() { println!(\"Hello\"); }", "rust", 0.5) // code, language, world_height

Why: Syntax highlighting, monospace font, code-specific formatting.

Limitations: Not for prose or math.

Recommendation:

  • Titles/labels → Label
  • Math → Latex
  • Rich text → Typst
  • Code → CodeBlock

Drawing Paths: draw() vs write() vs appear()​

Use .draw() animation​

When: Progressively revealing a path, line, or stroke

timeline
.animate(line_id)
.at(0.0)
.for_duration(2.0)
.ease(Ease::OutCubic)
.draw()
.spawn();

Why: Natural "drawing" effect for paths.

Works on: Lines, arrows, paths, curves, vector graphics.

Use .write() animation (legacy)​

When: Same as .draw() (legacy name)

timeline.animate(line_id).at(0.0).for_duration(2.0).write().spawn();

Why: Older name, still works but .draw() is preferred.

Use .appear() animation​

When: Fading in any object (including paths)

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

Why: Simple fade-in effect.

Recommendation:

  • Path-like objects (lines, arrows) → .draw()
  • Filled shapes (circles, squares) → .appear()
  • Legacy code → .write() works but migrate to .draw()

Text Animation: typewrite_text() vs reveal_text() vs appear()​

Use .typewrite_text()​

When: Character-by-character reveal, typing effect

timeline
.animate(label_id)
.at(0.0)
.for_duration(2.0)
.ease(Ease::Linear)
.typewrite_text()
.spawn();

Why: Mimics typing, good for code, terminal output, dramatic reveals.

Best for: Code snippets, terminal text, dramatic emphasis.

Use .reveal_text()​

When: Centered shifting reveal effect

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

Why: Elegant reveal with motion, good for titles.

Best for: Titles, headings, emphasis text.

Use .appear()​

When: Simple fade-in

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

Why: Clean, simple, works for any text.

Best for: Body text, labels, when you want subtle appearance.

Recommendation:

  • Dramatic/typing effect → .typewrite_text()
  • Elegant title reveal → .reveal_text()
  • Simple fade → .appear()

Layout: Scene Helpers vs Manual Positioning​

Use Scene Helpers​

When: Common layout tasks (edges, alignment, relative positioning)

scene.to_edge(id, Direction::Up, 0.35);
scene.next_to(label_id, circle_id, Direction::Right, 0.5);
scene.align_to(id1, id2, Anchor::Center);

Why: Clear intent, handles calculations for you, easier to read.

Use Manual Positioning​

When: Precise control, custom layouts, mathematical positioning

scene.set_position_2d(id, Vec2::new(2.5, 1.3)); // For 2D scenes
scene.set_position_3d(id, Vec3::new(2.5, 1.3, 0.0)); // For 3D scenes

Why: Full control, exact coordinates.

Recommendation:

  • Common layouts → Scene helpers
  • Precise positioning → Manual
  • Grid layouts → Manual with loops
  • Edge alignment → to_edge()

Camera: Orthographic vs Perspective​

Use Orthographic (Default)​

When: 2D scenes, mathematical diagrams, UI-like content

scene.camera_mut().set_view_width(16.0);

Why: No perspective distortion, parallel lines stay parallel, easier to reason about.

Best for: Most Murali scenes, 2D content, diagrams.

Use Perspective​

When: 3D scenes, depth perception needed

scene.camera_mut().set_perspective(60.0); // FOV in degrees

Why: Realistic 3D depth, objects get smaller with distance.

Best for: 3D visualizations, architectural views, realistic scenes.

Recommendation: Start with orthographic (default), switch to perspective only for 3D scenes.


Callbacks: call_at() vs call_during() vs Animation Verbs​

Use Animation Verbs​

When: Changing standard properties (position, scale, opacity)

timeline.animate(id).at(0.0).for_duration(2.0).move_to(...).spawn();

Why: Declarative, handles easing, cleaner code.

Use call_at()​

When: Discrete events, state changes, one-time actions

timeline.call_at(2.0, |scene| {
scene.hide(id);
println!("Checkpoint reached");
});

Why: Runs once at specific time, good for events.

Use call_during()​

When: Complex motion paths, custom interpolation, dependent motion

timeline.call_during(1.0, 2.0, |scene, t| {
let angle = t * std::f32::consts::TAU;
let pos = Vec3::new(angle.cos() * 3.0, angle.sin() * 3.0, 0.0);
scene.set_position_3d(id, pos);
});

Why: Full control over motion, runs every frame.

Recommendation:

  • Standard animations → Animation verbs
  • One-time events → call_at()
  • Custom motion → call_during()

Quick Reference Table​

TaskRecommended APIAlternative
Add objectscene.add_tattva()scene.add() (advanced)
Single timelinescene.play()scene.play_named()
Multiple timelinesscene.play_named()scene.set_timeline()
Change position (2D)scene.set_position_2d()Typed access
Change position (3D)scene.set_position_3d()Typed access
Hide instantlyscene.hide()scene.set_opacity(0.0)
Fade out.fade_to(0.0)scene.set_opacity()
Reveal object.appear().fade_to(1.0)
Simple textLabelTypst
Math textLatexTypst
Draw path.draw().write() (legacy)
Type text.typewrite_text().appear()
Edge layoutscene.to_edge()Manual position
2D sceneOrthographic (default)-
3D scenePerspective-
Simple animationAnimation verbcall_during()
Custom motioncall_during()Animation verb

General Principles​

  1. Prefer high-level authored APIs - They are clearer and closer to how Murali wants scenes to be written
  2. Use intent helpers first - scene.hide() is clearer than scene.set_opacity(0.0)
  3. Start simple - Use scene.play() before play_named()
  4. Match the tool to the task - Don't use .draw() on filled shapes
  5. Be explicit - .appear() is clearer than .fade_to(1.0) for reveals

When in Doubt​

  • For scenes: Use scene.play(timeline)
  • For tattvas: Use scene.add_tattva()
  • For visibility: Use scene.hide() / scene.show()
  • For text: Use Label first, Latex for math
  • For animations: Use animation verbs, not callbacks
  • For layout: Use scene helpers when available

What's Next?​