use crate::math::{Vec2, Vec3, Rect, Mat4, Vec2Ext};
#[derive(Debug, Clone)]
pub struct Camera {
pub position: Vec2,
pub rotation: f32,
pub zoom: f32,
pub min_zoom: f32,
pub max_zoom: f32,
follow_target: Option<Vec2>,
follow_lerp: f32,
follow_deadzone: f32,
shake: CameraShake,
viewport: Option<Rect>,
layers: u32,
z_order: i32,
}
#[derive(Debug, Clone, Default)]
struct CameraShake {
intensity: f32,
duration: f32,
remaining: f32,
offset: Vec2,
}
impl Camera {
pub fn new(x: f32, y: f32) -> Self {
Self {
position: Vec2::new(x, y),
rotation: 0.0,
zoom: 1.0,
min_zoom: 0.01,
max_zoom: 100.0,
follow_target: None,
follow_lerp: 0.1,
follow_deadzone: 0.0,
shake: CameraShake::default(),
viewport: None,
layers: u32::MAX, z_order: 0,
}
}
pub fn centered() -> Self {
Self::new(0.0, 0.0)
}
pub fn set_zoom(&mut self, zoom: f32) {
self.zoom = zoom.clamp(self.min_zoom, self.max_zoom);
}
pub fn set_zoom_limits(&mut self, min: f32, max: f32) {
self.min_zoom = min.min(max);
self.max_zoom = max.max(min);
self.zoom = self.zoom.clamp(self.min_zoom, self.max_zoom);
}
pub fn follow(&mut self, target: Vec2, lerp_speed: f32) {
self.follow_target = Some(target);
self.follow_lerp = lerp_speed;
}
pub fn set_follow_deadzone(&mut self, deadzone: f32) {
self.follow_deadzone = deadzone;
}
pub fn stop_following(&mut self) {
self.follow_target = None;
}
pub fn shake(&mut self, intensity: f32, duration: f32) {
self.shake.intensity = intensity;
self.shake.duration = duration;
self.shake.remaining = duration;
}
pub fn set_viewport(&mut self, viewport: Rect) {
self.viewport = Some(viewport);
}
pub fn clear_viewport(&mut self) {
self.viewport = None;
}
pub fn set_layers(&mut self, layers: u32) {
self.layers = layers;
}
pub fn set_z_order(&mut self, z: i32) {
self.z_order = z;
}
pub fn update(&mut self, dt: f32) {
if let Some(target) = self.follow_target {
let diff = target - self.position;
let dist = diff.length();
if dist > self.follow_deadzone {
self.position = self.position.move_toward(target, dist * self.follow_lerp);
}
}
if self.shake.remaining > 0.0 {
self.shake.remaining -= dt;
let t = self.shake.remaining / self.shake.duration;
let current_intensity = self.shake.intensity * t;
let angle = quad_rand::gen_range(0.0, std::f32::consts::TAU);
self.shake.offset = Vec2::new(
angle.cos() * current_intensity,
angle.sin() * current_intensity,
);
} else {
self.shake.offset = Vec2::ZERO;
}
}
pub fn view_matrix(&self, screen_size: Vec2) -> Mat4 {
let offset = self.position + self.shake.offset;
let mut view = Mat4::from_translation(Vec3::new(
-offset.x,
-offset.y,
0.0,
));
if self.rotation.abs() > 1e-6 {
view = Mat4::from_rotation_z(-self.rotation) * view;
}
if (self.zoom - 1.0).abs() > 1e-6 {
let scale = Mat4::from_scale(Vec3::new(self.zoom, self.zoom, 1.0));
let to_center = Mat4::from_translation(Vec3::new(
screen_size.x * 0.5,
screen_size.y * 0.5,
0.0,
));
let from_center = Mat4::from_translation(Vec3::new(
-screen_size.x * 0.5,
-screen_size.y * 0.5,
0.0,
));
view = to_center * scale * from_center * view;
}
view
}
pub fn screen_to_world(&self, screen_pos: Vec2, screen_size: Vec2) -> Vec2 {
let center = screen_size * 0.5;
let mut world = (screen_pos - center) / self.zoom;
if self.rotation.abs() > 1e-6 {
world = world.rotated(-self.rotation);
}
world + self.position + self.shake.offset
}
pub fn world_to_screen(&self, world_pos: Vec2, screen_size: Vec2) -> Vec2 {
let center = screen_size * 0.5;
let relative = world_pos - self.position - self.shake.offset;
let rotated = if self.rotation.abs() > 1e-6 {
relative.rotated(self.rotation)
} else {
relative
};
rotated * self.zoom + center
}
pub fn visible_rect(&self, screen_size: Vec2) -> Rect {
let half_extents = screen_size / (2.0 * self.zoom);
Rect::centered(self.position, half_extents * 2.0)
}
}