sge_camera 1.2.0

Camera functionality for SGE
Documentation
use glium::winit::window::Window;
use sge_vectors::{Mat3, Mat4, Rect, Vec2, Vec4, vec2};

const BIG_NUMBER: f32 = 9999.9;

#[derive(Clone, Debug, Copy)]
pub struct Camera2D {
    translation: Vec2,
    scale: f32,
    rotation: f32,

    offset: Vec2,
    flip_y: bool,

    window_size: Vec2,

    view_matrix: Mat3,
    pub(crate) inverse_view_matrix: Mat3,
    projection_matrix: Mat4,
    needs_update: bool,
    visible_bounds: (Vec2, Vec2),
}

impl Camera2D {
    pub fn new(window_width: u32, window_height: u32, flip_y: bool) -> Self {
        let mut camera = Self {
            translation: Vec2::ZERO,
            offset: Vec2::ZERO,
            scale: 1.0,
            rotation: 0.0,
            window_size: Vec2::new(window_width as f32, window_height as f32),
            view_matrix: Mat3::IDENTITY,
            inverse_view_matrix: Mat3::IDENTITY,
            projection_matrix: Mat4::IDENTITY,
            visible_bounds: (Vec2::ZERO, Vec2::ZERO),
            needs_update: true,
            flip_y,
        };
        camera.update_matrices();
        camera
    }

    pub fn from_window(window: &Window, flip_y: bool) -> Self {
        let size = window.inner_size();
        Self::new(size.width, size.height, flip_y)
    }

    pub fn update_sizes(&mut self, window_width: u32, window_height: u32) {
        self.window_size = Vec2::new(window_width as f32, window_height as f32);
        self.needs_update = true;
    }

    pub fn mark_dirty(&mut self) {
        self.needs_update = true;
    }

    pub fn update_matrices(&mut self) {
        if !self.needs_update {
            return;
        }

        self.needs_update = false;

        let sign = if self.flip_y { 1.0 } else { -1.0 };
        let translation_matrix =
            Mat3::from_translation((sign * self.translation) + (sign * self.offset));
        let rotation_matrix = Mat3::from_angle(sign * self.rotation);
        let scale_matrix = Mat3::from_scale(Vec2::splat(self.scale) * vec2(1.0, -sign));

        self.view_matrix = scale_matrix * rotation_matrix * translation_matrix;
        self.inverse_view_matrix = self.view_matrix.inverse();
        self.projection_matrix = self.generate_projection_matrix();

        self.visible_bounds = self.calculate_visible_bounds();
    }

    pub fn screen_center(&self) -> Vec2 {
        self.window_size * 0.5
    }

    pub fn screen_to_world(&mut self, screen_pos: Vec2) -> Vec2 {
        self.update_matrices();

        if self.scale == 0.0 {
            return self.translation;
        }

        let centered = screen_pos - self.screen_center();
        let world_pos_homogeneous = self.inverse_view_matrix * centered.extend(1.0);

        world_pos_homogeneous.truncate()
    }

    pub fn world_to_screen(&mut self, world_pos: Vec2) -> Vec2 {
        self.update_matrices();

        let camera_pos_homogeneous = self.view_matrix * world_pos.extend(1.0);
        let camera_pos = camera_pos_homogeneous.truncate();

        camera_pos + self.screen_center()
    }

    pub fn visible_bounds(&mut self) -> (Vec2, Vec2) {
        self.update_matrices();
        self.visible_bounds
    }

    fn calculate_visible_bounds(&mut self) -> (Vec2, Vec2) {
        let top_left = self.screen_to_world(Vec2::ZERO);
        let top_right = self.screen_to_world(Vec2::new(self.window_size.x, 0.0));
        let bottom_left = self.screen_to_world(Vec2::new(0.0, self.window_size.y));
        let bottom_right = self.screen_to_world(self.window_size);

        let min = top_left.min(top_right).min(bottom_left).min(bottom_right);
        let max = top_left.max(top_right).max(bottom_left).max(bottom_right);

        (min, max)
    }

    pub fn world_distance_to_screen(&self, world_distance: f32) -> f32 {
        world_distance * self.scale
    }

    pub fn screen_distance_to_world(&self, screen_distance: f32) -> f32 {
        screen_distance / self.scale
    }

    pub fn translate_by(&mut self, delta: Vec2) {
        self.translation += delta;
        self.mark_dirty();
    }

    pub fn pan_to(&mut self, world_pos: Vec2) {
        self.translation = world_pos;
        self.mark_dirty();
    }

    pub fn pan_by_screen(&mut self, screen_delta: Vec2) {
        let world_delta = self.screen_distance_to_world(1.0) * screen_delta;

        let angle = if self.flip_y {
            self.rotation
        } else {
            -self.rotation
        };
        let cos = angle.cos();
        let sin = angle.sin();
        let rotated = Vec2::new(
            world_delta.x * cos - world_delta.y * sin,
            world_delta.x * sin + world_delta.y * cos,
        );
        self.translate_by(rotated);
    }

    pub fn lerp_to(&mut self, target: Vec2, t: f32) {
        self.translation = self.translation.lerp(target, t);
        self.mark_dirty();
    }

    pub fn clamp_to_bounds(&mut self, min: Vec2, max: Vec2) {
        self.translation = self.translation.clamp(min, max);
        self.mark_dirty();
    }

