use crate::viewport::CanvasViewport;
#[derive(Clone, Debug)]
pub struct PanController {
velocity_x: f32,
velocity_y: f32,
smoothing: f32,
deceleration: f32,
pub is_animating: bool,
last_dx: f32,
last_dy: f32,
}
impl Default for PanController {
fn default() -> Self {
Self::new()
}
}
impl PanController {
pub fn new() -> Self {
Self {
velocity_x: 0.0,
velocity_y: 0.0,
smoothing: 0.3,
deceleration: 1500.0,
is_animating: false,
last_dx: 0.0,
last_dy: 0.0,
}
}
pub fn with_smoothing(mut self, smoothing: f32) -> Self {
self.smoothing = smoothing.clamp(0.01, 1.0);
self
}
pub fn with_deceleration(mut self, deceleration: f32) -> Self {
self.deceleration = deceleration.max(0.0);
self
}
pub fn on_drag(&mut self, viewport: &mut CanvasViewport, dx: f32, dy: f32) {
viewport.pan_by(dx - self.last_dx, dy - self.last_dy);
let instant_vx = (dx - self.last_dx) * 60.0;
let instant_vy = (dy - self.last_dy) * 60.0;
self.velocity_x = self.velocity_x * (1.0 - self.smoothing) + instant_vx * self.smoothing;
self.velocity_y = self.velocity_y * (1.0 - self.smoothing) + instant_vy * self.smoothing;
self.last_dx = dx;
self.last_dy = dy;
self.is_animating = false;
}
pub fn on_drag_end(&mut self) {
self.last_dx = 0.0;
self.last_dy = 0.0;
let speed = (self.velocity_x * self.velocity_x + self.velocity_y * self.velocity_y).sqrt();
self.is_animating = speed > 50.0;
if !self.is_animating {
self.velocity_x = 0.0;
self.velocity_y = 0.0;
}
}
pub fn tick(&mut self, viewport: &mut CanvasViewport, dt: f32) -> bool {
if !self.is_animating {
return false;
}
viewport.pan_by(self.velocity_x * dt, self.velocity_y * dt);
let speed = (self.velocity_x * self.velocity_x + self.velocity_y * self.velocity_y).sqrt();
if speed < 1.0 {
self.stop();
return false;
}
let decel = self.deceleration * dt;
let factor = ((speed - decel) / speed).max(0.0);
self.velocity_x *= factor;
self.velocity_y *= factor;
true
}
pub fn stop(&mut self) {
self.velocity_x = 0.0;
self.velocity_y = 0.0;
self.is_animating = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pan_controller_default() {
let pc = PanController::new();
assert_eq!(pc.velocity_x, 0.0);
assert_eq!(pc.velocity_y, 0.0);
assert!(!pc.is_animating);
}
#[test]
fn test_on_drag_applies_delta() {
let mut pc = PanController::new();
let mut vp = CanvasViewport::new();
pc.on_drag(&mut vp, 10.0, 20.0);
assert!((vp.pan_x - 10.0).abs() < 1e-3);
assert!((vp.pan_y - 20.0).abs() < 1e-3);
}
#[test]
fn test_drag_end_starts_momentum() {
let mut pc = PanController::new();
let mut vp = CanvasViewport::new();
pc.on_drag(&mut vp, 5.0, 0.0);
pc.on_drag(&mut vp, 15.0, 0.0);
pc.on_drag(&mut vp, 30.0, 0.0);
pc.on_drag_end();
assert!(pc.is_animating);
assert!(pc.velocity_x.abs() > 0.0);
}
#[test]
fn test_momentum_decelerates_to_zero() {
let mut pc = PanController::new();
let mut vp = CanvasViewport::new();
pc.on_drag(&mut vp, 10.0, 0.0);
pc.on_drag(&mut vp, 25.0, 0.0);
pc.on_drag_end();
let dt = 1.0 / 60.0;
let mut frames = 0;
while pc.tick(&mut vp, dt) {
frames += 1;
if frames > 600 {
break;
}
}
assert!(!pc.is_animating);
assert!(frames < 600, "momentum should settle within 10 seconds");
}
#[test]
fn test_stop() {
let mut pc = PanController::new();
pc.velocity_x = 500.0;
pc.is_animating = true;
pc.stop();
assert!(!pc.is_animating);
assert_eq!(pc.velocity_x, 0.0);
}
}