use crate::camera::camera::{Camera, Projection};
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum Easing {
Linear,
EaseOutCubic,
EaseInOutCubic,
}
impl Easing {
pub fn eval(self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match self {
Self::Linear => t,
Self::EaseOutCubic => {
let inv = 1.0 - t;
1.0 - inv * inv * inv
}
Self::EaseInOutCubic => {
if t < 0.5 {
4.0 * t * t * t
} else {
let p = -2.0 * t + 2.0;
1.0 - p * p * p / 2.0
}
}
}
}
}
#[derive(Clone, Debug)]
pub struct CameraDamping {
pub orbit: f32,
pub pan: f32,
pub zoom: f32,
pub epsilon: f32,
}
impl Default for CameraDamping {
fn default() -> Self {
Self {
orbit: 0.85,
pan: 0.85,
zoom: 0.85,
epsilon: 0.0001,
}
}
}
#[derive(Clone, Debug)]
struct CameraFlight {
start_center: glam::Vec3,
start_distance: f32,
start_orientation: glam::Quat,
#[allow(dead_code)]
start_projection: Projection,
target_center: glam::Vec3,
target_distance: f32,
target_orientation: glam::Quat,
target_projection: Option<Projection>,
duration: f32,
elapsed: f32,
easing: Easing,
}
pub struct CameraAnimator {
damping: CameraDamping,
orbit_velocity: glam::Vec2,
pan_velocity: glam::Vec2,
zoom_velocity: f32,
flight: Option<CameraFlight>,
}
impl CameraAnimator {
pub fn new(damping: CameraDamping) -> Self {
Self {
damping,
orbit_velocity: glam::Vec2::ZERO,
pan_velocity: glam::Vec2::ZERO,
zoom_velocity: 0.0,
flight: None,
}
}
pub fn with_default_damping() -> Self {
Self::new(CameraDamping::default())
}
pub fn apply_orbit(&mut self, yaw_delta: f32, pitch_delta: f32) {
if self.flight.is_some() {
self.flight = None;
}
self.orbit_velocity += glam::Vec2::new(yaw_delta, pitch_delta);
}
pub fn apply_pan(&mut self, right_delta: f32, up_delta: f32) {
if self.flight.is_some() {
self.flight = None;
}
self.pan_velocity += glam::Vec2::new(right_delta, up_delta);
}
pub fn apply_zoom(&mut self, delta: f32) {
if self.flight.is_some() {
self.flight = None;
}
self.zoom_velocity += delta;
}
pub fn fly_to(
&mut self,
camera: &Camera,
target_center: glam::Vec3,
target_distance: f32,
target_orientation: glam::Quat,
duration: f32,
) {
self.fly_to_full(
camera,
target_center,
target_distance,
target_orientation,
None,
duration,
Easing::EaseOutCubic,
);
}
pub fn fly_to_with_easing(
&mut self,
camera: &Camera,
target_center: glam::Vec3,
target_distance: f32,
target_orientation: glam::Quat,
duration: f32,
easing: Easing,
) {
self.fly_to_full(
camera,
target_center,
target_distance,
target_orientation,
None,
duration,
easing,
);
}
#[allow(clippy::too_many_arguments)]
pub fn fly_to_full(
&mut self,
camera: &Camera,
target_center: glam::Vec3,
target_distance: f32,
target_orientation: glam::Quat,
target_projection: Option<Projection>,
duration: f32,
easing: Easing,
) {
self.orbit_velocity = glam::Vec2::ZERO;
self.pan_velocity = glam::Vec2::ZERO;
self.zoom_velocity = 0.0;
self.flight = Some(CameraFlight {
start_center: camera.center,
start_distance: camera.distance,
start_orientation: camera.orientation,
start_projection: camera.projection,
target_center,
target_distance,
target_orientation,
target_projection,
duration: duration.max(0.001),
elapsed: 0.0,
easing,
});
}
pub fn cancel_flight(&mut self) {
self.flight = None;
}
pub fn is_animating(&self) -> bool {
if self.flight.is_some() {
return true;
}
let eps = self.damping.epsilon;
self.orbit_velocity.length() > eps
|| self.pan_velocity.length() > eps
|| self.zoom_velocity.abs() > eps
}
pub fn update(&mut self, dt: f32, camera: &mut Camera) -> bool {
if let Some(ref mut flight) = self.flight {
flight.elapsed += dt;
let raw_t = (flight.elapsed / flight.duration).min(1.0);
let t = flight.easing.eval(raw_t);
camera.center = flight.start_center.lerp(flight.target_center, t);
camera.distance =
flight.start_distance + (flight.target_distance - flight.start_distance) * t;
camera.orientation = flight.start_orientation.slerp(flight.target_orientation, t);
if raw_t >= 1.0 {
if let Some(proj) = flight.target_projection {
camera.projection = proj;
}
self.flight = None;
}
return true;
}
let eps = self.damping.epsilon;
let mut changed = false;
if self.orbit_velocity.length() > eps {
let yaw = self.orbit_velocity.x;
let pitch = self.orbit_velocity.y;
let yaw_rot = glam::Quat::from_rotation_y(-yaw);
let pitch_rot = glam::Quat::from_rotation_x(-pitch);
camera.orientation = (yaw_rot * camera.orientation * pitch_rot).normalize();
self.orbit_velocity *= self.damping.orbit.powf(dt * 60.0);
if self.orbit_velocity.length() <= eps {
self.orbit_velocity = glam::Vec2::ZERO;
}
changed = true;
}
if self.pan_velocity.length() > eps {
let right = camera.right();
let up = camera.up();
camera.center -= right * self.pan_velocity.x + up * self.pan_velocity.y;
self.pan_velocity *= self.damping.pan.powf(dt * 60.0);
if self.pan_velocity.length() <= eps {
self.pan_velocity = glam::Vec2::ZERO;
}
changed = true;
}
if self.zoom_velocity.abs() > eps {
camera.distance = (camera.distance + self.zoom_velocity).max(0.01);
self.zoom_velocity *= self.damping.zoom.powf(dt * 60.0);
if self.zoom_velocity.abs() <= eps {
self.zoom_velocity = 0.0;
}
changed = true;
}
changed
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_camera() -> Camera {
Camera::default()
}
#[test]
fn test_damping_decays_velocity() {
let mut anim = CameraAnimator::with_default_damping();
let mut cam = default_camera();
anim.apply_orbit(0.5, 0.3);
assert!(anim.is_animating());
for _ in 0..300 {
anim.update(1.0 / 60.0, &mut cam);
}
assert!(!anim.is_animating(), "should have settled after 300 frames");
}
#[test]
fn test_zero_damping_passes_through() {
let damping = CameraDamping {
orbit: 0.0,
pan: 0.0,
zoom: 0.0,
epsilon: 0.0001,
};
let mut anim = CameraAnimator::new(damping);
let mut cam = default_camera();
let orig_orientation = cam.orientation;
anim.apply_orbit(0.1, 0.0);
anim.update(1.0 / 60.0, &mut cam);
assert!(
!anim.is_animating(),
"zero damping should settle in one frame"
);
assert!(
(cam.orientation.x - orig_orientation.x).abs() > 1e-6
|| (cam.orientation.y - orig_orientation.y).abs() > 1e-6,
"camera orientation should have changed"
);
}
#[test]
fn test_fly_to_reaches_target() {
let mut anim = CameraAnimator::with_default_damping();
let mut cam = default_camera();
let target_center = glam::Vec3::new(10.0, 20.0, 30.0);
let target_dist = 15.0;
let target_orient = glam::Quat::from_rotation_y(std::f32::consts::PI);
anim.fly_to(&cam, target_center, target_dist, target_orient, 0.5);
for _ in 0..120 {
anim.update(1.0 / 60.0, &mut cam);
}
assert!(
(cam.center - target_center).length() < 1e-4,
"center should match target: {:?}",
cam.center
);
assert!(
(cam.distance - target_dist).abs() < 1e-4,
"distance should match target: {}",
cam.distance
);
}
#[test]
fn test_fly_to_cancelled_by_input() {
let mut anim = CameraAnimator::with_default_damping();
let mut cam = default_camera();
let target = glam::Vec3::new(100.0, 0.0, 0.0);
anim.fly_to(&cam, target, 50.0, glam::Quat::IDENTITY, 1.0);
for _ in 0..5 {
anim.update(1.0 / 60.0, &mut cam);
}
anim.apply_orbit(0.1, 0.0);
anim.update(1.0 / 60.0, &mut cam);
assert!(
(cam.center - target).length() > 1.0,
"flight should have been cancelled"
);
}
#[test]
fn test_is_animating_reflects_state() {
let mut anim = CameraAnimator::with_default_damping();
let mut cam = default_camera();
assert!(!anim.is_animating());
anim.apply_zoom(1.0);
assert!(anim.is_animating());
for _ in 0..300 {
anim.update(1.0 / 60.0, &mut cam);
}
assert!(!anim.is_animating());
}
#[test]
fn test_easing_boundaries() {
for easing in [Easing::Linear, Easing::EaseOutCubic, Easing::EaseInOutCubic] {
let v0 = easing.eval(0.0);
let v1 = easing.eval(1.0);
assert!(v0.abs() < 1e-6, "{easing:?}: eval(0) = {v0}, expected 0");
assert!(
(v1 - 1.0).abs() < 1e-6,
"{easing:?}: eval(1) = {v1}, expected 1"
);
}
}
}