MotionGfx
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 *;
// `Timeline` can only be created via a `TimelineBuilder`.
let mut b = 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
// Every action needs an interpolation function.
.with_interp
// An optional easing function can be added.
.with_ease;
// Once an action is created, it can be "played" into a
// `TrackFragment` with a given duration.
let frag = action.play;
// 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;
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 *;
// 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 = new;
let pipeline_registry = new;
let mut timeline = new.compile;
// Bake actions into segments.
timeline.bake_actions;
// Actions needs to be queued before it can be sampled.
timeline.queue_actions;
timeline.sample_queued_actions;
Track Ordering
TrackFragments can be ordered using track ordering trait or
functions. There are 4 ways to order track fragments:
1. Chain
use *;
// Using empty fragments as an example only.
let f0 = new;
let f1 = new;
let f = .ord_chain;
// Or...
// use motiongfx::track::chain;
// let f = chain([f0, f1]);
Chaining runs f1 after f0 finishes.
2. All
use *;
let f0 = new;
let f1 = new;
let f = .ord_all;
All runs f0 and f1 concurrently and waits for all of them to
finish.
3. Any
use *;
let f0 = new;
let f1 = new;
let f = .ord_any;
Any runs f0 and f1 concurrenly and wait for any of them to finish.
4. Flow
use *;
let f0 = new;
let f1 = new;
let f = .ord_flow;
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 *;
;
let mut accessor_registry = new;
accessor_registry.register_typed;
Pipeline Registry
Pipelines handle the baking of actions and the sampling of animation segments for playback or preview.
use HashMap;
use *;
;
;
type SubjectWorld = ;
let mut pipeline_registry = new;
pipeline_registry.register_unchecked;
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 HashMap;
;
;
type SubjectWorld = ;
Below is a comprehensive example on how MotionGfx can be used with a custom world!
use HashMap;
use *;
// First, we have to initialize a subject world and the
// registries.
;
;
type SubjectWorld = ;
let mut subject_world = new;
let mut accessor_registry = new;
let mut pipeline_registry =
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;
// Similarly, the pipeline registry shoiud contain pipelines to
// bake and sample the fields in the subjects.
pipeline_registry.register_unchecked;
// Now that the registries are complete, we can start adding
// subjects into the subject world.
subject_world.insert;
// A timeline can only be created via the `TimelineBuilder`.
let mut builder = new;
let track = builder
// Creates the action.
.act
// Adds an interpolation method.
.with_interp
// Specifies the duration of the action.
.play
// Compiles into a track.
.compile;
// Adds the track to the builder.
builder.add_tracks;
// 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;
// Let's visualize the current state of the subject world before
// the sampling happens.
println!;
// We fast forward the timeline.
timeline.set_target_time;
// Actions need to be queued before it can be sampled.
// The queued actions are stored internally.
timeline.queue_actions;
timeline.sample_queued_actions;
// Visualize the state of the subject world after the sampling.
println!;
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:
- 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.