Skip to main content
Version: 0.1.0

Scene & App

This guide covers how to work with Scene (the container for your animation) and App (the runtime that brings it to life).

For most authored code, prefer the higher-level APIs first:

  • scene.add_tattva(...) instead of scene.add(...)
  • scene intent helpers like hide, show, set_position_2d, set_position_3d, set_scale, and set_rotation
  • scene.play(...) for the common single-timeline case

Use lower-level access only when you need type-specific mutation, generic scene tooling, or explicit multi-timeline organization.

Scene

Scene is the authoritative source of truth for your animation. It owns all tattvas, timelines, camera state, and scene time.

use murali::engine::scene::Scene;

let mut scene = Scene::new();

What the scene owns:

  • All tattvas (visual objects)
  • All timelines
  • Scene time (current playback position)
  • Camera configuration
  • Updaters (frame-by-frame callbacks)

Adding Tattvas

The primary way to add objects to your scene is add_tattva:

use murali::frontend::collection::primitives::circle::Circle;
use glam::{Vec3, Vec4};

// Returns a TattvaId for later reference
let circle_id = scene.add_tattva(
Circle::new(1.5, 48, Vec4::new(0.2, 0.6, 0.3, 1.0)),
Vec3::new(0.0, 0.0, 0.0), // position
);

Key points:

  • add_tattva(state, position) is the preferred API for all user code
  • It returns a TattvaId that you use to reference the tattva later
  • The position is in world-space coordinates (not pixels)

Multiple Tattvas

let title_id = scene.add_tattva(
Label::new("My Animation", 0.32),
Vec3::new(0.0, 3.0, 0.0),
);

let square_id = scene.add_tattva(
Square::new(1.2, Vec4::new(0.9, 0.3, 0.3, 1.0)),
Vec3::new(-2.0, 0.0, 0.0),
);

let circle_id = scene.add_tattva(
Circle::new(0.8, 48, Vec4::new(0.2, 0.6, 0.3, 1.0)),
Vec3::new(2.0, 0.0, 0.0),
);

Advanced: scene.add()

scene.add(...) is a lower-level insertion API for already-wrapped tattva types. It's used internally and in advanced scenarios, but you should prefer add_tattva() in normal code.

// Advanced usage - not recommended for typical scenes
let tattva = state.into_tattva();
let id = scene.add(tattva);

Modifying Tattvas

Preferred: Intent Helpers

For common operations, use the scene's intent helpers. These are clear, type-safe, and work across all tattva types:

// Visibility
scene.hide(id);
scene.show(id);

// Opacity
scene.set_opacity(id, 0.5);

// Position
scene.set_position_2d(id, glam::Vec2::new(1.0, 2.0));
scene.set_position_3d(id, glam::Vec3::new(1.0, 2.0, 0.5));

// Transform
scene.set_scale(id, glam::Vec3::splat(1.25));
scene.set_rotation(id, glam::Quat::from_rotation_z(0.4));

When to use: Anytime you need to change position, visibility, opacity, scale, or rotation.

Why these are better: They're explicit about intent, work on any tattva type, and are easier to read than low-level access.

Low-Level Access

When you need to modify type-specific properties (like a circle's radius or a label's text), use typed access:

// Typed mutable access
if let Some(circle) = scene.get_tattva_typed_mut::<Circle>(circle_id) {
circle.state.radius = 2.0;
circle.state.segments = 64;
}

// Typed immutable access
if let Some(label) = scene.get_tattva_typed::<Label>(label_id) {
println!("Label text: {}", label.state.text);
}

When to use: When you need to access or modify properties specific to a tattva type.

Untyped Access

For advanced scenarios where you don't know the concrete type:

// Untyped mutable access
if let Some(tattva) = scene.get_tattva_any_mut(id) {
// Access through TattvaTrait methods
}

// Untyped immutable access
if let Some(tattva) = scene.get_tattva_any(id) {
// Read-only access
}

When to use: Generic code that works with any tattva type, internal systems, advanced patterns.

Removing Tattvas

scene.remove_tattva(id);

The tattva is marked for removal and will be cleaned up during the next sync pass.

Camera

The camera determines what part of the world is visible and how it's projected to the screen.

// Access the camera
scene.camera_mut().position = Vec3::new(0.0, 0.0, 10.0);
scene.camera_mut().set_view_width(16.0);

