use retroglyph_core::grid::{HasSize, Pos, Rect, Size};
mod tile;
mod transform;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Camera {
requested: Rect,
fit: ViewportFit,
viewport: Rect,
world: Size,
origin: Pos,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ViewportFit {
Exact,
Letterbox,
}
fn letterbox(rect: Rect, world: Size) -> Rect {
let width = rect.width().min(world.width());
let height = rect.height().min(world.height());
let x = rect.left().saturating_add((rect.width() - width) / 2);
let y = rect.top().saturating_add((rect.height() - height) / 2);
Rect::new(x, y, width, height)
}
impl Camera {
#[must_use]
pub const fn new(viewport: Rect, world: Size) -> Self {
Self {
requested: viewport,
fit: ViewportFit::Exact,
viewport,
world,
origin: Pos::new(0, 0),
}
}
#[must_use]
pub const fn viewport(&self) -> Rect {
self.viewport
}
#[must_use]
pub const fn world(&self) -> Size {
self.world
}
#[must_use]
pub const fn origin(&self) -> Pos {
self.origin
}
pub fn set_viewport(&mut self, viewport: Rect) {
self.requested = viewport;
self.fit = ViewportFit::Exact;
self.apply_viewport();
}
pub fn set_world(&mut self, world: Size) {
self.world = world;
self.apply_viewport();
}
fn apply_viewport(&mut self) {
self.viewport = match self.fit {
ViewportFit::Exact => self.requested,
ViewportFit::Letterbox => letterbox(self.requested, self.world),
};
self.set_origin(self.origin);
}
pub fn set_viewport_fitted(&mut self, viewport: Rect) {
self.requested = viewport;
self.fit = ViewportFit::Letterbox;
self.apply_viewport();
}
pub fn center_on(&mut self, target: Pos) {
self.set_origin(Pos::new(
target.x.saturating_sub(self.viewport.width() / 2),
target.y.saturating_sub(self.viewport.height() / 2),
));
}
pub fn set_origin(&mut self, origin: Pos) {
self.origin = Rect::from_tl_size(origin, self.viewport.size())
.clamp_within(self.world.to_rect())
.top_left();
}
pub fn scroll_by(&mut self, dx: i32, dy: i32) {
self.set_origin(self.origin.saturating_add_signed(ixy::Pos::new(dx, dy)));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileCamera {
tiles: Camera,
viewport: Rect,
zoom: Size,
}
impl TileCamera {
#[must_use]
pub fn new(viewport: Rect, world: Size, zoom: Size) -> Self {
let zoom = clamp_zoom(zoom);
Self {
tiles: Camera::new(tile_viewport(viewport, zoom), world),
viewport,
zoom,
}
}
#[must_use]
pub const fn viewport(&self) -> Rect {
self.viewport
}
#[must_use]
pub const fn world(&self) -> Size {
self.tiles.world()
}
#[must_use]
pub const fn origin(&self) -> Pos {
self.tiles.origin()
}
#[must_use]
pub const fn zoom(&self) -> Size {
self.zoom
}
pub fn set_zoom(&mut self, zoom: Size) {
self.zoom = clamp_zoom(zoom);
self.tiles
.set_viewport(tile_viewport(self.viewport, self.zoom));
}
pub fn set_viewport(&mut self, viewport: Rect) {
self.viewport = viewport;
self.tiles.set_viewport(tile_viewport(viewport, self.zoom));
}
pub fn set_world(&mut self, world: Size) {
self.tiles.set_world(world);
}
pub fn center_on(&mut self, target: Pos) {
self.tiles.center_on(target);
}
pub fn set_origin(&mut self, origin: Pos) {
self.tiles.set_origin(origin);
}
pub fn scroll_by(&mut self, dx: i32, dy: i32) {
self.tiles.scroll_by(dx, dy);
}
#[must_use]
pub fn visible_bounds(&self) -> Rect {
self.tiles.visible_bounds()
}
}
const fn clamp_zoom(zoom: Size) -> Size {
let width = if zoom.width == 0 { 1 } else { zoom.width };
let height = if zoom.height == 0 { 1 } else { zoom.height };
Size::new(width, height)
}
fn tile_viewport(viewport: Rect, zoom: Size) -> Rect {
Rect::from_tl_size(Pos::new(0, 0), viewport.size() / zoom)
}
#[cfg(test)]
pub(super) const fn cam() -> Camera {
Camera::new(Rect::new(0, 0, 10, 10), Size::new(100, 100))
}
#[cfg(test)]
pub(super) fn tile_cam() -> TileCamera {
TileCamera::new(
Rect::new(0, 0, 10, 10),
Size::new(100, 100),
Size::new(1, 1),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn centers_in_the_interior() {
let mut c = cam();
c.center_on(Pos::new(50, 50));
assert_eq!(c.origin(), Pos::new(45, 45));
assert_eq!(c.world_to_screen(Pos::new(50, 50)), Some(Pos::new(5, 5)));
assert_eq!(c.screen_to_world(Pos::new(5, 5)), Some(Pos::new(50, 50)));
}
#[test]
fn clamps_at_the_low_edge() {
let mut c = cam();
c.center_on(Pos::new(1, 1));
assert_eq!(c.origin(), Pos::new(0, 0));
assert_eq!(c.world_to_screen(Pos::new(1, 1)), Some(Pos::new(1, 1)));
}
#[test]
fn clamps_at_the_high_edge() {
let mut c = cam();
c.center_on(Pos::new(99, 99));
assert_eq!(c.origin(), Pos::new(90, 90));
assert_eq!(c.world_to_screen(Pos::new(99, 99)), Some(Pos::new(9, 9)));
}
#[test]
fn world_smaller_than_viewport_pins_origin() {
let mut c = Camera::new(Rect::new(2, 2, 20, 20), Size::new(5, 5));
c.center_on(Pos::new(3, 3));
assert_eq!(c.origin(), Pos::new(0, 0));
let visible = c.visible_bounds();
assert_eq!((visible.width(), visible.height()), (5, 5));
assert_eq!(c.world_to_screen(Pos::new(0, 0)), Some(Pos::new(2, 2)));
assert_eq!(c.world_to_screen(Pos::new(7, 7)), None);
}
#[test]
fn set_world_re_clamps_the_origin_when_the_world_shrinks() {
let mut c = cam();
c.center_on(Pos::new(50, 50));
assert_eq!(c.origin(), Pos::new(45, 45));
c.set_world(Size::new(20, 20));
assert_eq!(c.world(), Size::new(20, 20));
assert_eq!(c.origin(), Pos::new(10, 10));
}
#[test]
fn set_world_leaves_an_in_bounds_origin_unchanged_when_the_world_grows() {
let mut c = cam();
c.center_on(Pos::new(50, 50));
assert_eq!(c.origin(), Pos::new(45, 45));
c.set_world(Size::new(200, 200));
assert_eq!(c.world(), Size::new(200, 200));
assert_eq!(c.origin(), Pos::new(45, 45));
}
#[test]
fn set_world_pins_origin_to_zero_when_the_new_world_is_smaller_than_the_viewport() {
let mut c = cam();
c.center_on(Pos::new(50, 50));
c.set_world(Size::new(5, 5));
assert_eq!(c.origin(), Pos::new(0, 0));
}
#[test]
fn set_viewport_fitted_shrinks_and_centers_a_world_smaller_on_both_axes() {
let mut c = Camera::new(Rect::new(0, 0, 1, 1), Size::new(5, 5));
c.set_viewport_fitted(Rect::new(2, 2, 20, 20));
assert_eq!(c.viewport(), Rect::new(9, 9, 5, 5));
assert_eq!(c.origin(), Pos::new(0, 0));
assert_eq!(c.visible_bounds(), Rect::new(0, 0, 5, 5));
assert_eq!(c.world_to_screen(Pos::new(0, 0)), Some(Pos::new(9, 9)));
}
#[test]
fn set_viewport_fitted_shrinks_only_the_axis_that_is_smaller() {
let mut c = Camera::new(Rect::new(0, 0, 1, 1), Size::new(5, 100));
c.set_viewport_fitted(Rect::new(0, 0, 20, 10));
assert_eq!(c.viewport(), Rect::new(7, 0, 5, 10));
}
#[test]
fn set_viewport_fitted_rounds_odd_slack_toward_the_right_and_bottom() {
let mut c = Camera::new(Rect::new(0, 0, 1, 1), Size::new(4, 4));
c.set_viewport_fitted(Rect::new(0, 0, 9, 9));
assert_eq!(c.viewport(), Rect::new(2, 2, 4, 4));
}
#[test]
fn set_world_re_derives_a_fitted_viewport_instead_of_leaving_it_stale() {
let mut c = Camera::new(Rect::new(2, 2, 20, 20), Size::new(5, 5));
c.set_viewport_fitted(Rect::new(2, 2, 20, 20));
assert_eq!(c.viewport(), Rect::new(9, 9, 5, 5));
c.set_world(Size::new(100, 100));
assert_eq!(c.viewport(), Rect::new(2, 2, 20, 20));
}
#[test]
fn set_viewport_reclamps_the_origin_when_the_viewport_grows() {
let mut c = cam(); c.center_on(Pos::new(99, 99)); c.set_viewport(Rect::new(0, 0, 40, 40));
assert_eq!(c.origin(), Pos::new(60, 60)); assert_eq!(c.visible_bounds(), Rect::new(60, 60, 40, 40));
}
#[test]
fn zero_size_camera_is_inert() {
let c = Camera::new(Rect::new(0, 0, 0, 0), Size::new(0, 0));
assert_eq!(c.visible_bounds(), Rect::new(0, 0, 0, 0));
assert_eq!(c.cells().count(), 0);
assert_eq!(c.world_to_screen(Pos::new(0, 0)), None);
assert_eq!(c.screen_to_world(Pos::new(0, 0)), None);
let zero_world = Camera::new(Rect::new(0, 0, 10, 10), Size::new(0, 0));
assert_eq!(zero_world.visible_bounds(), Rect::new(0, 0, 0, 0));
assert_eq!(zero_world.cells().count(), 0);
assert_eq!(zero_world.screen_to_world(Pos::new(0, 0)), None);
}
#[test]
fn set_viewport_fitted_matches_set_viewport_when_the_world_is_not_smaller() {
let mut a = Camera::new(Rect::new(0, 0, 1, 1), Size::new(100, 100));
a.set_viewport_fitted(Rect::new(0, 0, 10, 10));
let mut b = Camera::new(Rect::new(0, 0, 1, 1), Size::new(100, 100));
b.set_viewport(Rect::new(0, 0, 10, 10));
assert_eq!(a.viewport(), b.viewport());
assert_eq!(a.origin(), b.origin());
}
#[test]
fn set_viewport_fitted_saturates_instead_of_overflowing() {
let mut c = Camera::new(Rect::new(0, 0, 1, 1), Size::new(5, 5));
c.set_viewport_fitted(Rect::new(65_530, 0, 1_000, 10));
assert_eq!(c.viewport(), Rect::new(u16::MAX, 2, 5, 5));
}
#[test]
fn scroll_by_moves_the_origin_by_the_delta() {
let mut c = cam();
c.scroll_by(5, 3);
assert_eq!(c.origin(), Pos::new(5, 3));
c.scroll_by(-2, 1);
assert_eq!(c.origin(), Pos::new(3, 4));
}
#[test]
fn scroll_by_clamps_at_the_low_edge_without_overshooting() {
let mut c = cam();
c.scroll_by(-1000, -1000);
assert_eq!(c.origin(), Pos::new(0, 0));
}
#[test]
fn scroll_by_clamps_at_the_high_edge_without_overshooting() {
let mut c = cam(); c.scroll_by(1000, 1000);
assert_eq!(c.origin(), Pos::new(90, 90));
}
#[test]
fn scroll_by_has_no_dead_zone_reversing_direction_past_an_edge() {
let mut c = cam(); c.scroll_by(1000, 0); assert_eq!(c.origin(), Pos::new(90, 0));
c.scroll_by(-1, 0); assert_eq!(
c.origin(),
Pos::new(89, 0),
"a single reversed cell must move the origin"
);
}
#[test]
fn set_origin_places_the_origin_exactly_when_in_bounds() {
let mut c = cam();
c.set_origin(Pos::new(12, 34));
assert_eq!(c.origin(), Pos::new(12, 34));
}
#[test]
fn set_origin_clamps_to_the_world_edge() {
let mut c = cam(); c.set_origin(Pos::new(95, 200));
assert_eq!(c.origin(), Pos::new(90, 90));
}
#[test]
fn set_origin_never_underflows_when_the_viewport_exceeds_the_world() {
let mut c = Camera::new(Rect::new(0, 0, 20, 20), Size::new(5, 5));
c.set_origin(Pos::new(3, 3));
assert_eq!(c.origin(), Pos::new(0, 0));
}
#[test]
fn center_on_and_set_origin_agree_on_the_clamped_result() {
let mut a = cam();
a.center_on(Pos::new(99, 99));
let mut b = cam();
b.set_origin(Pos::new(94, 94));
assert_eq!(a.origin(), b.origin());
}
}