motiongfx

Crates.iomotiongfx
lib.rsmotiongfx
version0.2.0
created_at2025-01-26 09:44:10.053936+00
updated_at2026-01-22 14:04:28.384826+00
descriptionBackend agnostic motion graphics creation framework.
homepage
repositoryhttps://github.com/voxell-tech/motiongfx
max_upload_size
id1531206
size88,788
Nixon (nixonyh)

documentation

README

MotionGfx

License Crates.io Downloads Docs CI Discord

MotionGfx is a backend-agnostic motion graphics framework built on top of Bevy ECS. It provides a modular foundation for procedural animations.

Key Features

  • Backend agnostic: Works with any rendering backend.
  • Procedural: Write animations with code - loops, functions, logic.
  • Type-erased: Powered by Field Path, allowing runtime-flexible animation of arbitrary data.
  • Two-way playback: Play animations both forward and backward with no extra computation.
  • Batteries included: Packed with common easing and interpolation functions.

Core Concepts

Timeline

Timeline is a top-level structure that coordinates a sequence of tracks and their associated actions. Each track acts like a checkpoint, allowing animations to be grouped into discrete blocks (especially useful for creating slides).

A Track represents sequences of actions in chronological order, each with a defined start time and duration. Tracks ensure that actions within them are played in the correct temporal order.

use motiongfx::prelude::*;

// `Timeline` can only be created via a `TimelineBuilder`.
let mut b = TimelineBuilder::new();
// To create a track, you first have to create the actions.
let action = b
    // Create an action with:
    //   id   field path     action fn
    .act("x", field!(<f32>), |x| x + 1.0)
    // Every action needs an interpolation function.
    .with_interp(|&a, &b, t| a + (b - a) * t)
    // An optional easing function can be added.
    .with_ease(ease::cubic::ease_in_out);

// Once an action is created, it can be "played" into a
// `TrackFragment` with a given duration.
let frag = action.play(1.0);

// Which can then be compiled into a `Track`.
let track = frag.compile();

// 1 or more tracks can be added to the builder to create a timeline.
b.add_tracks(track);
let timeline = b.compile();

Bake and Sample Timeline

Once a timeline is created, it is ready for baking and sampling. Bake must happen before sample. Otherwise, sampling it will be a no-op.

Registries must be created to perform baking/sampling. For more info about registries, see below.

use motiongfx::prelude::*;

// Using a dummy world, in reality, it should be something that maps
// subjects' Ids to their animatable components.
type SubjectWorld = ();

let mut world: SubjectWorld = ();
let accessor_registry = FieldAccessorRegistry::new();
let pipeline_registry = PipelineRegistry::<SubjectWorld>::new();
let mut timeline = TimelineBuilder::new().compile();

// Bake actions into segments.
timeline.bake_actions(
    &accessor_registry,
    &pipeline_registry,
    &world,
);

// Actions needs to be queued before it can be sampled.
timeline.queue_actions();
timeline.sample_queued_actions(
    &accessor_registry,
    &pipeline_registry,
    &mut world,
);

Track Ordering

TrackFragments can be ordered using track ordering trait or functions. There are 4 ways to order track fragments:

1. Chain

use motiongfx::prelude::*;

// Using empty fragments as an example only.
let f0 = TrackFragment::new();
let f1 = TrackFragment::new();

let f = [f0, f1].ord_chain();
// Or...
// use motiongfx::track::chain;
// let f = chain([f0, f1]);

Chaining runs f1 after f0 finishes.

2. All

use motiongfx::prelude::*;

let f0 = TrackFragment::new();
let f1 = TrackFragment::new();

let f = [f0, f1].ord_all();

All runs f0 and f1 concurrently and waits for all of them to finish.

3. Any

use motiongfx::prelude::*;

let f0 = TrackFragment::new();
let f1 = TrackFragment::new();

let f = [f0, f1].ord_any();

Any runs f0 and f1 concurrenly and wait for any of them to finish.

4. Flow

use motiongfx::prelude::*;

let f0 = TrackFragment::new();
let f1 = TrackFragment::new();

let f = [f0, f1].ord_flow(0.5);

Flow runs f1 after f0 with a fixed delay time rather than waiting for f0 to finish.

Registries

Registries are used to perform reflection and safely erase types.

Field Accessor Regisry

The FieldAccessorRegistry maintains a mapping between animatable fields and their corresponding accessors, enabling MotionGfx to read and write values on arbitrary data structures in a type-safe yet dynamic way.

use motiongfx::prelude::*;

#[derive(Debug, Clone, Copy)]
struct Subject(f32);