    pub fn zoom_at(&mut self, screen_pos: Vec2, zoom_factor: f32) {
        let world_pos = self.screen_to_world(screen_pos);
        self.scale *= zoom_factor;
        self.mark_dirty();

        let new_screen_pos = self.world_to_screen(world_pos);
        let screen_delta = screen_pos - new_screen_pos;
        let world_delta = screen_delta / self.scale;
        self.translation -= world_delta;
        self.mark_dirty();
    }

    pub fn set_scale(&mut self, new_scale: f32) {
        let center = self.screen_center();
        let factor = new_scale / self.scale.max(f32::EPSILON);
        self.zoom_at(center, factor);
    }

    pub fn zoom_to_fit(&mut self, rect: Rect, padding: f32) {
        let world_w = rect.width() + padding * 2.0;
        let world_h = rect.height() + padding * 2.0;
        let scale_x = self.window_size.x / world_w.max(f32::EPSILON);
        let scale_y = self.window_size.y / world_h.max(f32::EPSILON);
        self.scale = scale_x.min(scale_y);
        self.translation = rect.center();
        self.mark_dirty();
    }

    pub fn lerp_zoom_to(&mut self, target_scale: f32, t: f32) {
        self.scale = self.scale + (target_scale - self.scale) * t;
        self.mark_dirty();
    }

    pub fn rotate_by(&mut self, delta_radians: f32) {
        self.rotation += delta_radians;
        self.mark_dirty();
    }

    pub fn set_rotation(&mut self, radians: f32) {
        self.rotation = radians;
        self.mark_dirty();
    }

    pub fn rotate_around(&mut self, pivot: Vec2, delta_radians: f32) {
        let to_pivot = self.translation - pivot;
        let cos = delta_radians.cos();
        let sin = delta_radians.sin();
        let rotated = Vec2::new(
            to_pivot.x * cos - to_pivot.y * sin,
            to_pivot.x * sin + to_pivot.y * cos,
        );
        self.translation = pivot + rotated;
        self.rotation += delta_radians;
        self.mark_dirty();
    }

    pub fn clear_offset(&mut self) {
        self.offset = Vec2::ZERO;
        self.mark_dirty();
    }

    pub fn center(&self) -> Vec2 {
        self.translation
    }

    pub fn contains_point(&mut self, world_pos: Vec2) -> bool {
        let (min, max) = self.visible_bounds();
        world_pos.cmpge(min).all() && world_pos.cmple(max).all()
    }

    pub fn overlaps_aabb(&mut self, min: Vec2, max: Vec2) -> bool {
        let (vmin, vmax) = self.visible_bounds();
        min.x <= vmax.x && max.x >= vmin.x && min.y <= vmax.y && max.y >= vmin.y
    }

    pub fn visible_rect(&mut self) -> Rect {
        let (min, max) = self.visible_bounds();
        Rect::from_corners(min, max)
    }

    pub fn window_size(&self) -> Vec2 {
        self.window_size
    }

    fn generate_projection_matrix(&mut self) -> Mat4 {
        let half_width = self.window_size.x * 0.5;
        let half_height = self.window_size.y * 0.5;

        let ortho = Mat4::orthographic_rh(
            -half_width,
            half_width,
            half_height,
            -half_height,
            -BIG_NUMBER,
            BIG_NUMBER,
        );

        let view_mat4 = Mat4::from_cols(
            self.view_matrix.x_axis.extend(0.0),
            self.view_matrix.y_axis.extend(0.0),
            Vec4::new(0.0, 0.0, 1.0, 0.0),
            self.view_matrix.z_axis.extend(1.0),
        );

        ortho * view_mat4
    }

    pub fn projection_matrix(&mut self) -> Mat4 {
        self.update_matrices();
        self.projection_matrix
    }

    pub fn offset(&self) -> Vec2 {
        self.offset
    }

    pub fn offset_mut(&mut self) -> &mut Vec2 {
        self.mark_dirty();
        &mut self.offset
    }

    pub fn set_offset(&mut self, offset: Vec2) {
        self.offset = offset;
        self.mark_dirty();
    }

    pub fn offset_by(&mut self, delta: Vec2) {
        self.offset += delta;
        self.mark_dirty();
    }

    pub fn translation(&self) -> Vec2 {
        self.translation
    }

    pub fn set_translation(&mut self, translation: Vec2) {
        self.translation = translation;
        self.mark_dirty();
    }

    pub fn translation_mut(&mut self) -> &mut Vec2 {
        self.mark_dirty();
        &mut self.translation
    }

    pub fn scale(&self) -> f32 {
        self.scale
    }

    pub fn scale_mut(&mut self) -> &mut f32 {
        self.mark_dirty();
        &mut self.scale
    }

    pub fn rotation(&self) -> f32 {
        self.rotation
    }

    pub fn rotation_mut(&mut self) -> &mut f32 {
        self.mark_dirty();
        &mut self.rotation
    }

    pub fn flip_y(&self) -> bool {
        self.flip_y
    }

    pub fn set_flip_y(&mut self, flip: bool) {
        if self.flip_y != flip {
            self.flip_y = flip;
            self.mark_dirty();
            self.update_matrices();
        }
    }
}

pub fn projection_from_window(window: &Window, flip_y: bool) -> Mat4 {
    let size = window.inner_size();
    projection(size.width, size.height, flip_y)
}

pub fn projection(width: u32, height: u32, flip_y: bool) -> Mat4 {
    if flip_y {
        Mat4::orthographic_rh(0.0, width as f32, 0.0, height as f32, -1.0, 1.0)
    } else {
        Mat4::orthographic_rh(0.0, width as f32, height as f32, 0.0, -1.0, 1.0)
    }
}