linebender / interpoli

Apache License 2.0
15 stars 6 forks source link

SMPTE Timecodes & Timelines, Sequences and Keyframes. #22

Open TheNachoBIT opened 2 months ago

TheNachoBIT commented 2 months ago

I'm making this a draft for now, this PR is to document the progress i've been making so far with this experiment.

This experiment can also fail. If you see any kind of flaw, please feel free to point it out.

The final goals of this (if succeeds) are:

Progress.

TO-DO In Review

SMPTE Timecodes (Update 2).

SMPTE Timecodes are time units that are set like a clock: HH:MM:SS:FF (Hours, Minutes, Seconds, Frames).

For convenience, there's a macro that helps visualize this concept better in code:

tcode_hmsf!(01:23:45:01) // A timecode with Hours (h), Minutes (m), Seconds (s) and Frames (f).

~(Sadly, Rust's macros don't allow me to use ":" so i have to use ";" instead, if there's a workaround for this, please let me know)~

EDIT: I fixed it!

Timecodes can be used as a Timestamp, or as a value for a Timeline, by setting the Framerate:

tcode_hmsf_framerate!(00:01:02:56, Framerate::Fixed(20.0))

There's Framerate::Fixed(n) and Framerate::Interpolated(n). Once Timelines become a thing, fixed framerates will round up to the nearest frame when the tween is calculated, which will be useful for frame-by-frame animations. Interpolated frames on the other hand, will interpolate regardless of the framerate you're running (unless you explicitly set "hold" frames).

Timecodes with a framerate can advance and reverse by frames, seconds, minutes, hours and use Duration (Instant coming soon).

Example: Play one second frame-by-frame.

let mut time = tcode_hmsf_framerate!(00:00:00:00, Framerate::Fixed(24.0));

for i in 0..24 {
    time.next_frame();
}

println!("{:?}", time.as_string()); // Outputs "00:00:01:00 (24.0)"

Example: Add by Duration.

use std::time::Duration;

let mut time = tcode_hmsf_framerate!(00:00:00:00, Framerate::Fixed(24.0));

time.add_by_duration(Duration::from_millis(999));

println!("{:?}", time.as_string()); // Outputs "00:00:00:23 (24.0)"

Example: Sub by Duration.

use std::time::Duration;

let mut time = tcode_hmsf_framerate!(01:00:00:00, Framerate::Fixed(24.0)); // 1 hour

time.sub_by_duration(Duration::from_secs(1800));

println!("{:?}", time.as_string()); // Outputs "00:30:00:00 (24.0)" (30 minutes)

The draft repository contains more examples in form of tests inside of lib.rs.

This is all of the progress for now, for any questions, please feel free to ask :)

TheNachoBIT commented 1 month ago

Alright, i think this is ready for review! :D

In here i'll show how the Timelines & Sequences API works:

Timelines

A timeline is like a director: It takes care of storing the sequences and it contains a "Sequence Player" that allows you to play all of the animations that are stored in a specified framerate.

let mut timeline_one = Timeline::new(Framerate::Fixed(24.0)); // Fixed Timeline
let mut timeline_two = Timeline::new(Framerate::Interpolated(24.0)); // Interpolated Timeline

You can play forwards, and backwards based on what you want, and you can use Timestamps, Timecodes and Durations.

let mut timeline = Timeline::new(Framerate::Interpolated(24.0));

// If you're in a game/real-time context, you can use
// your engine's delta time to play the timeline.
timeline.add_by_duration(time.delta());

// And also play it backwards.
timeline.sub_by_duration(time.delta());

Sequences

Sequences store all of the keyframes of the animation of a specific variable.

This is an example for creating sequences in a timeline:

let mut t = Timeline::new(Framerate::Interpolated(12.0));

// Let's create a Sequence called "count", that'll be an 'f64'.
let s: &mut Sequence<f64> = t.new_sequence("count").unwrap();

// "count" will be an animation that goes from 0 to 1 in one second.
s.add_keyframe_at_timestamp(Keyframe { value: 0.0 }, tcode_hms!(00:00:00));
s.add_keyframe_at_timestamp(Keyframe { value: 1.0 }, tcode_hms!(00:00:01));

As long as the type contains interpoli's Tween trait, it can be animated.

Here's an example with kurbo's Vec2:

let s: &mut Sequence<Vec2> = t.new_sequence("position").unwrap();

s.add_keyframe_at_timestamp(
    Keyframe {
        value: Vec2::new(200.0, 200.0),
    },
    &tcode_hms!(00:00:00),
);
s.add_keyframe_at_timestamp(
    Keyframe {
        value: Vec2::new(300.0, 200.0),
    },
    &tcode_hms!(00:00:01),
);
s.add_keyframe_at_timestamp(
    Keyframe {
        value: Vec2::new(800.0, 400.0),
    },
    &tcode_hms!(00:00:02),
);
s.add_keyframe_at_timestamp(
    Keyframe {
        value: Vec2::new(700.0, 500.0),
    },
    &tcode_hms!(00:00:03),
);

To finally use the animation's result, by taking the Vec2 example, you can get the value via tween_by_name():

let player_position: Vec2 = t.tween_by_name::<Vec2>("position");

If you're aiming for performance, you can get the sequence's pointer and store it as a reference.

let pointer = t.get_sequence_pointer("position").unwrap();

// More code here...

let player_position: Vec2 = t.tween_by_pointer::<Vec2>(pointer);

Here's an example of the Timelines that you can see and run for yourself in here. (TODO for myself: Add instructions and record video).

Static Timelines

These work like Timelines, but the size of the Sequences are always known at compile-time, so they can only have one type (or you can use an enum if you want to store different types).

let mut timeline: StaticTimeline<Vec2> = StaticTimeline::new(Framerate::Fixed(24.0));
let sequence = timeline.new_sequence("sequence").unwrap();
sequence.add_keyframes_at_timestamp(vec![
    (
        Keyframe {
            value: Vec2::new(0.0, 1.0),
        },
        &tcode_hmsf!(00:00:01:00),
    ),
    (
        Keyframe {
            value: Vec2::new(1.0, 1.0),
        },
        &tcode_hmsf!(00:00:02:00),
    ),
    (
        Keyframe {
            value: Vec2::new(1.0, 2.0),
        },
        &tcode_hmsf!(00:00:03:00),
    ),
]);

This gives even more performance at the cost of flexibility, so if you're in a situation where you need to squeeze as much performance as you want, you can use these.

Stuff that i didn't add yet (Otherwise this PR was going to be really big to review).

TheNachoBIT commented 1 month ago

I'm gonna need a ton of help and wisdom from everyone, because there's probably a lot of stuff that i'm doing wrong or can be handled much better. So any kind of input is appreciated c: