1#[derive(Debug, Clone, Copy, PartialEq)]
23pub struct Vec2 {
24 pub x: f32,
25 pub y: f32,
26}
27
28impl Vec2 {
29 pub const fn new(x: f32, y: f32) -> Self {
30 Self { x, y }
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq)]
36pub struct Rect {
37 pub x: f32,
38 pub y: f32,
39 pub w: f32,
40 pub h: f32,
41}
42
43impl Rect {
44 pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
45 Self { x, y, w, h }
46 }
47}
48
49const TILE_SIZE: f32 = 8.0;
51
52const DEFAULT_FOLLOW_SPEED: f32 = 12.0;
54
55#[derive(Debug, Clone, PartialEq)]
60pub struct Camera {
61 pub position: Vec2,
63 pub target: Option<Vec2>,
65 pub bounds: Option<Rect>,
67 pub smooth_factor: f32,
69 pub zoom: f32,
71 pub viewport_size: Vec2,
73}
74
75impl Camera {
76 pub fn new(viewport_width: f32, viewport_height: f32) -> Self {
85 Self {
86 position: Vec2::new(0.0, 0.0),
87 target: None,
88 bounds: None,
89 smooth_factor: 0.0,
90 zoom: 1.0,
91 viewport_size: Vec2::new(viewport_width, viewport_height),
92 }
93 }
94
95 pub fn follow_target(&mut self, target: Vec2) {
104 self.target = Some(target);
105 }
106
107 pub fn stop_follow(&mut self) {
109 self.target = None;
110 }
111
112 pub fn update(&mut self, dt: f32) {
128 assert!(dt >= 0.0, "dt must be non-negative, got {dt}");
129
130 if let Some(ref target) = self.target {
131 if self.smooth_factor <= 0.0 {
132 self.position = *target;
134 } else {
135 let follow_strength = (1.0 - self.smooth_factor) * DEFAULT_FOLLOW_SPEED;
136 let t = 1.0 - (-follow_strength * dt).exp();
137 self.position.x += (target.x - self.position.x) * t;
138 self.position.y += (target.y - self.position.y) * t;
139 }
140 }
141
142 self.apply_bounds();
143 }
144
145 pub fn world_to_screen(&self, world_pos: Vec2) -> Vec2 {
160 Vec2::new(
161 (world_pos.x - self.position.x) * self.zoom,
162 (world_pos.y - self.position.y) * self.zoom,
163 )
164 }
165
166 pub fn screen_to_world(&self, screen_pos: Vec2) -> Vec2 {
178 Vec2::new(
179 screen_pos.x / self.zoom + self.position.x,
180 screen_pos.y / self.zoom + self.position.y,
181 )
182 }
183
184 pub fn clamp_to_bounds(&mut self, bounds: Rect) {
194 self.bounds = Some(bounds);
195 self.apply_bounds();
196 }
197
198 pub fn clear_bounds(&mut self) {
200 self.bounds = None;
201 }
202
203 fn apply_bounds(&mut self) {
205 let Some(ref bounds) = self.bounds else {
206 return;
207 };
208
209 let visible_w = self.viewport_size.x / self.zoom;
210 let visible_h = self.viewport_size.y / self.zoom;
211
212 if visible_w >= bounds.w {
214 self.position.x = bounds.x + (bounds.w - visible_w) * 0.5;
215 } else {
216 self.position.x = self
217 .position
218 .x
219 .clamp(bounds.x, bounds.x + bounds.w - visible_w);
220 }
221
222 if visible_h >= bounds.h {
223 self.position.y = bounds.y + (bounds.h - visible_h) * 0.5;
224 } else {
225 self.position.y = self
226 .position
227 .y
228 .clamp(bounds.y, bounds.y + bounds.h - visible_h);
229 }
230 }
231
232 pub fn set_zoom(&mut self, zoom: f32) {
238 self.zoom = zoom.clamp(0.1, 10.0);
239 self.apply_bounds();
241 }
242
243 pub fn tile_x(&self) -> i32 {
251 (self.position.x / TILE_SIZE).floor() as i32
252 }
253
254 pub fn tile_y(&self) -> i32 {
258 (self.position.y / TILE_SIZE).floor() as i32
259 }
260
261 pub fn is_visible(&self, world_pos: Vec2) -> bool {
268 let screen = self.world_to_screen(world_pos);
269 screen.x >= -TILE_SIZE
270 && screen.y >= -TILE_SIZE
271 && screen.x < self.viewport_size.x + TILE_SIZE
272 && screen.y < self.viewport_size.y + TILE_SIZE
273 }
274
275 pub fn is_rect_visible(&self, rect: Rect) -> bool {
278 let visible_w = self.viewport_size.x / self.zoom;
279 let visible_h = self.viewport_size.y / self.zoom;
280
281 let margin = TILE_SIZE / self.zoom;
283
284 !(rect.x + rect.w < self.position.x - margin
285 || rect.x > self.position.x + visible_w + margin
286 || rect.y + rect.h < self.position.y - margin
287 || rect.y > self.position.y + visible_h + margin)
288 }
289}
290
291#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
304 fn camera_starts_at_origin() {
305 let cam = Camera::new(160.0, 144.0);
306 assert_eq!(cam.position, Vec2::new(0.0, 0.0));
307 assert_eq!(cam.zoom, 1.0);
308 assert_eq!(cam.smooth_factor, 0.0);
309 assert!(cam.target.is_none());
310 assert!(cam.bounds.is_none());
311 }
312
313 #[test]
314 fn new_is_gameboy_screen() {
315 let cam = Camera::new(160.0, 144.0);
316 assert_eq!(cam.viewport_size, Vec2::new(160.0, 144.0));
317 }
318
319 #[test]
320 fn custom_viewport_size() {
321 let cam = Camera::new(640.0, 480.0);
322 assert_eq!(cam.viewport_size, Vec2::new(640.0, 480.0));
323 }
324
325 #[test]
330 fn follow_target_sets_target() {
331 let mut cam = Camera::new(160.0, 144.0);
332 cam.follow_target(Vec2::new(100.0, 50.0));
333 assert_eq!(cam.target, Some(Vec2::new(100.0, 50.0)));
334 }
335
336 #[test]
337 fn follow_target_overwrites_previous() {
338 let mut cam = Camera::new(160.0, 144.0);
339 cam.follow_target(Vec2::new(10.0, 20.0));
340 cam.follow_target(Vec2::new(30.0, 40.0));
341 assert_eq!(cam.target, Some(Vec2::new(30.0, 40.0)));
342 }
343
344 #[test]
345 fn stop_follow_clears_target() {
346 let mut cam = Camera::new(160.0, 144.0);
347 cam.follow_target(Vec2::new(100.0, 50.0));
348 cam.stop_follow();
349 assert!(cam.target.is_none());
350 }
351
352 #[test]
353 fn update_snaps_when_smooth_factor_zero() {
354 let mut cam = Camera::new(160.0, 144.0);
355 cam.smooth_factor = 0.0;
356 cam.follow_target(Vec2::new(100.0, 50.0));
357 cam.update(0.016);
358 assert_eq!(cam.position, Vec2::new(100.0, 50.0));
359 }
360
361 #[test]
362 fn update_moves_toward_target_with_smooth() {
363 let mut cam = Camera::new(160.0, 144.0);
364 cam.smooth_factor = 0.5;
365 cam.follow_target(Vec2::new(100.0, 0.0));
366 let start = cam.position;
367 cam.update(0.016);
368 assert!(cam.position.x > start.x);
369 assert!(cam.position.x < 100.0);
370 }
371
372 #[test]
373 fn update_no_movement_when_smooth_factor_one() {
374 let mut cam = Camera::new(160.0, 144.0);
375 cam.smooth_factor = 1.0;
376 cam.position = Vec2::new(50.0, 50.0);
377 cam.follow_target(Vec2::new(200.0, 200.0));
378 cam.update(0.016);
379 assert_eq!(cam.position.x, 50.0);
380 assert_eq!(cam.position.y, 50.0);
381 }
382
383 #[test]
384 fn update_no_target_does_nothing() {
385 let mut cam = Camera::new(160.0, 144.0);
386 cam.position = Vec2::new(30.0, 40.0);
387 cam.update(0.016);
388 assert_eq!(cam.position, Vec2::new(30.0, 40.0));
389 }
390
391 #[test]
392 fn update_converges_after_many_frames() {
393 let mut cam = Camera::new(160.0, 144.0);
394 cam.smooth_factor = 0.2;
395 cam.follow_target(Vec2::new(200.0, 0.0));
396 for _ in 0..200 {
397 cam.update(0.016);
398 }
399 assert!((cam.position.x - 200.0).abs() < 1.0);
400 }
401
402 #[test]
407 fn world_to_screen_at_origin() {
408 let cam = Camera::new(160.0, 144.0);
409 let screen = cam.world_to_screen(Vec2::new(50.0, 30.0));
410 assert_eq!(screen, Vec2::new(50.0, 30.0));
411 }
412
413 #[test]
414 fn world_to_screen_offset() {
415 let mut cam = Camera::new(160.0, 144.0);
416 cam.position = Vec2::new(100.0, 50.0);
417 let screen = cam.world_to_screen(Vec2::new(150.0, 100.0));
418 assert_eq!(screen, Vec2::new(50.0, 50.0));
419 }
420
421 #[test]
422 fn world_to_screen_negative_when_world_is_left_of_camera() {
423 let mut cam = Camera::new(160.0, 144.0);
424 cam.position = Vec2::new(100.0, 50.0);
425 let screen = cam.world_to_screen(Vec2::new(50.0, 20.0));
426 assert_eq!(screen, Vec2::new(-50.0, -30.0));
427 }
428
429 #[test]
430 fn screen_to_world_at_origin() {
431 let cam = Camera::new(160.0, 144.0);
432 let world = cam.screen_to_world(Vec2::new(80.0, 72.0));
433 assert_eq!(world, Vec2::new(80.0, 72.0));
434 }
435
436 #[test]
437 fn screen_to_world_offset() {
438 let mut cam = Camera::new(160.0, 144.0);
439 cam.position = Vec2::new(100.0, 50.0);
440 let world = cam.screen_to_world(Vec2::new(20.0, 30.0));
441 assert_eq!(world, Vec2::new(120.0, 80.0));
442 }
443
444 #[test]
445 fn round_trip_world_to_screen_to_world() {
446 let mut cam = Camera::new(160.0, 144.0);
447 cam.position = Vec2::new(42.5, 17.3);
448 cam.zoom = 1.5;
449
450 let world = Vec2::new(123.4, 56.7);
451 let screen = cam.world_to_screen(world);
452 let round_tripped = cam.screen_to_world(screen);
453
454 assert!((round_tripped.x - world.x).abs() < 0.001);
455 assert!((round_tripped.y - world.y).abs() < 0.001);
456 }
457
458 #[test]
459 fn round_trip_screen_to_world_to_screen() {
460 let mut cam = Camera::new(160.0, 144.0);
461 cam.position = Vec2::new(50.0, 30.0);
462 cam.zoom = 2.0;
463
464 let screen = Vec2::new(80.0, 72.0);
465 let world = cam.screen_to_world(screen);
466 let round_tripped = cam.world_to_screen(world);
467
468 assert!((round_tripped.x - screen.x).abs() < 0.001);
469 assert!((round_tripped.y - screen.y).abs() < 0.001);
470 }
471
472 #[test]
477 fn zoom_affects_world_to_screen() {
478 let mut cam = Camera::new(160.0, 144.0);
479 cam.position = Vec2::new(100.0, 100.0);
480
481 cam.zoom = 2.0;
484 let screen = cam.world_to_screen(Vec2::new(200.0, 200.0));
485 assert_eq!(screen, Vec2::new(200.0, 200.0));
486 }
487
488 #[test]
489 fn zoom_affects_screen_to_world() {
490 let mut cam = Camera::new(160.0, 144.0);
491 cam.position = Vec2::new(100.0, 100.0);
492 cam.zoom = 2.0;
493
494 let world = cam.screen_to_world(Vec2::new(80.0, 72.0));
496 assert_eq!(world, Vec2::new(140.0, 136.0));
497 }
498
499 #[test]
500 fn set_zoom_clamps_to_range() {
501 let mut cam = Camera::new(160.0, 144.0);
502 cam.set_zoom(0.0);
503 assert_eq!(cam.zoom, 0.1);
504 cam.set_zoom(100.0);
505 assert_eq!(cam.zoom, 10.0);
506 cam.set_zoom(1.5);
507 assert_eq!(cam.zoom, 1.5);
508 }
509
510 #[test]
511 fn set_zoom_clamps_bounds() {
512 let mut cam = Camera::new(160.0, 144.0);
513 cam.position = Vec2::new(0.0, 0.0);
514 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 320.0, 288.0));
515
516 assert_eq!(cam.position, Vec2::new(0.0, 0.0));
518 cam.position = Vec2::new(200.0, 200.0);
519 cam.set_zoom(1.0);
520 assert_eq!(cam.position, Vec2::new(160.0, 144.0));
521 }
522
523 #[test]
528 fn clamp_keeps_camera_in_bounds() {
529 let mut cam = Camera::new(160.0, 144.0);
530 cam.position = Vec2::new(500.0, 500.0);
531 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 256.0, 256.0));
533 assert_eq!(cam.position, Vec2::new(96.0, 112.0));
534 }
535
536 #[test]
537 fn clamp_prevents_negative_position() {
538 let mut cam = Camera::new(160.0, 144.0);
539 cam.position = Vec2::new(-100.0, -50.0);
540 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 256.0, 256.0));
541 assert_eq!(cam.position, Vec2::new(0.0, 0.0));
542 }
543
544 #[test]
545 fn clamp_centers_when_viewport_larger_than_bounds() {
546 let mut cam = Camera::new(320.0, 240.0);
547 cam.position = Vec2::new(0.0, 0.0);
548 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 160.0, 120.0));
550 assert_eq!(cam.position, Vec2::new(-80.0, -60.0));
552 }
553
554 #[test]
555 fn clear_bounds_allows_any_position() {
556 let mut cam = Camera::new(160.0, 144.0);
557 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 100.0, 100.0));
559 assert_eq!(cam.position, Vec2::new(-30.0, -22.0));
560
561 cam.position = Vec2::new(500.0, 500.0);
562 cam.clear_bounds();
563 assert_eq!(cam.position, Vec2::new(500.0, 500.0));
564 }
565
566 #[test]
567 fn update_clamps_after_follow() {
568 let mut cam = Camera::new(160.0, 144.0);
569 cam.smooth_factor = 0.0;
570 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 200.0, 200.0));
572 cam.follow_target(Vec2::new(500.0, 500.0));
573 cam.update(0.016);
574 assert_eq!(cam.position, Vec2::new(40.0, 56.0));
575 }
576
577 #[test]
582 fn tile_x_returns_correct_column() {
583 let mut cam = Camera::new(160.0, 144.0);
584 cam.position = Vec2::new(0.0, 0.0);
585 assert_eq!(cam.tile_x(), 0);
586
587 cam.position = Vec2::new(8.0, 0.0);
588 assert_eq!(cam.tile_x(), 1);
589
590 cam.position = Vec2::new(15.0, 0.0);
591 assert_eq!(cam.tile_x(), 1); cam.position = Vec2::new(16.0, 0.0);
594 assert_eq!(cam.tile_x(), 2);
595
596 cam.position = Vec2::new(-5.0, 0.0);
597 assert_eq!(cam.tile_x(), -1); }
599
600 #[test]
601 fn tile_y_returns_correct_row() {
602 let mut cam = Camera::new(160.0, 144.0);
603 cam.position = Vec2::new(0.0, 0.0);
604 assert_eq!(cam.tile_y(), 0);
605
606 cam.position = Vec2::new(0.0, 8.0);
607 assert_eq!(cam.tile_y(), 1);
608
609 cam.position = Vec2::new(0.0, 72.0);
610 assert_eq!(cam.tile_y(), 9); }
612
613 #[test]
618 fn is_visible_in_viewport() {
619 let cam = Camera::new(160.0, 144.0);
620 assert!(cam.is_visible(Vec2::new(80.0, 72.0)));
622 assert!(cam.is_visible(Vec2::new(0.0, 0.0)));
624 assert!(cam.is_visible(Vec2::new(-7.0, 0.0))); assert!(!cam.is_visible(Vec2::new(1000.0, 1000.0)));
628 }
629
630 #[test]
631 fn is_rect_visible_intersects() {
632 let cam = Camera::new(160.0, 144.0);
633 assert!(cam.is_rect_visible(Rect::new(0.0, 0.0, 32.0, 32.0)));
634 assert!(cam.is_rect_visible(Rect::new(140.0, 120.0, 32.0, 32.0)));
635 }
636
637 #[test]
638 fn is_rect_visible_far_away() {
639 let cam = Camera::new(160.0, 144.0);
640 assert!(!cam.is_rect_visible(Rect::new(1000.0, 1000.0, 32.0, 32.0)));
641 }
642
643 #[test]
648 fn vec2_new() {
649 let v = Vec2::new(3.0, 4.0);
650 assert_eq!(v.x, 3.0);
651 assert_eq!(v.y, 4.0);
652 }
653
654 #[test]
655 fn rect_new() {
656 let r = Rect::new(1.0, 2.0, 10.0, 20.0);
657 assert_eq!(r.x, 1.0);
658 assert_eq!(r.y, 2.0);
659 assert_eq!(r.w, 10.0);
660 assert_eq!(r.h, 20.0);
661 }
662}