let mut accessor_registry = FieldAccessorRegistry::new();
accessor_registry.register_typed(
    field!(<Subject>::0),
    accessor!(<Subject>::0)
);

Pipeline Registry

Pipelines handle the baking of actions and the sampling of animation segments for playback or preview.

use std::collections::HashMap;

use motiongfx::prelude::*;

#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
struct Id(u32);
#[derive(Debug, Clone, Copy)]
struct Subject(f32);
type SubjectWorld = HashMap<Id, Subject>;

let mut pipeline_registry = PipelineRegistry::<SubjectWorld>::new();
pipeline_registry.register_unchecked(
    PipelineKey::new::<Id, Subject, f32>(),
    Pipeline::new(
        |world, ctx| {
            ctx.bake::<Id, Subject, f32>(|id| world.get(&id));
        },
        |world, ctx| {
            ctx.sample::<Id, Subject, f32>(
                |id, target, accessor| {
                    if let Some(x) = world.get_mut(&id) {
                        *accessor.get_mut(x) = target;
                    }
                },
            );
        },
    ),
);

Subject World

Because MotionGfx is backend agnostic, it can be used to animate subjects in any world. A typical subject world would hold unique Ids that maps subject entities to their associated animatable components.

A simple example of such would be a HashMap.

use std::collections::HashMap;

#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
struct Id(u32);
#[derive(Debug, Clone, Copy)]
struct Subject(f32);
type SubjectWorld = HashMap<Id, Subject>;

Below is a comprehensive example on how MotionGfx can be used with a custom world!

use std::collections::HashMap;

use motiongfx::prelude::*;

// First, we have to initialize a subject world and the
// registries.
#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
struct Id(u32);
#[derive(Debug, Clone, Copy)]
struct Subject(f32);
type SubjectWorld = HashMap<Id, Subject>;

let mut subject_world = SubjectWorld::new();
let mut accessor_registry = FieldAccessorRegistry::new();
let mut pipeline_registry =
    PipelineRegistry::<SubjectWorld>::new();

// The accessor registry should contain accessors to the fields in
// the subjects. In our case, it's just the first field in
// the tuple struct: `Subject::0`.

accessor_registry.register_typed(
    field!(<Subject>::0),
    accessor!(<Subject>::0),
);

// Similarly, the pipeline registry shoiud contain pipelines to
// bake and sample the fields in the subjects.

pipeline_registry.register_unchecked(
    PipelineKey::new::<Id, Subject, f32>(),
    Pipeline::new(
        |world, ctx| {
            ctx.bake::<Id, Subject, f32>(|id| world.get(&id));
        },
        |world, ctx| {
            ctx.sample::<Id, Subject, f32>(
                |id, target, accessor| {
                    if let Some(x) = world.get_mut(&id) {
                        *accessor.get_mut(x) = target;
                    }
                },
            );
        },
    ),
);

// Now that the registries are complete, we can start adding
// subjects into the subject world.

subject_world.insert(Id(1), Subject(0.0));

// A timeline can only be created via the `TimelineBuilder`.

let mut builder = TimelineBuilder::new();

let track = builder
    // Creates the action.
    .act(Id(1), field!(<Subject>::0), |x| x + 10.0)
    // Adds an interpolation method.
    .with_interp(|&a, &b, t| a + (b - a) * t)
    // Specifies the duration of the action.
    .play(1.0)
    // Compiles into a track.
    .compile();

// Adds the track to the builder.
builder.add_tracks(track);
// And compile it into a timeline.
let mut timeline = builder.compile();
// The timeline needs to be baked once before sampling can happen.
timeline.bake_actions(
    &accessor_registry,
    &pipeline_registry,
    &subject_world,
);

// Let's visualize the current state of the subject world before
// the sampling happens.
println!("Before: {:?}", subject_world);

// We fast forward the timeline.
timeline.set_target_time(0.5);
// Actions need to be queued before it can be sampled.
// The queued actions are stored internally.
timeline.queue_actions();
timeline.sample_queued_actions(
    &accessor_registry,
    &pipeline_registry,
    &mut subject_world,
);

// Visualize the state of the subject world after the sampling.
println!("After:  {:?}", subject_world);

Officially Supported Backends

Join the community!

You can join us on the Voxell discord server.

Inspirations and Similar Projects

Version Matrix

Bevy MotionGfx
0.18 0.2
0.17 0.1

License

motiongfx is dual-licensed under either:

This means you can select the license you prefer! This dual-licensing approach is the de-facto standard in the Rust ecosystem and there are very good reasons to include both.

Commit count: 38

cargo fmt