use std::time::Duration;
use super::{LottiePlayer, PlayerTransition, asset::VelloLottie};
use crate::{
PlaybackDirection, PlaybackLoopBehavior, PlaybackOptions, Playhead,
integrations::lottie::{
LottieAssetVariant, PlaybackPlayMode, UiVelloLottie, VelloLottie2d,
player::events::{LottieOnAfterEvent, LottieOnCompletedEvent, LottieOnShowEvent},
},
prelude::VelloLottieAnchor,
};
use bevy::{
camera::primitives::Aabb,
platform::time::Instant,
prelude::*,
ui::{ContentSize, NodeMeasure},
};
use tracing::debug;
#[inline(always)]
fn prev_f64(x: f64) -> f64 {
let u = x.to_bits();
let new_u = if u == 0x0000_0000_0000_0000 {
0x8000_0000_0000_0000 } else if u == 0xFFF0_0000_0000_0000 {
u } else if x <= -0.0 {
u + 1
} else {
u - 1
};
f64::from_bits(new_u)
}
pub fn advance_playheads<A: LottieAssetVariant>(
mut lotties: Query<(&A, &mut Playhead, &mut LottiePlayer<A>, &PlaybackOptions)>,
mut assets: ResMut<Assets<VelloLottie>>,
time: Res<Time>,
) {
let all_lotties = lotties.iter_mut();
for (asset, mut playhead, mut player, options) in all_lotties {
let Some(asset) = assets.get_mut(asset.asset_id()) else {
continue;
};
let start_frame = options.segments.start.max(asset.composition.frames.start);
let end_frame = prev_f64(options.segments.end.min(asset.composition.frames.end));
playhead.frame = playhead.frame.clamp(start_frame, end_frame);
if player.stopped {
continue;
}
playhead.first_render.get_or_insert(Instant::now());
if !player.started && options.autoplay {
player.started = true;
player.playing = true;
}
if !player.playing {
continue;
}
if let Some(intermission) = playhead.intermission.as_mut() {
intermission.tick(time.delta());
if intermission.is_finished() {
playhead.intermission.take();
match options.direction {
PlaybackDirection::Normal => {
playhead.frame = start_frame;
}
PlaybackDirection::Reverse => {
playhead.frame = end_frame;
}
}
}
continue;
}
let length = end_frame - start_frame;
playhead.frame += (time.delta_secs_f64()
* options.speed
* asset.composition.frame_rate
* (options.direction as i32 as f64)
* playhead.playmode_dir)
% length;
let looping = match options.looping {
PlaybackLoopBehavior::Loop => true,
PlaybackLoopBehavior::Amount(amt) => playhead.loops_completed < amt,
PlaybackLoopBehavior::DoNotLoop => false,
};
if playhead.frame > end_frame {
if looping {
playhead.loops_completed += 1;
if let PlaybackPlayMode::Bounce = options.play_mode {
playhead.playmode_dir *= -1.0;
}
if options.intermission > Duration::ZERO {
playhead
.intermission
.replace(Timer::new(options.intermission, TimerMode::Once));
playhead.frame = end_frame;
} else {
playhead.frame = start_frame + (playhead.frame - end_frame);
}
} else {
playhead.frame = end_frame;
}
if let PlaybackPlayMode::Bounce = options.play_mode {
playhead.frame = end_frame;
}
} else if playhead.frame < start_frame {
if looping {
playhead.loops_completed += 1;
if let PlaybackPlayMode::Bounce = options.play_mode {
playhead.playmode_dir *= -1.0;
}
if options.intermission > Duration::ZERO {
playhead
.intermission
.replace(Timer::new(options.intermission, TimerMode::Once));
playhead.frame = start_frame;
} else {
playhead.frame = end_frame - (start_frame - playhead.frame);
}
} else {
playhead.frame = start_frame;
}
if let PlaybackPlayMode::Bounce = options.play_mode {
playhead.frame = start_frame;
}
}
}
}
pub fn run_time_transitions<A: LottieAssetVariant>(
mut commands: Commands,
query_player: Query<(Entity, &LottiePlayer<A>, &Playhead, &PlaybackOptions, &A)>,
mut assets: ResMut<Assets<VelloLottie>>,
) {
for (entity, player, playhead, options, lottie) in query_player.iter() {
let Some(current_asset) = assets.get_mut(lottie.asset_id()) else {
continue;
};
for transition in player.state().transitions.iter() {
match transition {
PlayerTransition::OnMouseEnter { .. }
| PlayerTransition::OnMouseClick { .. }
| PlayerTransition::OnMouseLeave { .. } => {
}
PlayerTransition::OnAfter { state, secs } => {
let started = playhead.first_render;
if started.is_some_and(|s| s.elapsed().as_secs_f32() >= *secs) {
commands
.entity(entity)
.trigger(|entity| LottieOnAfterEvent {
entity,
next_state: state,
});
}
}
PlayerTransition::OnComplete { state } => {
let loops_needed = match options.looping {
PlaybackLoopBehavior::DoNotLoop => Some(0),
PlaybackLoopBehavior::Amount(amt) => Some(amt),
PlaybackLoopBehavior::Loop => Some(0),
};
match options.direction {
PlaybackDirection::Normal => {
let end_frame = prev_f64(
options
.segments
.end
.min(current_asset.composition.frames.end),
);
if playhead.frame == end_frame
&& loops_needed
.is_some_and(|needed| playhead.loops_completed >= needed)
{
commands
.entity(entity)
.trigger(|entity| LottieOnCompletedEvent {
entity,
next_state: state,
});
}
}
PlaybackDirection::Reverse => {
let start_frame = options
.segments
.start
.max(current_asset.composition.frames.start);
if playhead.frame == start_frame
&& loops_needed
.is_some_and(|needed| playhead.loops_completed >= needed)
{
commands
.entity(entity)
.trigger(|entity| LottieOnCompletedEvent {
entity,
next_state: state,
});
}
}
}
}
PlayerTransition::OnShow { state } => {
if playhead.first_render.is_some() {
commands.entity(entity).trigger(|entity| LottieOnShowEvent {
entity,
next_state: state,
});
}
}
}
}
}
}
pub fn transition_state<A: LottieAssetVariant>(
mut commands: Commands,
mut query_sm: Query<(Entity, &mut LottiePlayer<A>, &mut Playhead)>,
assets: Res<Assets<VelloLottie>>,
) {
for (entity, mut player, mut playhead) in query_sm.iter_mut() {
let Some(next_state) = player.next_state else {
continue;
};
if Some(next_state) == player.current_state {
player.next_state.take();
continue;
}
debug!("animation controller transitioning to={next_state}");
let target_state = player
.states
.get(&next_state)
.unwrap_or_else(|| panic!("state not found: '{next_state}'"));
let target_options = target_state
.options
.as_ref()
.or(player.state().options.as_ref())
.cloned()
.unwrap_or_default();
if let Some(target_handle) = target_state.asset.as_ref() {
commands.entity(entity).insert(target_handle.clone());
}
let reset_playhead =
player.state().reset_playhead_on_exit || target_state.reset_playhead_on_start;
if reset_playhead {
let target_asset = target_state.asset.as_ref();
if let Some(target_asset) = target_asset {
let Some(asset) = assets.get(target_asset.asset_id()) else {
tracing::warn!("not ready for state transition, re-queueing {next_state}...");
player.next_state = Some(next_state);
continue;
};
let frame = match target_options.direction {
PlaybackDirection::Normal => target_options
.segments
.start
.max(asset.composition.frames.start),
PlaybackDirection::Reverse => prev_f64(
target_options
.segments
.end
.min(asset.composition.frames.end),
),
};
playhead.seek(frame);
}
}
if let Some(theme) = target_state.theme.as_ref() {
commands.entity(entity).insert(theme.clone());
}
if target_state.options.is_some() {
commands.entity(entity).insert(target_options);
}
playhead.intermission.take();
playhead.loops_completed = 0;
playhead.first_render.take();
playhead.playmode_dir = 1.0;
player.started = false;
player.playing = false;
player.current_state.replace(next_state);
}
}
fn helper_calculate_aabb(lottie: &VelloLottie, anchor: &VelloLottieAnchor) -> Aabb {
let (width, height) = (
lottie.composition.width as f32,
lottie.composition.height as f32,
);
let half_size = Vec3::new(width / 2.0, height / 2.0, 0.0);
let (dx, dy) = {
match anchor {
VelloLottieAnchor::TopLeft => (half_size.x, -half_size.y),
VelloLottieAnchor::Left => (half_size.x, 0.0),
VelloLottieAnchor::BottomLeft => (half_size.x, half_size.y),
VelloLottieAnchor::Top => (0.0, -half_size.y),
VelloLottieAnchor::Center => (0.0, 0.0),
VelloLottieAnchor::Bottom => (0.0, half_size.y),
VelloLottieAnchor::TopRight => (-half_size.x, -half_size.y),
VelloLottieAnchor::Right => (-half_size.x, 0.0),
VelloLottieAnchor::BottomRight => (-half_size.x, half_size.y),
}
};
let adjustment = Vec3::new(dx, dy, 0.0);
let min = -half_size + adjustment;
let max = half_size + adjustment;
Aabb::from_min_max(min, max)
}
pub fn update_lottie_2d_aabb_on_asset_load(
mut asset_events: MessageReader<AssetEvent<VelloLottie>>,
mut world_lotties: Query<(&mut Aabb, &mut VelloLottie2d, &VelloLottieAnchor)>,
lotties: Res<Assets<VelloLottie>>,
) {
for event in asset_events.read() {
let id = if let AssetEvent::LoadedWithDependencies { id } = event {
*id
} else {
continue;
};
let Some(lottie) = lotties.get(id) else {
continue;
};
for (mut aabb, _, anchor) in world_lotties
.iter_mut()
.filter(|(_, lottie, _)| lottie.id() == id)
{
let new_aabb = helper_calculate_aabb(lottie, anchor);
*aabb = new_aabb;
}
}
}
pub fn update_lottie_2d_aabb_on_change(
mut world_lotties: Query<
(&mut Aabb, &mut VelloLottie2d, &VelloLottieAnchor),
Changed<VelloLottie2d>,
>,
lotties: Res<Assets<VelloLottie>>,
) {
for (mut aabb, lottie, anchor) in world_lotties.iter_mut() {
let Some(lottie) = lotties.get(&lottie.0) else {
continue;
};
let new_aabb = helper_calculate_aabb(lottie, anchor);
*aabb = new_aabb;
}
}
pub fn update_ui_lottie_content_size_on_change(
mut query: Query<
(&mut ContentSize, &ComputedNode, &mut UiVelloLottie),
Or<(Changed<UiVelloLottie>, Changed<ComputedNode>)>,
>,
lotties: Res<Assets<VelloLottie>>,
) {
for (mut content_size, node, lottie) in query.iter_mut() {
let Some(lottie) = lotties.get(&lottie.0) else {
continue;
};
let (width, height) = (
lottie.composition.width as f32,
lottie.composition.height as f32,
);
let size = Vec2::new(width, height) / node.inverse_scale_factor();
let measure = NodeMeasure::Fixed(bevy::ui::FixedMeasure { size });
content_size.set(measure);
}
}