use std::collections::VecDeque;
use std::time::Duration;
use bevy::math::curve::Curve;
use bevy::math::curve::easing::EaseFunction;
use bevy::prelude::*;
use bevy_kana::Displacement;
use bevy_kana::Position;
use super::components::AnimationSourceMarker;
use super::components::CameraInputInterruptBehavior;
use super::components::ZoomAnimationMarker;
use super::events::AnimationCancelled;
use super::events::AnimationEnd;
use super::events::AnimationSource;
use super::events::CameraMoveBegin;
use super::events::CameraMoveEnd;
use super::events::ZoomCancelled;
use super::events::ZoomEnd;
use crate::ForceUpdate;
use crate::OrbitCam;
#[derive(Clone, Reflect)]
pub enum CameraMove {
ToPosition {
translation: Vec3,
focus: Vec3,
duration: Duration,
easing: EaseFunction,
},
ToOrbit {
focus: Vec3,
yaw: f32,
pitch: f32,
radius: f32,
duration: Duration,
easing: EaseFunction,
},
}
impl CameraMove {
#[must_use]
pub const fn duration(&self) -> Duration {
match self {
Self::ToPosition { duration, .. } | Self::ToOrbit { duration, .. } => *duration,
}
}
#[must_use]
pub fn duration_ms(&self) -> f32 { self.duration().as_secs_f32() * 1000.0 }
#[must_use]
pub const fn easing(&self) -> EaseFunction {
match self {
Self::ToPosition { easing, .. } | Self::ToOrbit { easing, .. } => *easing,
}
}
#[must_use]
pub const fn focus(&self) -> Vec3 {
match self {
Self::ToPosition { focus, .. } | Self::ToOrbit { focus, .. } => *focus,
}
}
#[must_use]
pub fn translation(&self) -> Vec3 {
match self {
Self::ToPosition { translation, .. } => *translation,
Self::ToOrbit {
focus,
yaw,
pitch,
radius,
..
} => {
let yaw_rot = Quat::from_axis_angle(Vec3::Y, *yaw);
let pitch_rot = Quat::from_axis_angle(Vec3::X, -*pitch);
let rotation = yaw_rot * pitch_rot;
*focus + rotation * Vec3::new(0.0, 0.0, *radius)
},
}
}
fn orbital_params(&self) -> (f32, f32, f32) {
match self {
Self::ToPosition {
translation, focus, ..
} => orbital_params_from_offset(Displacement(*translation - *focus)),
Self::ToOrbit {
yaw, pitch, radius, ..
} => (*yaw, *pitch, *radius),
}
}
}
pub(crate) fn orbital_params_from_offset(offset: Displacement) -> (f32, f32, f32) {
let radius = offset.length();
let yaw = offset.x.atan2(offset.z);
let horizontal_dist = offset.x.hypot(offset.z);
let pitch = offset.y.atan2(horizontal_dist);
(yaw, pitch, radius)
}
use crate::constants::EXTERNAL_INPUT_TOLERANCE;
#[derive(Clone, Reflect, Default, Debug)]
enum MoveState {
InProgress {
elapsed_ms: f32,
start_focus: Position,
start_pitch: f32,
start_radius: f32,
start_yaw: f32,
last_written_focus: Position,
last_written_yaw: f32,
last_written_pitch: f32,
last_written_radius: f32,
},
#[default]
Ready,
}
impl MoveState {
fn externally_modified(&self, camera: &OrbitCam) -> bool {
match self {
Self::InProgress {
last_written_focus,
last_written_yaw,
last_written_pitch,
last_written_radius,
..
} => {
let focus_changed =
last_written_focus.distance(camera.target_focus) > EXTERNAL_INPUT_TOLERANCE;
let yaw_changed =
(last_written_yaw - camera.target_yaw).abs() > EXTERNAL_INPUT_TOLERANCE;
let pitch_changed =
(last_written_pitch - camera.target_pitch).abs() > EXTERNAL_INPUT_TOLERANCE;
let radius_changed =
(last_written_radius - camera.target_radius).abs() > EXTERNAL_INPUT_TOLERANCE;
focus_changed || yaw_changed || pitch_changed || radius_changed
},
Self::Ready => false,
}
}
}
#[derive(Component, Reflect, Default)]
#[require(CameraInputInterruptBehavior)]
#[reflect(Component, Default)]
pub struct CameraMoveList {
pub camera_moves: VecDeque<CameraMove>,
state: MoveState,
}
impl CameraMoveList {
#[must_use]
pub const fn new(camera_moves: VecDeque<CameraMove>) -> Self {
Self {
camera_moves,
state: MoveState::Ready,
}
}
pub fn remaining_time_ms(&self) -> f32 {
let current_remaining = match &self.state {
MoveState::InProgress { elapsed_ms, .. } => {
self.camera_moves.front().map_or(0.0, |current_move| {
(current_move.duration_ms() - elapsed_ms).max(0.0)
})
},
MoveState::Ready => self
.camera_moves
.front()
.map_or(0.0, CameraMove::duration_ms),
};
let remaining_queue: f32 = self
.camera_moves
.iter()
.skip(1)
.map(CameraMove::duration_ms)
.sum();
current_remaining + remaining_queue
}
}
fn handle_empty_queue(
commands: &mut Commands,
entity: Entity,
source: AnimationSource,
zoom_marker: Option<&ZoomAnimationMarker>,
) {
commands
.entity(entity)
.remove::<(CameraMoveList, AnimationSourceMarker)>();
commands.trigger(AnimationEnd {
camera: entity,
source,
});
if let Some(marker) = zoom_marker {
commands.entity(entity).remove::<ZoomAnimationMarker>();
commands.trigger(ZoomEnd {
camera: entity,
target: marker.0.target,
margin: marker.0.margin,
duration: marker.0.duration,
easing: marker.0.easing,
});
}
}
fn handle_camera_input_interrupt(
commands: &mut Commands,
entity: Entity,
pan_orbit: &mut OrbitCam,
queue: &CameraMoveList,
interrupt_behavior: CameraInputInterruptBehavior,
source: AnimationSource,
current_move: &CameraMove,
zoom_marker: Option<&ZoomAnimationMarker>,
) -> CameraInputInterruptBehavior {
match interrupt_behavior {
CameraInputInterruptBehavior::Ignore => CameraInputInterruptBehavior::Ignore,
CameraInputInterruptBehavior::Cancel => {
commands
.entity(entity)
.remove::<(CameraMoveList, AnimationSourceMarker)>();
commands.trigger(AnimationCancelled {
camera: entity,
source,
camera_move: current_move.clone(),
});
if let Some(marker) = zoom_marker {
commands.entity(entity).remove::<ZoomAnimationMarker>();
commands.trigger(ZoomCancelled {
camera: entity,
target: marker.0.target,
margin: marker.0.margin,
duration: marker.0.duration,
easing: marker.0.easing,
});
}
CameraInputInterruptBehavior::Cancel
},
CameraInputInterruptBehavior::Complete => {
if let Some(final_move) = queue.camera_moves.back() {
let (yaw, pitch, radius) = final_move.orbital_params();
pan_orbit.target_focus = final_move.focus();
pan_orbit.target_yaw = yaw;
pan_orbit.target_pitch = pitch;
pan_orbit.target_radius = radius;
pan_orbit.force_update = ForceUpdate::Pending;
}
commands
.entity(entity)
.remove::<(CameraMoveList, AnimationSourceMarker)>();
commands.trigger(AnimationEnd {
camera: entity,
source,
});
if let Some(marker) = zoom_marker {
commands.entity(entity).remove::<ZoomAnimationMarker>();
commands.trigger(ZoomEnd {
camera: entity,
target: marker.0.target,
margin: marker.0.margin,
duration: marker.0.duration,
easing: marker.0.easing,
});
}
CameraInputInterruptBehavior::Complete
},
}
}
fn handle_ready_state(
commands: &mut Commands,
entity: Entity,
pan_orbit: &mut OrbitCam,
queue: &mut CameraMoveList,
current_move: &CameraMove,
) -> bool {
if current_move.duration().is_zero() {
commands.trigger(CameraMoveBegin {
camera: entity,
camera_move: current_move.clone(),
});
let (target_yaw, target_pitch, target_radius) = current_move.orbital_params();
pan_orbit.target_focus = current_move.focus();
pan_orbit.target_radius = target_radius;
pan_orbit.target_yaw = target_yaw;
pan_orbit.target_pitch = target_pitch;
pan_orbit.force_update = ForceUpdate::Pending;
commands.trigger(CameraMoveEnd {
camera: entity,
camera_move: current_move.clone(),
});
queue.camera_moves.pop_front();
return true;
}
queue.state = MoveState::InProgress {
elapsed_ms: 0.0,
start_focus: Position(pan_orbit.target_focus),
start_radius: pan_orbit.target_radius,
start_yaw: pan_orbit.target_yaw,
start_pitch: pan_orbit.target_pitch,
last_written_focus: Position(pan_orbit.target_focus),
last_written_yaw: pan_orbit.target_yaw,
last_written_pitch: pan_orbit.target_pitch,
last_written_radius: pan_orbit.target_radius,
};
commands.trigger(CameraMoveBegin {
camera: entity,
camera_move: current_move.clone(),
});
false
}
fn handle_in_progress(
commands: &mut Commands,
entity: Entity,
pan_orbit: &mut OrbitCam,
queue: &mut CameraMoveList,
current_move: &CameraMove,
delta_secs: f32,
) {
let MoveState::InProgress {
elapsed_ms,
start_focus,
start_radius,
start_yaw,
start_pitch,
last_written_focus,
last_written_yaw,
last_written_pitch,
last_written_radius,
} = &mut queue.state
else {
return;
};
*elapsed_ms += delta_secs * 1000.0;
let duration_ms = current_move.duration_ms();
let t = if duration_ms <= 0.0 {
1.0
} else {
(*elapsed_ms / duration_ms).min(1.0)
};
let is_final_frame = t >= 1.0;
let (canonical_yaw, canonical_pitch, canonical_radius) = current_move.orbital_params();
let t_interp = current_move.easing().sample_unchecked(t);
let mut yaw_diff = canonical_yaw - *start_yaw;
yaw_diff = std::f32::consts::TAU.mul_add(
-((yaw_diff + std::f32::consts::PI) / std::f32::consts::TAU).floor(),
yaw_diff,
);
let mut pitch_target = canonical_pitch;
let pitch_diff_raw = pitch_target - *start_pitch;
if pitch_diff_raw > std::f32::consts::PI {
pitch_target -= std::f32::consts::TAU;
} else if pitch_diff_raw < -std::f32::consts::PI {
pitch_target += std::f32::consts::TAU;
}
let pitch_diff = pitch_target - *start_pitch;
pan_orbit.target_focus = Vec3::lerp(**start_focus, current_move.focus(), t_interp);
pan_orbit.target_radius = (canonical_radius - *start_radius).mul_add(t_interp, *start_radius);
pan_orbit.target_yaw = yaw_diff.mul_add(t_interp, *start_yaw);
pan_orbit.target_pitch = pitch_diff.mul_add(t_interp, *start_pitch);
pan_orbit.force_update = ForceUpdate::Pending;
*last_written_focus = Position(pan_orbit.target_focus);
*last_written_yaw = pan_orbit.target_yaw;
*last_written_pitch = pan_orbit.target_pitch;
*last_written_radius = pan_orbit.target_radius;
if is_final_frame {
commands.trigger(CameraMoveEnd {
camera: entity,
camera_move: current_move.clone(),
});
queue.camera_moves.pop_front();
queue.state = MoveState::Ready;
}
}
pub(crate) fn process_camera_move_list(
mut commands: Commands,
time: Res<Time>,
mut camera_query: Query<(
Entity,
&mut OrbitCam,
&mut CameraMoveList,
&CameraInputInterruptBehavior,
Option<&ZoomAnimationMarker>,
Option<&AnimationSourceMarker>,
)>,
) {
for (entity, mut pan_orbit, mut queue, interrupt_behavior, zoom_marker, source_marker) in
&mut camera_query
{
let source = source_marker.map_or(AnimationSource::PlayAnimation, |m| m.0);
let Some(current_move) = queue.camera_moves.front().cloned() else {
handle_empty_queue(&mut commands, entity, source, zoom_marker);
continue;
};
if queue.state.externally_modified(&pan_orbit) {
let outcome = handle_camera_input_interrupt(
&mut commands,
entity,
&mut pan_orbit,
&queue,
*interrupt_behavior,
source,
¤t_move,
zoom_marker,
);
match outcome {
CameraInputInterruptBehavior::Ignore => {},
CameraInputInterruptBehavior::Cancel | CameraInputInterruptBehavior::Complete => {
continue;
},
}
}
match &queue.state {
MoveState::Ready => {
handle_ready_state(
&mut commands,
entity,
&mut pan_orbit,
&mut queue,
¤t_move,
);
},
MoveState::InProgress { .. } => {
handle_in_progress(
&mut commands,
entity,
&mut pan_orbit,
&mut queue,
¤t_move,
time.delta_secs(),
);
},
}
}
}