linebender / bevy_vello

An integration to render with Vello in Bevy game engine.
https://linebender.org/bevy_vello/
Apache License 2.0
117 stars 12 forks source link

Low-level animation control #29

Closed kristoff3r closed 6 months ago

kristoff3r commented 6 months ago

First, great work on this crate so far!

I'm evaluating using this for a project where it seems lottie might fit nicely, but I would need more fine grained than what this crate currently provides. An example effect would be a health bar that I can set to e.g. 80%, and then after taking damage it would animate towards 60%. Is it possible for this crate to expose an API that makes this possible?

Also, what is the recommended way to generate lottie files without using Adobe? I tried a bunch of stuff yesterday to convert one of my SVGs to no avail.

simbleau commented 6 months ago

You can do this currently- Example here: https://github.com/vectorgameexperts/bevy-vello/blob/main/demo/src/main.rs

The demo which exhibits state machine behavior and playhead tracking: https://vectorgameexperts.github.io/bevy-vello/ (must be on Chrome or FF Nightly with a flag)

Also, I would recommend using lottielab.com to create lottie animations. It is what I am using to support and maintain this crate.

I am going to assume you have a health bar lottie file which is animated and goes from 0 to 100 health linearly, in a 100 frame rate animation.

Hence, if you had a playhead at 60.0, the animation would reflect 60/100 health.

First, spawn your health bar:

commands
        .spawn(VelloAssetBundle {
            vector: asset_server.load("../assets/health-bar.json"),
            ..default()
        })
        .insert(
            LottiePlayer::new("forward")
                .with_state({
                    PlayerState::new("forward")
                        .with_playback_options(PlaybackOptions {
                            looping: PlaybackLoopBehavior::DoNotLoop,
                            direction: PlaybackDirection::Normal,
                            autoplay: false,
                            ..default()
                        })
                        .with_theme(Theme::new().add("health", Color::GREEN))
                        .reset_playhead_on_start(false)
                })
                .with_state(
                    PlayerState::new("reverse")
                        .with_playback_options(PlaybackOptions {
                            looping: PlaybackLoopBehavior::DoNotLoop,
                            direction: PlaybackDirection::Reverse,
                            autoplay: false,
                            ..default()
                        })
                        .with_theme(Theme::new().add("health", Color::RED))
                )
...

Then, I would use an event system like this:

pub fn control_animation(
    mut health_events: EventReader<HealthEvent>,
    mut controller_query: Query<(
        &mut LottiePlayer,
        &mut Playhead,
        &mut PlaybackOptions,
        &mut Theme,
    ), With<HealthBarTag>>,
    mut player_query: Query<(
        &mut MyPlayer
    ), With<HealthBarTag>>,
    assets: Res<Assets<VelloAsset>>,
) {
    let Ok((mut controller, mut playhead, mut options, mut theme, handle)) =
        controller_query.get_single_mut()
    else {
        return;
    };
    let Ok(mut my_player) = player_query.get_single_mut()
    else {
        return;
    };
    for health_event in health_events.read() {
        match health_event {
            HealthEvent::Damaged(amount) => {
                my_player.health = (my_player.health - amount).min(0.0);
                controller.transition("reverse");
                controller.play();
            }
            HealthEvent::Healed(amount) => {
                my_player.health = (my_player.health + amount).max(100.0);
                controller.transition("forward");
                controller.play();
            }
        }
    }
    theme.edit("layer name", Color::YELLOW); // Interpolate between red and green based on my_player.health
    if my_player.health == playhead.frame() {
        // Reached desired health
        controller.pause();
    }
}
simbleau commented 6 months ago

Side note: I think this would make a great example. If you want to produce one, I'll accept the PR. Otherwise, I may do this myself when I find bandwidth.

kristoff3r commented 6 months ago

Thanks for the comprehensive response, that looks pretty easy to work with!

I'll try to implement it as an example and do a PR.