use glam::{Vec3, Vec4, Quat, Mat4};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum EasingCurve {
Linear,
EaseIn,
EaseOut,
EaseInOut,
Step,
Spring { stiffness: f32, damping: f32 },
Custom(Vec<[f32; 2]>),
}
pub fn easing_evaluate(curve: &EasingCurve, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match curve {
EasingCurve::Linear => t,
EasingCurve::EaseIn => t * t,
EasingCurve::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
EasingCurve::EaseInOut => {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
}
}
EasingCurve::Step => {
if t < 0.5 { 0.0 } else { 1.0 }
}
EasingCurve::Spring { stiffness, damping } => {
let omega = stiffness.max(0.01).sqrt();
let zeta = damping / (2.0 * omega);
if zeta >= 1.0 {
let r1 = -omega * (zeta - (zeta * zeta - 1.0).max(0.0).sqrt());
let r2 = -omega * (zeta + (zeta * zeta - 1.0).max(0.0).sqrt());
let c2 = (r1) / (r1 - r2);
let c1 = 1.0 - c2;
1.0 - (c1 * (r1 * t).exp() + c2 * (r2 * t).exp())
} else {
let wd = omega * (1.0 - zeta * zeta).max(0.0).sqrt();
let decay = (-zeta * omega * t).exp();
1.0 - decay * (
(wd * t).cos() + (zeta * omega / wd.max(f32::EPSILON)) * (wd * t).sin()
)
}
}
EasingCurve::Custom(points) => {
if points.is_empty() { return t; }
if points.len() == 1 { return points[0][1]; }
let mut lo = [0.0f32, 0.0f32];
let mut hi = [1.0f32, 1.0f32];
for &pt in points.iter() {
if pt[0] <= t { lo = pt; }
else { hi = pt; break; }
}
let range = hi[0] - lo[0];
if range < f32::EPSILON { return lo[1]; }
let local_t = (t - lo[0]) / range;
lo[1] + local_t * (hi[1] - lo[1])
}
}
}
#[derive(Debug, Clone)]
pub enum ShotTransition {
Cut,
Dissolve { duration: f32 },
WipeLeft { duration: f32 },
ZoomBlur { duration: f32 },
FadeThrough { duration: f32 },
}
impl ShotTransition {
pub fn duration(&self) -> f32 {
match self {
ShotTransition::Cut => 0.0,
ShotTransition::Dissolve { duration } => *duration,
ShotTransition::WipeLeft { duration } => *duration,
ShotTransition::ZoomBlur { duration } => *duration,
ShotTransition::FadeThrough { duration } => *duration,
}
}
pub fn is_cut(&self) -> bool { matches!(self, ShotTransition::Cut) }
}
#[derive(Debug, Clone)]
pub struct CameraShot {
pub start_pos: Vec3,
pub end_pos: Vec3,
pub start_rot: Quat,
pub end_rot: Quat,
pub start_fov: f32,
pub end_fov: f32,
pub near: f32,
pub far: f32,
pub duration: f32,
pub pos_easing: EasingCurve,
pub rot_easing: EasingCurve,
pub fov_easing: EasingCurve,
pub transition: ShotTransition,
pub label: Option<String>,
}
impl CameraShot {
pub fn new(duration: f32) -> Self {
Self {
start_pos: Vec3::ZERO,
end_pos: Vec3::ZERO,
start_rot: Quat::IDENTITY,
end_rot: Quat::IDENTITY,
start_fov: 60.0,
end_fov: 60.0,
near: 0.1,
far: 1000.0,
duration,
pos_easing: EasingCurve::EaseInOut,
rot_easing: EasingCurve::EaseInOut,
fov_easing: EasingCurve::Linear,
transition: ShotTransition::Cut,
label: None,
}
}
pub fn from_to(from: Vec3, to: Vec3, duration: f32) -> Self {
Self { start_pos: from, end_pos: to, ..Self::new(duration) }
}
pub fn static_at(pos: Vec3, rot: Quat, fov: f32, duration: f32) -> Self {
Self {
start_pos: pos, end_pos: pos,
start_rot: rot, end_rot: rot,
start_fov: fov, end_fov: fov,
..Self::new(duration)
}
}
pub fn with_rotation(mut self, from: Quat, to: Quat) -> Self {
self.start_rot = from;
self.end_rot = to;
self
}
pub fn with_fov(mut self, from: f32, to: f32) -> Self {
self.start_fov = from;
self.end_fov = to;
self
}
pub fn with_clip(mut self, near: f32, far: f32) -> Self {
self.near = near;
self.far = far;
self
}
pub fn with_pos_easing(mut self, e: EasingCurve) -> Self { self.pos_easing = e; self }
pub fn with_rot_easing(mut self, e: EasingCurve) -> Self { self.rot_easing = e; self }
pub fn with_fov_easing(mut self, e: EasingCurve) -> Self { self.fov_easing = e; self }
pub fn with_transition(mut self, t: ShotTransition) -> Self { self.transition = t; self }
pub fn with_label(mut self, l: impl Into<String>) -> Self { self.label = Some(l.into()); self }
pub fn evaluate(&self, t: f32) -> CameraFrame {
let pt = easing_evaluate(&self.pos_easing, t);
let rt = easing_evaluate(&self.rot_easing, t);
let ft = easing_evaluate(&self.fov_easing, t);
let pos = self.start_pos + pt * (self.end_pos - self.start_pos);
let rot = self.start_rot.slerp(self.end_rot, rt);
let fov = self.start_fov + ft * (self.end_fov - self.start_fov);
CameraFrame { pos, rot, fov, near: self.near, far: self.far }
}
}
#[derive(Debug, Clone, Copy)]
pub struct CameraFrame {
pub pos: Vec3,
pub rot: Quat,
pub fov: f32,
pub near: f32,
pub far: f32,
}
impl CameraFrame {
pub fn identity() -> Self {
Self { pos: Vec3::ZERO, rot: Quat::IDENTITY, fov: 60.0, near: 0.1, far: 1000.0 }
}
pub fn view_matrix(&self) -> Mat4 {
let forward = self.rot * Vec3::NEG_Z;
let up = self.rot * Vec3::Y;
Mat4::look_at_rh(self.pos, self.pos + forward, up)
}
pub fn projection_matrix(&self, aspect: f32) -> Mat4 {
let fov_rad = self.fov.to_radians();
Mat4::perspective_rh(fov_rad, aspect, self.near, self.far)
}
pub fn blend(&self, other: &CameraFrame, t: f32) -> CameraFrame {
let t = t.clamp(0.0, 1.0);
CameraFrame {
pos: self.pos + t * (other.pos - self.pos),
rot: self.rot.slerp(other.rot, t),
fov: self.fov + t * (other.fov - self.fov),
near: self.near + t * (other.near - self.near),
far: self.far + t * (other.far - self.far),
}
}
}
impl Default for CameraFrame {
fn default() -> Self { Self::identity() }
}
#[derive(Debug, Clone)]
pub struct CameraTrack {
pub shots: Vec<CameraShot>,
starts: Vec<f32>,
pub total_duration: f32,
}
impl CameraTrack {
pub fn new() -> Self {
Self { shots: Vec::new(), starts: Vec::new(), total_duration: 0.0 }
}
pub fn push(&mut self, shot: CameraShot) {
self.starts.push(self.total_duration);
self.total_duration += shot.duration;
self.shots.push(shot);
}
pub fn with_shot(mut self, shot: CameraShot) -> Self {
self.push(shot);
self
}
pub fn shot_count(&self) -> usize { self.shots.len() }
pub fn evaluate(&self, t: f32) -> CameraFrame {
if self.shots.is_empty() {
return CameraFrame::identity();
}
let t = t.clamp(0.0, self.total_duration);
let idx = self.shot_index_at(t);
let shot = &self.shots[idx];
let start = self.starts[idx];
let local = (t - start) / shot.duration.max(f32::EPSILON);
let local = local.clamp(0.0, 1.0);
shot.evaluate(local)
}
fn shot_index_at(&self, t: f32) -> usize {
let mut best = 0;
for (i, &s) in self.starts.iter().enumerate() {
if s <= t { best = i; }
else { break; }
}
best
}
pub fn active_shot(&self, t: f32) -> Option<&CameraShot> {
if self.shots.is_empty() { return None; }
Some(&self.shots[self.shot_index_at(t.clamp(0.0, self.total_duration))])
}
pub fn transition_at(&self, t: f32) -> &ShotTransition {
if let Some(shot) = self.active_shot(t) {
&shot.transition
} else {
&ShotTransition::Cut
}
}
pub fn markers(&self) -> Vec<(String, f32)> {
self.shots.iter().zip(self.starts.iter())
.filter_map(|(shot, &start)| {
shot.label.as_ref().map(|l| (l.clone(), start))
})
.collect()
}
}
impl Default for CameraTrack {
fn default() -> Self { Self::new() }
}
#[derive(Debug, Clone)]
pub struct CatmullRomSpline {
pub points: Vec<Vec3>,
arc_table: Vec<(f32, f32, usize)>, pub total_length: f32,
pub closed: bool,
}
impl CatmullRomSpline {
pub fn new(points: Vec<Vec3>) -> Self {
let mut spline = Self {
points,
arc_table: Vec::new(),
total_length: 0.0,
closed: false,
};
spline.build_arc_table();
spline
}
pub fn closed(mut self) -> Self {
self.closed = true;
self.build_arc_table();
self
}
fn num_segments(&self) -> usize {
if self.points.len() < 2 { return 0; }
if self.closed { self.points.len() } else { self.points.len() - 1 }
}
fn get_point(&self, idx: i32) -> Vec3 {
let n = self.points.len() as i32;
if n == 0 { return Vec3::ZERO; }
if self.closed {
self.points[idx.rem_euclid(n) as usize]
} else {
self.points[idx.clamp(0, n - 1) as usize]
}
}
pub fn evaluate_segment(&self, seg: usize, t: f32) -> Vec3 {
let i = seg as i32;
let p0 = self.get_point(i - 1);
let p1 = self.get_point(i);
let p2 = self.get_point(i + 1);
let p3 = self.get_point(i + 2);
let t2 = t * t;
let t3 = t2 * t;
let b0 = -t3 + 2.0 * t2 - t;
let b1 = 3.0 * t3 - 5.0 * t2 + 2.0;
let b2 = -3.0 * t3 + 4.0 * t2 + t;
let b3 = t3 - t2;
(p0 * b0 + p1 * b1 + p2 * b2 + p3 * b3) * 0.5
}
pub fn tangent_segment(&self, seg: usize, t: f32) -> Vec3 {
let i = seg as i32;
let p0 = self.get_point(i - 1);
let p1 = self.get_point(i);
let p2 = self.get_point(i + 1);
let p3 = self.get_point(i + 2);
let t2 = t * t;
let b0 = -3.0 * t2 + 4.0 * t - 1.0;
let b1 = 9.0 * t2 - 10.0 * t;
let b2 = -9.0 * t2 + 8.0 * t + 1.0;
let b3 = 3.0 * t2 - 2.0 * t;
(p0 * b0 + p1 * b1 + p2 * b2 + p3 * b3) * 0.5
}
fn build_arc_table(&mut self) {
self.arc_table.clear();
self.total_length = 0.0;
let segs = self.num_segments();
if segs == 0 { return; }
let steps_per_seg = 32usize;
let mut prev = self.evaluate_segment(0, 0.0);
self.arc_table.push((0.0, 0.0, 0));
for seg in 0..segs {
for step in 1..=steps_per_seg {
let t = step as f32 / steps_per_seg as f32;
let curr = self.evaluate_segment(seg, t);
self.total_length += (curr - prev).length();
prev = curr;
self.arc_table.push((self.total_length, t, seg));
}
}
}
pub fn evaluate_arc(&self, s: f32) -> Vec3 {
if self.arc_table.is_empty() {
return self.points.first().copied().unwrap_or(Vec3::ZERO);
}
let s = s.clamp(0.0, self.total_length);
let idx = self.arc_table.partition_point(|&(arc, _, _)| arc <= s);
if idx == 0 {
return self.evaluate_segment(0, 0.0);
}
if idx >= self.arc_table.len() {
let &(_, t, seg) = self.arc_table.last().unwrap();
return self.evaluate_segment(seg, t);
}
let (arc0, t0, seg0) = self.arc_table[idx - 1];
let (arc1, t1, seg1) = self.arc_table[idx];
let range = arc1 - arc0;
if range < f32::EPSILON {
return self.evaluate_segment(seg0, t0);
}
let alpha = (s - arc0) / range;
if seg0 == seg1 {
let t = t0 + alpha * (t1 - t0);
self.evaluate_segment(seg0, t)
} else {
let a = self.evaluate_segment(seg0, t0);
let b = self.evaluate_segment(seg1, t1);
a + alpha * (b - a)
}
}
pub fn tangent_arc(&self, s: f32) -> Vec3 {
if self.arc_table.is_empty() { return Vec3::Z; }
let s = s.clamp(0.0, self.total_length);
let idx = self.arc_table.partition_point(|&(arc, _, _)| arc <= s);
let idx = idx.clamp(1, self.arc_table.len() - 1);
let (_, t, seg) = self.arc_table[idx];
let tan = self.tangent_segment(seg, t);
if tan.length_squared() > f32::EPSILON { tan.normalize() } else { Vec3::Z }
}
pub fn evaluate_normalized(&self, u: f32) -> Vec3 {
self.evaluate_arc(u * self.total_length)
}
pub fn tangent_normalized(&self, u: f32) -> Vec3 {
self.tangent_arc(u * self.total_length)
}
}
#[derive(Debug, Clone)]
pub struct CameraRig {
pub path: CatmullRomSpline,
pub look_at: Option<Vec3>,
pub up_override: Option<Vec3>,
pub fov: f32,
pub near: f32,
pub far: f32,
pub path_position: f32,
}
impl CameraRig {
pub fn new(path: CatmullRomSpline) -> Self {
Self {
path,
look_at: None,
up_override: None,
fov: 60.0,
near: 0.1,
far: 1000.0,
path_position: 0.0,
}
}
pub fn with_look_at(mut self, target: Vec3) -> Self { self.look_at = Some(target); self }
pub fn with_up(mut self, up: Vec3) -> Self { self.up_override = Some(up); self }
pub fn with_fov(mut self, fov: f32) -> Self { self.fov = fov; self }
pub fn with_clip(mut self, near: f32, far: f32) -> Self { self.near = near; self.far = far; self }
pub fn evaluate(&self, u: f32) -> CameraFrame {
let pos = self.path.evaluate_normalized(u);
let forward = if let Some(target) = self.look_at {
let d = target - pos;
if d.length_squared() > f32::EPSILON { d.normalize() } else { Vec3::NEG_Z }
} else {
let t = self.path.tangent_normalized(u);
if t.length_squared() > f32::EPSILON { t } else { Vec3::NEG_Z }
};
let up = self.up_override.unwrap_or(Vec3::Y);
let right = forward.cross(up);
let right_n = if right.length_squared() > f32::EPSILON { right.normalize() } else { Vec3::X };
let up_cam = right_n.cross(forward).normalize();
let rot = Quat::from_mat4(&Mat4::from_cols(
right_n.extend(0.0),
up_cam.extend(0.0),
(-forward).extend(0.0),
Vec4::W,
));
CameraFrame { pos, rot, fov: self.fov, near: self.near, far: self.far }
}
pub fn advance(&mut self, delta: f32) {
self.path_position = (self.path_position + delta).clamp(0.0, 1.0);
}
pub fn frame(&self) -> CameraFrame {
self.evaluate(self.path_position)
}
}
#[derive(Debug, Clone)]
pub struct SpeedProfile {
entries: Vec<(f32, f32)>,
}
impl SpeedProfile {
pub fn constant(speed: f32) -> Self {
Self { entries: vec![(0.0, speed), (1.0, speed)] }
}
pub fn new() -> Self {
Self { entries: vec![(0.0, 1.0), (1.0, 1.0)] }
}
pub fn add_point(mut self, pos: f32, speed: f32) -> Self {
let idx = self.entries.partition_point(|&(p, _)| p <= pos);
self.entries.insert(idx, (pos.clamp(0.0, 1.0), speed.max(0.0)));
self
}
pub fn evaluate(&self, u: f32) -> f32 {
if self.entries.is_empty() { return 1.0; }
let idx = self.entries.partition_point(|&(p, _)| p <= u);
if idx == 0 { return self.entries[0].1; }
if idx >= self.entries.len() { return self.entries.last().unwrap().1; }
let (p0, s0) = self.entries[idx - 1];
let (p1, s1) = self.entries[idx];
let range = p1 - p0;
if range < f32::EPSILON { return s0; }
let t = (u - p0) / range;
s0 + t * (s1 - s0)
}
}
impl Default for SpeedProfile {
fn default() -> Self { Self::new() }
}
#[derive(Debug, Clone)]
pub struct DollyTrack {
pub waypoints: Vec<Vec3>,
pub total_length: f32,
pub speed_profile: SpeedProfile,
pub look_at: Option<Vec3>,
pub position: f32,
pub fov: f32,
pub near: f32,
pub far: f32,
}
impl DollyTrack {
pub fn new(waypoints: Vec<Vec3>) -> Self {
let length = waypoints.windows(2)
.map(|w| (w[1] - w[0]).length())
.sum::<f32>();
Self {
waypoints,
total_length: length,
speed_profile: SpeedProfile::constant(2.0),
look_at: None,
position: 0.0,
fov: 60.0,
near: 0.1,
far: 1000.0,
}
}
pub fn with_speed_profile(mut self, sp: SpeedProfile) -> Self { self.speed_profile = sp; self }
pub fn with_look_at(mut self, t: Vec3) -> Self { self.look_at = Some(t); self }
pub fn with_fov(mut self, fov: f32) -> Self { self.fov = fov; self }
pub fn tick(&mut self, dt: f32) {
let u = if self.total_length > f32::EPSILON {
self.position / self.total_length
} else {
0.0
};
let speed = self.speed_profile.evaluate(u);
self.position = (self.position + speed * dt).clamp(0.0, self.total_length);
}
pub fn position_at(&self, s: f32) -> Vec3 {
if self.waypoints.is_empty() { return Vec3::ZERO; }
if self.waypoints.len() == 1 { return self.waypoints[0]; }
let s = s.clamp(0.0, self.total_length);
let mut acc = 0.0f32;
for i in 0..self.waypoints.len() - 1 {
let seg_len = (self.waypoints[i + 1] - self.waypoints[i]).length();
if acc + seg_len >= s || i == self.waypoints.len() - 2 {
let local = if seg_len > f32::EPSILON { (s - acc) / seg_len } else { 0.0 };
let local = local.clamp(0.0, 1.0);
return self.waypoints[i] + local * (self.waypoints[i + 1] - self.waypoints[i]);
}
acc += seg_len;
}
*self.waypoints.last().unwrap()
}
pub fn frame(&self) -> CameraFrame {
let pos = self.position_at(self.position);
let forward = if let Some(target) = self.look_at {
let d = target - pos;
if d.length_squared() > f32::EPSILON { d.normalize() } else { Vec3::NEG_Z }
} else {
let next = self.position_at((self.position + 0.01).min(self.total_length));
let d = next - pos;
if d.length_squared() > f32::EPSILON { d.normalize() } else { Vec3::NEG_Z }
};
let up = Vec3::Y;
let right = forward.cross(up);
let right_n = if right.length_squared() > f32::EPSILON { right.normalize() } else { Vec3::X };
let up_cam = right_n.cross(forward).normalize();
let rot = Quat::from_mat4(&Mat4::from_cols(
right_n.extend(0.0),
up_cam.extend(0.0),
(-forward).extend(0.0),
Vec4::W,
));
CameraFrame { pos, rot, fov: self.fov, near: self.near, far: self.far }
}
pub fn normalized_position(&self) -> f32 {
if self.total_length > f32::EPSILON {
self.position / self.total_length
} else {
0.0
}
}
pub fn is_at_end(&self) -> bool {
self.position >= self.total_length - f32::EPSILON
}
}
#[derive(Debug, Clone)]
pub struct VirtualCamera {
pub focus_distance: f32,
pub aperture: f32,
pub sensor_height: f32,
pub focal_length: f32,
pub shutter_angle: f32,
}
impl VirtualCamera {
pub fn new() -> Self {
Self {
focus_distance: 10.0,
aperture: 2.8,
sensor_height: 24.0,
focal_length: 35.0,
shutter_angle: 180.0,
}
}
pub fn cinematic() -> Self {
Self {
focus_distance: 8.0,
aperture: 1.8,
sensor_height: 24.0,
focal_length: 50.0,
shutter_angle: 180.0,
}
}
pub fn coc_radius(&self, depth: f32) -> f32 {
let fl = self.focal_length / 1000.0; let fd = self.focus_distance;
let ap = self.focal_length / self.aperture.max(f32::EPSILON) / 1000.0;
let sensor_m = self.sensor_height / 1000.0;
let num = (fd - depth).abs() * fl * ap;
let den = depth.max(f32::EPSILON) * (fd - fl).abs().max(f32::EPSILON);
(num / den / sensor_m).clamp(0.0, 1.0)
}
pub fn fov_degrees(&self) -> f32 {
2.0 * ((self.sensor_height / (2.0 * self.focal_length.max(f32::EPSILON))).atan()).to_degrees()
}
pub fn shutter_speed(&self, fps: f32) -> f32 {
(self.shutter_angle / 360.0) / fps.max(f32::EPSILON)
}
pub fn with_focus(mut self, d: f32) -> Self { self.focus_distance = d; self }
pub fn with_aperture(mut self, a: f32) -> Self { self.aperture = a; self }
pub fn with_focal_length(mut self, fl: f32) -> Self { self.focal_length = fl; self }
pub fn with_sensor(mut self, h: f32) -> Self { self.sensor_height = h; self }
}
impl Default for VirtualCamera {
fn default() -> Self { Self::new() }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CameraAnimatorMode {
Track,
Rig,
Dolly,
Blend,
}
pub struct CameraAnimator {
pub track: CameraTrack,
pub rig: Option<CameraRig>,
pub dolly: Option<DollyTrack>,
pub virtual_cam: VirtualCamera,
pub mode: CameraAnimatorMode,
pub blend: f32,
pub time: f32,
pub speed: f32,
pub looping: bool,
pub playing: bool,
pub shake_offset: Vec3,
}
impl CameraAnimator {
pub fn new() -> Self {
Self {
track: CameraTrack::new(),
rig: None,
dolly: None,
virtual_cam: VirtualCamera::new(),
mode: CameraAnimatorMode::Track,
blend: 0.0,
time: 0.0,
speed: 1.0,
looping: false,
playing: false,
shake_offset: Vec3::ZERO,
}
}
pub fn with_track(mut self, t: CameraTrack) -> Self { self.track = t; self }
pub fn with_rig(mut self, r: CameraRig) -> Self { self.rig = Some(r); self }
pub fn with_dolly(mut self, d: DollyTrack) -> Self { self.dolly = Some(d); self }
pub fn with_virtual_cam(mut self, v: VirtualCamera) -> Self { self.virtual_cam = v; self }
pub fn with_mode(mut self, m: CameraAnimatorMode) -> Self { self.mode = m; self }
pub fn with_blend(mut self, b: f32) -> Self { self.blend = b.clamp(0.0, 1.0); self }
pub fn with_speed(mut self, s: f32) -> Self { self.speed = s; self }
pub fn with_looping(mut self, l: bool) -> Self { self.looping = l; self }
pub fn play(&mut self) { self.playing = true; }
pub fn pause(&mut self) { self.playing = false; }
pub fn stop(&mut self) { self.playing = false; self.time = 0.0; }
pub fn seek(&mut self, t: f32) { self.time = t.max(0.0); }
pub fn tick(&mut self, dt: f32) -> CameraFrame {
if self.playing {
self.time += dt * self.speed;
let duration = self.duration();
if duration > 0.0 && self.time > duration {
if self.looping {
self.time -= duration;
} else {
self.time = duration;
self.playing = false;
}
}
}
if let Some(ref mut dolly) = self.dolly {
if self.playing {
dolly.tick(dt * self.speed);
}
}
let mut frame = self.evaluate(self.time);
frame.pos = frame.pos + self.shake_offset;
frame
}
pub fn evaluate(&self, t: f32) -> CameraFrame {
match self.mode {
CameraAnimatorMode::Track => {
self.track.evaluate(t)
}
CameraAnimatorMode::Rig => {
let u = if self.track.total_duration > 0.0 {
(t / self.track.total_duration).clamp(0.0, 1.0)
} else {
0.0
};
if let Some(ref rig) = self.rig {
rig.evaluate(u)
} else {
self.track.evaluate(t)
}
}
CameraAnimatorMode::Dolly => {
if let Some(ref dolly) = self.dolly {
dolly.frame()
} else {
self.track.evaluate(t)
}
}
CameraAnimatorMode::Blend => {
let track_frame = self.track.evaluate(t);
let rig_frame = if let Some(ref rig) = self.rig {
let u = if self.track.total_duration > 0.0 {
(t / self.track.total_duration).clamp(0.0, 1.0)
} else {
0.0
};
rig.evaluate(u)
} else {
track_frame
};
track_frame.blend(&rig_frame, self.blend)
}
}
}
pub fn duration(&self) -> f32 {
match self.mode {
CameraAnimatorMode::Track | CameraAnimatorMode::Blend => self.track.total_duration,
CameraAnimatorMode::Rig => {
self.rig.as_ref().map(|_| self.track.total_duration).unwrap_or(0.0)
}
CameraAnimatorMode::Dolly => {
self.dolly.as_ref().map(|d| d.total_length / 2.0_f32.max(f32::EPSILON)).unwrap_or(0.0)
}
}
}
pub fn progress(&self) -> f32 {
let d = self.duration();
if d < f32::EPSILON { 0.0 } else { (self.time / d).clamp(0.0, 1.0) }
}
pub fn is_playing(&self) -> bool { self.playing }
pub fn is_finished(&self) -> bool { !self.playing && self.time >= self.duration() }
pub fn apply_shake(&mut self, offset: Vec3) {
self.shake_offset = offset;
}
}
impl Default for CameraAnimator {
fn default() -> Self { Self::new() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn easing_linear_identity() {
for i in 0..=10 {
let t = i as f32 / 10.0;
let v = easing_evaluate(&EasingCurve::Linear, t);
assert!((v - t).abs() < 1e-5, "linear easing failed at t={}", t);
}
}
#[test]
fn easing_ease_in_out_symmetry() {
let a = easing_evaluate(&EasingCurve::EaseInOut, 0.25);
let b = easing_evaluate(&EasingCurve::EaseInOut, 0.75);
assert!((a - (1.0 - b)).abs() < 1e-5);
}
#[test]
fn easing_step_binary() {
assert_eq!(easing_evaluate(&EasingCurve::Step, 0.3), 0.0);
assert_eq!(easing_evaluate(&EasingCurve::Step, 0.7), 1.0);
}
#[test]
fn easing_spring_settles_near_one() {
let curve = EasingCurve::Spring { stiffness: 50.0, damping: 14.0 };
let v = easing_evaluate(&curve, 1.0);
assert!(v > 0.5, "spring didn't settle: v={}", v);
}
#[test]
fn shot_evaluate_at_zero_is_start() {
let shot = CameraShot::from_to(Vec3::ZERO, Vec3::new(10.0, 0.0, 0.0), 2.0);
let frame = shot.evaluate(0.0);
assert!((frame.pos - Vec3::ZERO).length() < 1e-5);
}
#[test]
fn shot_evaluate_at_one_is_end() {
let shot = CameraShot::from_to(Vec3::ZERO, Vec3::new(10.0, 0.0, 0.0), 2.0);
let frame = shot.evaluate(1.0);
assert!((frame.pos - Vec3::new(10.0, 0.0, 0.0)).length() < 1e-5);
}
#[test]
fn shot_fov_interpolates() {
let shot = CameraShot::new(1.0).with_fov(60.0, 90.0);
let frame = shot.evaluate(0.5);
assert!((frame.fov - 75.0).abs() < 0.5, "fov={}", frame.fov);
}
#[test]
fn track_single_shot() {
let mut track = CameraTrack::new();
track.push(CameraShot::static_at(Vec3::new(1.0, 0.0, 0.0), Quat::IDENTITY, 60.0, 3.0));
let frame = track.evaluate(1.5);
assert!((frame.pos - Vec3::new(1.0, 0.0, 0.0)).length() < 1e-5);
}
#[test]
fn track_two_shots_transition() {
let mut track = CameraTrack::new();
track.push(CameraShot::from_to(Vec3::ZERO, Vec3::new(5.0, 0.0, 0.0), 2.0));
track.push(CameraShot::from_to(Vec3::new(5.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0), 2.0));
assert!((track.total_duration - 4.0).abs() < 1e-5);
let frame = track.evaluate(2.0); assert!(frame.pos.x >= 4.9 && frame.pos.x <= 5.1, "pos.x={}", frame.pos.x);
}
#[test]
fn track_markers() {
let mut track = CameraTrack::new();
track.push(CameraShot::new(1.0).with_label("intro"));
track.push(CameraShot::new(2.0));
let markers = track.markers();
assert_eq!(markers.len(), 1);
assert_eq!(markers[0].0, "intro");
assert!((markers[0].1 - 0.0).abs() < 1e-5);
}
#[test]
fn spline_passes_through_endpoints() {
let pts = vec![Vec3::ZERO, Vec3::new(5.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0)];
let spline = CatmullRomSpline::new(pts);
let start = spline.evaluate_normalized(0.0);
let end = spline.evaluate_normalized(1.0);
assert!((start - Vec3::ZERO).length() < 0.5);
assert!((end - Vec3::new(10.0, 0.0, 0.0)).length() < 0.5);
}
#[test]
fn spline_arc_length_positive() {
let pts = vec![Vec3::ZERO, Vec3::new(10.0, 0.0, 0.0), Vec3::new(10.0, 10.0, 0.0)];
let spline = CatmullRomSpline::new(pts);
assert!(spline.total_length > 0.0);
}
#[test]
fn dolly_advances_on_tick() {
let mut dolly = DollyTrack::new(vec![Vec3::ZERO, Vec3::new(10.0, 0.0, 0.0)]);
dolly.tick(1.0);
assert!(dolly.position > 0.0);
}
#[test]
fn dolly_frame_is_valid() {
let mut dolly = DollyTrack::new(vec![Vec3::ZERO, Vec3::new(10.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 10.0)]);
dolly.tick(1.0);
let frame = dolly.frame();
assert!(frame.pos.length() >= 0.0); assert!(frame.fov > 0.0);
}
#[test]
fn virtual_camera_fov() {
let cam = VirtualCamera::new();
let fov = cam.fov_degrees();
assert!(fov > 10.0 && fov < 120.0, "fov={}", fov);
}
#[test]
fn virtual_camera_coc_at_focus_is_zero() {
let cam = VirtualCamera::new();
let coc = cam.coc_radius(cam.focus_distance);
assert!(coc < 0.01, "coc={}", coc);
}
#[test]
fn animator_track_mode() {
let mut track = CameraTrack::new();
track.push(CameraShot::static_at(Vec3::new(0.0, 5.0, 0.0), Quat::IDENTITY, 60.0, 2.0));
let mut anim = CameraAnimator::new()
.with_track(track)
.with_mode(CameraAnimatorMode::Track);
anim.play();
let frame = anim.tick(0.5);
assert!((frame.pos.y - 5.0).abs() < 0.01, "pos.y={}", frame.pos.y);
}
#[test]
fn animator_progress() {
let mut track = CameraTrack::new();
track.push(CameraShot::new(4.0));
let mut anim = CameraAnimator::new().with_track(track);
anim.play();
anim.tick(2.0);
let p = anim.progress();
assert!((p - 0.5).abs() < 0.01, "progress={}", p);
}
#[test]
fn animator_stops_at_end() {
let mut track = CameraTrack::new();
track.push(CameraShot::new(1.0));
let mut anim = CameraAnimator::new().with_track(track);
anim.play();
anim.tick(2.0);
assert!(anim.is_finished());
}
#[test]
fn animator_loops() {
let mut track = CameraTrack::new();
track.push(CameraShot::new(1.0));
let mut anim = CameraAnimator::new()
.with_track(track)
.with_looping(true);
anim.play();
anim.tick(1.5);
assert!(anim.is_playing());
assert!(anim.time < 1.0, "time should have wrapped: {}", anim.time);
}
#[test]
fn camera_frame_blend() {
let a = CameraFrame { pos: Vec3::ZERO, rot: Quat::IDENTITY, fov: 60.0, near: 0.1, far: 100.0 };
let b = CameraFrame { pos: Vec3::new(10.0, 0.0, 0.0), rot: Quat::IDENTITY, fov: 90.0, near: 0.1, far: 100.0 };
let mid = a.blend(&b, 0.5);
assert!((mid.pos.x - 5.0).abs() < 0.01);
assert!((mid.fov - 75.0).abs() < 0.01);
}
#[test]
fn shot_transition_duration() {
assert_eq!(ShotTransition::Cut.duration(), 0.0);
assert!((ShotTransition::Dissolve { duration: 0.5 }.duration() - 0.5).abs() < 1e-5);
}
}