Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
/frames/*.png
*.DS_Store
4 changes: 2 additions & 2 deletions crates/bevy_motiongfx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ fn build_timeline(
.act(entity, path!(<Transform>::translation::x), |x| {
x + 6.0
})
.play(1.0)
.play(s(1))
.compile();

b.add_tracks(track);
Expand Down Expand Up @@ -90,7 +90,7 @@ fn build_timeline(
path!(<StandardMaterial>::base_color),
|_| Srgba::RED.into(),
)
.play(1.0)
.play(s(1))
.compile();

b.add_tracks(track);
Expand Down
108 changes: 94 additions & 14 deletions crates/bevy_motiongfx/src/controller.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use core::time::Duration;

use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_time::prelude::*;
Expand Down Expand Up @@ -30,10 +32,14 @@ fn realtime_player_update(
q_timelines.iter().filter(|(_, p)| p.is_playing)
{
if let Some(timeline) = motiongfx.get_timeline_mut(id) {
let target_time = timeline.target_time()
+ player.time_scale * time.delta_secs();
// Magnitude sets the step, sign picks the direction.
let delta = time.delta().mul_f64(player.time_scale.abs());

timeline.set_target_time(target_time);
if player.time_scale > 0.0 {
timeline.advance_time(delta);
} else if player.time_scale < 0.0 {
timeline.rewind_time(delta);
}
}
}
}
Expand All @@ -47,11 +53,8 @@ fn fixed_rate_player_update(
{
if let Some(timeline) = motiongfx.get_timeline_mut(id) {
// Each frame we update the timeline according to the fps.
let target_time =
timeline.target_time() + player.delta_secs();

timeline.set_target_time(target_time);
player.curr_frame += 1;
timeline.set_target_time(player.frame_time());
}
}
}
Expand Down Expand Up @@ -83,7 +86,7 @@ pub struct RealtimePlayer {
pub is_playing: bool,
/// The time scale of the player. Set this to negative
/// to play backwards.
pub time_scale: f32,
pub time_scale: f64,
}

impl RealtimePlayer {
Expand All @@ -107,7 +110,7 @@ impl RealtimePlayer {
/// Builder method for setting [`Self::time_scale`].
#[inline]
#[must_use]
pub const fn with_time_scale(mut self, time_scale: f32) -> Self {
pub const fn with_time_scale(mut self, time_scale: f64) -> Self {
self.time_scale = time_scale;
self
}
Expand All @@ -123,7 +126,7 @@ impl RealtimePlayer {
#[inline]
pub const fn set_time_scale(
&mut self,
time_scale: f32,
time_scale: f64,
) -> &mut Self {
self.time_scale = time_scale;
self
Expand Down Expand Up @@ -182,6 +185,22 @@ impl FixedRatePlayer {
1.0 / self.fps as f32
}

/// The exact timestamp of [`Self::curr_frame`].
///
/// A given frame index always maps to the same instant no matter
/// how long the recording runs.
///
/// Returns [`Duration::ZERO`] when [`Self::fps`] is zero.
#[inline]
#[must_use]
pub fn frame_time(&self) -> Duration {
if self.fps == 0 {
return Duration::ZERO;
}

Duration::from_secs(self.curr_frame) / self.fps as u32
}

/// Setter method for setting [`Self::is_playing`].
#[inline]
pub const fn set_playing(
Expand All @@ -192,28 +211,89 @@ impl FixedRatePlayer {
self
}
}

#[derive(Default, Component)]
pub struct PassivePlayer {
time: f32,
time: Duration,
track_index: usize,
}

impl PassivePlayer {
#[inline]
pub fn set_time(&mut self, time: f32) {
pub fn set_time(&mut self, time: Duration) -> &mut Self {
self.time = time;
self
}

#[inline]
pub fn set_track_index(&mut self, track_index: usize) {
pub fn set_track_index(
&mut self,
track_index: usize,
) -> &mut Self {
self.track_index = track_index;
self
}

pub fn get_time(&self) -> f32 {
pub fn get_time(&self) -> Duration {
self.time
}

pub fn get_track(&self) -> usize {
self.track_index
}
}

#[cfg(test)]
mod tests {
use motiongfx::time::{cs, ns, s};

use super::*;

fn at(fps: u16, frame: u64) -> Duration {
FixedRatePlayer {
fps,
curr_frame: frame,
is_playing: false,
}
.frame_time()
}

#[test]
fn frame_time_is_exact_where_the_rate_divides() {
assert_eq!(at(30, 0), Duration::ZERO);
assert_eq!(at(30, 30), s(1));
assert_eq!(at(30, 3), cs(10));
assert_eq!(at(25, 1), cs(4));
assert_eq!(at(60, 90), cs(150));
}

/// The whole point of deriving from the counter: no accumulated
/// error, so a long render lands on exact second boundaries.
#[test]
fn frame_time_does_not_drift_over_a_long_render() {
// 30 minutes at 30fps.
assert_eq!(at(30, 54_000), s(1800));
// Well past where the old nanosecond-based form overflowed.
assert_eq!(at(30, 30_000_000_000), s(1_000_000_000));
}

/// 1/3s is not representable in nanoseconds, so consecutive
/// frames differ by a nanosecond. The error must not compound.
#[test]
fn frame_time_stays_within_a_nanosecond_of_ideal() {
for frame in [1u64, 2, 1_000, 999_999] {
let ideal =
ns((frame as u128 * 1_000_000_000 / 3) as u64);

assert_eq!(at(3, frame).abs_diff(ideal), Duration::ZERO);
}
}

/// A rate of zero frames per second advances no time, rather than
/// dividing by zero or standing in some invented rate.
#[test]
fn zero_fps_yields_zero() {
assert_eq!(at(0, 0), Duration::ZERO);
assert_eq!(at(0, 5), Duration::ZERO);
}
}
12 changes: 6 additions & 6 deletions crates/motiongfx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ let id = 0;
let action = b.act(id, path!(<f32>), |x| x + 10.0);

// "Play" the action into a `TrackFragment` with a duration.
let frag = action.play(1.0);
let frag = action.play(s(1));

// Compile into a `Track`
// See "Track Ordering" section for composing fragments.
Expand All @@ -73,7 +73,7 @@ let mut timeline = b.compile();
timeline.bake_actions(&registry, &world);

// Sample at t = 0.5, world.0[0] should now be 5.0.
timeline.set_target_time(0.5);
timeline.set_target_time(cs(50));
timeline.queue_actions();
timeline.sample_queued_actions(&registry, &mut world);

Expand Down Expand Up @@ -147,7 +147,7 @@ let mut b = registry.create_builder::<World>();
Animations are built up in layers:

1. An **action** says what to animate and how to transform it.
2. A **track fragment** gives the action a duration by calling `.play(seconds)`.
2. A **track fragment** gives the action a duration by calling `.play(duration)`.
3. A **track** is one or more fragments compiled together. You can
order fragments before compiling (see [Track Ordering](#track-ordering)).
4. A **timeline** combines all your tracks into one playable sequence.
Expand All @@ -165,7 +165,7 @@ let action = b
.with_ease(ease::cubic::ease_in_out);

// Play: turn the action into a fragment with a 1-second duration.
let frag = action.play(1.0);
let frag = action.play(s(1));

// Compile the fragment into a Track.
let track = frag.compile();
Expand Down Expand Up @@ -196,7 +196,7 @@ Use `set_target_track` to jump between them.
timeline.bake_actions(&registry, &subjects);

// Set target time, queue, then sample.
timeline.set_target_time(0.5);
timeline.set_target_time(cs(50));
timeline.queue_actions();
timeline.sample_queued_actions(&registry, &mut subjects);

Expand Down Expand Up @@ -261,7 +261,7 @@ use motiongfx::prelude::*;
let f0 = TrackFragment::new();
let f1 = TrackFragment::new();

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

`f1` starts 0.5 seconds after `f0` begins, regardless of how long
Expand Down
20 changes: 9 additions & 11 deletions crates/motiongfx/benches/action_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ fn build_timeline(
builder
.act_builder(Id(i), path!(<Point>::x), |x| x + 72.0)
.with_interp(linear_f32)
.play(1.0)
.play(s(1))
})
.collect::<Vec<_>>();

Expand Down Expand Up @@ -156,16 +156,15 @@ fn bench_scrub(c: &mut Criterion) {
// the start so successive iterations are identical.
b.iter(|| {
for frame in 0..=FRAMES {
let t =
duration * frame as f32 / FRAMES as f32;
let t = duration * frame / FRAMES;
timeline.set_target_time(black_box(t));
timeline.queue_actions();
timeline.sample_queued_actions(
&registry, &mut world,
);
}
// Rewind to t=0 for the next iteration.
timeline.set_target_time(0.0);
timeline.set_target_time(s(0));
timeline.queue_actions();
timeline
.sample_queued_actions(&registry, &mut world);
Expand Down Expand Up @@ -278,7 +277,7 @@ fn build_mixed(
v + 1.0
})
.with_interp(linear_f32)
.play(1.0),
.play(s(1)),
builder
.act_builder(
Id(i),
Expand All @@ -289,7 +288,7 @@ fn build_mixed(
},
)
.with_interp(lerp2)
.play(1.0),
.play(s(1)),
builder
.act_builder(
Id(i),
Expand All @@ -301,7 +300,7 @@ fn build_mixed(
},
)
.with_interp(lerp3)
.play(1.0),
.play(s(1)),
builder
.act_builder(
Id(i),
Expand All @@ -314,7 +313,7 @@ fn build_mixed(
},
)
.with_interp(lerp4)
.play(1.0),
.play(s(1)),
]
})
.collect::<Vec<_>>();
Expand Down Expand Up @@ -383,15 +382,14 @@ fn bench_mixed_scrub(c: &mut Criterion) {

b.iter(|| {
for frame in 0..=FRAMES {
let t =
duration * frame as f32 / FRAMES as f32;
let t = duration * frame / FRAMES;
timeline.set_target_time(black_box(t));
timeline.queue_actions();
timeline.sample_queued_actions(
&registry, &mut world,
);
}
timeline.set_target_time(0.0);
timeline.set_target_time(s(0));
timeline.queue_actions();
timeline
.sample_queued_actions(&registry, &mut world);
Expand Down
2 changes: 1 addition & 1 deletion crates/motiongfx/docs/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub fn timeline() -> (Registry, Timeline<World>) {
let track = b
.act_builder(0usize, path!(<f32>), |x| x + 10.0)
.with_interp(|a: &f32, b: &f32, t| a + (b - a) * t)
.play(1.0)
.play(s(1))
.compile();
b.add_tracks(track);
let timeline = b.compile();
Expand Down
Loading
Loading