Common camera setup for 2D scenes:

// Position camera 10 units away, looking at origin
scene.camera_mut().position = Vec3::new(0.0, 0.0, 10.0);

// Set view width (how many world units fit horizontally)
scene.camera_mut().set_view_width(16.0);

Key concepts:

  • Position - Where the camera is in world space
  • View width - How many world units fit horizontally on screen
  • Look at - What point the camera is aimed at (default: origin)

For more details, see the Camera guide.

Layout Helpers

Murali provides helpers for common layout tasks:

to_edge

Push a tattva to the edge of the frame with padding:

use murali::frontend::layout::Direction;

// Push title to top with 0.35 units of padding
scene.to_edge(title_id, Direction::Up, 0.35);

// Push footer to bottom
scene.to_edge(footer_id, Direction::Down, 0.5);

// Push to left or right
scene.to_edge(sidebar_id, Direction::Left, 0.3);
scene.to_edge(menu_id, Direction::Right, 0.3);

next_to

Position one tattva relative to another:

// Place label next to circle, on the right side
scene.next_to(label_id, circle_id, Direction::Right, 0.5);

align_to

Align one tattva to another:

use murali::frontend::layout::Anchor;

// Align label to circle's center
scene.align_to(label_id, circle_id, Anchor::Center);

For more layout patterns, see the Layout and Composition guide (coming soon).

Timelines

Timelines schedule when animations happen.

Single Timeline (Preferred)

Most scenes use a single timeline:

let mut timeline = Timeline::new();

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

// Play the timeline (uses name "main" internally)
scene.play(timeline);

This is the recommended API for most use cases.

Multiple Named Timelines

For complex scenes, you can use multiple timelines:

let mut main_timeline = Timeline::new();
let mut background_timeline = Timeline::new();

// Add animations to each timeline...

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

Important: All timelines share the same scene_time. They progress together, not independently.

When to use multiple timelines:

  • Organizing complex scenes by layer or concern
  • Separating foreground and background animations
  • Managing different "tracks" independently

If you're not sure, start with one timeline. Multiple timelines are mainly an organizational tool, not a way to get independent clocks or playback control.

Current limitations:

  • All timelines advance with the same scene time
  • No independent playback control per timeline
  • This is an advanced feature still being refined

For more details, see the Timelines guide (coming soon).

Capture Helpers

For exporting screenshots or GIFs at specific times:

Screenshots

// Capture at specific times
scene.capture_screenshots([0.5, 1.0, 2.0]);

// Capture with custom filenames
scene.capture_screenshots_named([
(0.5, Some("captures/intro.png")),
(1.0, None::<&str>), // Auto-generated name
(2.0, Some("captures/final.png")),
]);

GIFs

// Capture a GIF from multiple frames
scene.capture_gif("overview", [0.5, 1.0, 2.0]);

When to use: Creating documentation, generating thumbnails, debugging specific frames.

Common Scene Patterns

Title and Content

// Title at top
let title_id = scene.add_tattva(
Label::new("My Topic", 0.36),
Vec3::ZERO,
);
scene.to_edge(title_id, Direction::Up, 0.35);

// Subtitle below title
let subtitle_id = scene.add_tattva(
Label::new("A detailed explanation", 0.18),
Vec3::new(0.0, 2.5, 0.0),
);

// Main content in center
let content_id = scene.add_tattva(
Circle::new(2.0, 48, color),
Vec3::ZERO,
);

Grid Layout

let spacing = 2.5;
let mut ids = Vec::new();

for row in 0..3 {
for col in 0..3 {
let x = (col as f32 - 1.0) * spacing;
let y = (row as f32 - 1.0) * spacing;

let id = scene.add_tattva(
Square::new(0.8, color),
Vec3::new(x, y, 0.0),
);
ids.push(id);
}
}

Staged Objects (Hidden Initially)

// Add object but keep it hidden
let circle_id = scene.add_tattva(
Circle::new(1.0, 48, color),
Vec3::ZERO,
);
scene.hide(circle_id);

// Later, reveal it with animation
timeline
.animate(circle_id)
.at(1.0)
.for_duration(1.0)
.ease(Ease::OutCubic)
.appear()
.spawn();

App

App is the runtime that executes your scene — either as a live preview window or as a video export.

use murali::App;

App::new()?
.with_scene(scene)
.run_app()

