MotionGfx is a backend-agnostic motion graphics framework built on top of Bevy ECS. It provides a modular foundation for procedural animations.
- 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.
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 and 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();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,
);TrackFragments can be ordered using track ordering trait or
functions. There are 4 ways to order track fragments:
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.
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.
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.
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 are used to perform reflection and safely erase types.
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)
);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;
}
},
);
},
),
);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);You can join us on the Voxell discord server.
motiongfx is dual-licensed under either:
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
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.