Preview vs Export

By default, run_app() checks command-line arguments to decide the mode:

  • No flags → Export mode (renders video)
  • --preview → Preview mode (opens window)
  • --export → Export mode (explicit)

Running from CLI

# Preview mode (interactive window)
cargo run --example my_scene --release -- --preview

# Export mode (renders video)
cargo run --example my_scene --release -- --export

# Default (export)
cargo run --example my_scene --release

Forcing a Mode in Code

You can override the default behavior:

// Always open preview window
App::new()?
.with_scene(scene)
.with_preview()
.run_app()

// Always export video
App::new()?
.with_scene(scene)
.with_video_export()
.run_app()

// Export frames in addition to video export
App::new()?
.with_scene(scene)
.with_frames_export(true)
.run_app()

Preview Mode

Preview mode opens an interactive window where you can watch your animation play.

Characteristics:

  • Advances one frame at a time
  • Tries to maintain target FPS (usually 60)
  • Slows down if your machine can't keep up (doesn't skip frames)
  • Allows camera control with mouse/keyboard

Why it doesn't skip frames: Unlike games or video players, Murali preview prioritizes showing every frame so you can inspect motion, easing, and visual changes. If your machine is slow, preview will run slower than real-time, but you'll see every frame in order.

Preview Controls

InputAction
[O]Switch to orbit camera mode
[P]Switch to pan/zoom camera mode
Left dragMove camera (orbit or pan depending on mode)
ScrollZoom in/out
[Esc]Exit preview

Camera modes:

  • Orbit - Rotate around a center point (good for 3D scenes)
  • Pan/Zoom - Move camera in 2D plane (good for 2D scenes)

Export Mode

Export mode renders your animation to video or image frames.

Characteristics:

  • Renders at exact target FPS (deterministic)
  • No window (headless rendering)
  • Outputs to configured directory
  • Can export video (MP4) or frames (PNG)

Output location: Check your project's export configuration or RenderConfig for output paths.

Export Settings

use murali::engine::export::ExportSettings;

let settings = ExportSettings {
output_dir: "output/my_animation".into(),
fps: 60,
duration_seconds: 10.0,
// ... other settings
};

App::new()?
.with_scene(scene)
.with_export_settings(settings)
.run_app()

Render Options

Customize rendering behavior:

use murali::engine::render::RenderOptions;

let options = RenderOptions {
video: Some(true), // Enable video export
frames: Some(true), // Also export individual frames
// ... other options
};

App::new()?
.with_scene(scene)
.with_render_options(options)
.run_app()

Common App Patterns

Simple Preview

fn main() -> anyhow::Result<()> {
let mut scene = Scene::new();
// ... build scene

App::new()?.with_scene(scene).run_app()
}

Run with: cargo run --example my_scene -- --preview

Always Export

fn main() -> anyhow::Result<()> {
let mut scene = Scene::new();
// ... build scene

App::new()?
.with_scene(scene)
.with_video_export()
.run_app()
}

Custom Export Settings

fn main() -> anyhow::Result<()> {
let mut scene = Scene::new();
// ... build scene

let settings = ExportSettings {
output_dir: "output/tutorial".into(),
fps: 60,
duration_seconds: 15.0,
..Default::default()
};

App::new()?
.with_scene(scene)
.with_export_settings(settings)
.run_app()
}

Best Practices

Scene Management

Do:

  • Use add_tattva() for adding objects
  • Use intent helpers (set_position_2d, set_position_3d, hide, show) for common operations
  • Keep tattva IDs in variables for later reference
  • Use scene.play(timeline) for single-timeline scenes
  • Stage objects (hide them) before reveal animations

Don't:

  • Use scene.add() unless you have a specific advanced need
  • Forget to save tattva IDs returned by add_tattva()
  • Try to animate tattvas that don't exist
  • Modify tattva properties during animation (let timeline handle it)

App Usage

Do:

  • Use --preview flag during development
  • Use --export for final renders
  • Test in preview mode before exporting
  • Use release builds for smooth preview (--release)

Don't:

  • Export without previewing first
  • Run preview in debug mode (it's slow)
  • Forget to check export output directory
  • Mix preview and export logic in the same run

What's Next?

  • Animations - Learn all animation verbs and patterns
  • Camera - Control camera movement and framing
  • Tattvas - Explore all available visual objects