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 #[cfg(target_os = "none")]
139 let decay = libm::expf(-follow_strength * dt);
140 #[cfg(not(target_os = "none"))]
141 let decay = (-follow_strength * dt).exp();
142 let t = 1.0 - decay;
143 self.position.x += (target.x - self.position.x) * t;
144 self.position.y += (target.y - self.position.y) * t;
145 }
146 }
147
148 self.apply_bounds();
149 }
150
151 pub fn world_to_screen(&self, world_pos: Vec2) -> Vec2 {
166 Vec2::new(
167 (world_pos.x - self.position.x) * self.zoom,
168 (world_pos.y - self.position.y) * self.zoom,
169 )
170 }
171
172 pub fn screen_to_world(&self, screen_pos: Vec2) -> Vec2 {
184 Vec2::new(
185 screen_pos.x / self.zoom + self.position.x,
186 screen_pos.y / self.zoom + self.position.y,
187 )
188 }
189
190 pub fn clamp_to_bounds(&mut self, bounds: Rect) {
200 self.bounds = Some(bounds);
201 self.apply_bounds();
202 }
203
204 pub fn clear_bounds(&mut self) {
206 self.bounds = None;
207 }
208
209 fn apply_bounds(&mut self) {
211 let Some(ref bounds) = self.bounds else {
212 return;
213 };
214
215 let visible_w = self.viewport_size.x / self.zoom;
216 let visible_h = self.viewport_size.y / self.zoom;
217
218 if visible_w >= bounds.w {
220 self.position.x = bounds.x + (bounds.w - visible_w) * 0.5;
221 } else {
222 self.position.x = self
223 .position
224 .x
225 .clamp(bounds.x, bounds.x + bounds.w - visible_w);
226 }
227
228 if visible_h >= bounds.h {
229 self.position.y = bounds.y + (bounds.h - visible_h) * 0.5;
230 } else {
231 self.position.y = self
232 .position
233 .y
234 .clamp(bounds.y, bounds.y + bounds.h - visible_h);
235 }
236 }
237
238 pub fn set_zoom(&mut self, zoom: f32) {
244 self.zoom = zoom.clamp(0.1, 10.0);
245 self.apply_bounds();
247 }
248
249 pub fn tile_x(&self) -> i32 {
257 #[cfg(target_os = "none")]
259 {
260 libm::floorf(self.position.x / TILE_SIZE) as i32
261 }
262 #[cfg(not(target_os = "none"))]
263 {
264 (self.position.x / TILE_SIZE).floor() as i32
265 }
266 }
267
268 pub fn tile_y(&self) -> i32 {
272 #[cfg(target_os = "none")]
273 {
274 libm::floorf(self.position.y / TILE_SIZE) as i32
275 }
276 #[cfg(not(target_os = "none"))]
277 {
278 (self.position.y / TILE_SIZE).floor() as i32
279 }
280 }
281
282 pub fn is_visible(&self, world_pos: Vec2) -> bool {
289 let screen = self.world_to_screen(world_pos);
290 screen.x >= -TILE_SIZE
291 && screen.y >= -TILE_SIZE
292 && screen.x < self.viewport_size.x + TILE_SIZE
293 && screen.y < self.viewport_size.y + TILE_SIZE
294 }
295
296 pub fn is_rect_visible(&self, rect: Rect) -> bool {
299 let visible_w = self.viewport_size.x / self.zoom;
300 let visible_h = self.viewport_size.y / self.zoom;
301
302 let margin = TILE_SIZE / self.zoom;
304
305 !(rect.x + rect.w < self.position.x - margin
306 || rect.x > self.position.x + visible_w + margin
307 || rect.y + rect.h < self.position.y - margin
308 || rect.y > self.position.y + visible_h + margin)
309 }
310}
311
312#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
325 fn camera_starts_at_origin() {
326 let cam = Camera::new(160.0, 144.0);
327 assert_eq!(cam.position, Vec2::new(0.0, 0.0));
328 assert_eq!(cam.zoom, 1.0);
329 assert_eq!(cam.smooth_factor, 0.0);
330 assert!(cam.target.is_none());
331 assert!(cam.bounds.is_none());
332 }
333
334 #[test]
335 fn new_is_gameboy_screen() {
336 let cam = Camera::new(160.0, 144.0);
337 assert_eq!(cam.viewport_size, Vec2::new(160.0, 144.0));
338 }
339
340 #[test]
341 fn custom_viewport_size() {
342 let cam = Camera::new(640.0, 480.0);
343 assert_eq!(cam.viewport_size, Vec2::new(640.0, 480.0));
344 }
345
346 #[test]
351 fn follow_target_sets_target() {
352 let mut cam = Camera::new(160.0, 144.0);
353 cam.follow_target(Vec2::new(100.0, 50.0));
354 assert_eq!(cam.target, Some(Vec2::new(100.0, 50.0)));
355 }
356
357 #[test]
358 fn follow_target_overwrites_previous() {
359 let mut cam = Camera::new(160.0, 144.0);
360 cam.follow_target(Vec2::new(10.0, 20.0));
361 cam.follow_target(Vec2::new(30.0, 40.0));
362 assert_eq!(cam.target, Some(Vec2::new(30.0, 40.0)));
363 }
364
365 #[test]
366 fn stop_follow_clears_target() {
367 let mut cam = Camera::new(160.0, 144.0);
368 cam.follow_target(Vec2::new(100.0, 50.0));
369 cam.stop_follow();
370 assert!(cam.target.is_none());
371 }
372
373 #[test]
374 fn update_snaps_when_smooth_factor_zero() {
375 let mut cam = Camera::new(160.0, 144.0);
376 cam.smooth_factor = 0.0;
377 cam.follow_target(Vec2::new(100.0, 50.0));
378 cam.update(0.016);
379 assert_eq!(cam.position, Vec2::new(100.0, 50.0));
380 }
381
382 #[test]
383 fn update_moves_toward_target_with_smooth() {
384 let mut cam = Camera::new(160.0, 144.0);
385 cam.smooth_factor = 0.5;
386 cam.follow_target(Vec2::new(100.0, 0.0));
387 let start = cam.position;
388 cam.update(0.016);
389 assert!(cam.position.x > start.x);
390 assert!(cam.position.x < 100.0);
391 }
392
393 #[test]
394 fn update_no_movement_when_smooth_factor_one() {
395 let mut cam = Camera::new(160.0, 144.0);
396 cam.smooth_factor = 1.0;
397 cam.position = Vec2::new(50.0, 50.0);
398 cam.follow_target(Vec2::new(200.0, 200.0));
399 cam.update(0.016);
400 assert_eq!(cam.position.x, 50.0);
401 assert_eq!(cam.position.y, 50.0);
402 }
403
404 #[test]
405 fn update_no_target_does_nothing() {
406 let mut cam = Camera::new(160.0, 144.0);
407 cam.position = Vec2::new(30.0, 40.0);
408 cam.update(0.016);
409 assert_eq!(cam.position, Vec2::new(30.0, 40.0));
410 }
411
412 #[test]
413 fn update_converges_after_many_frames() {
414 let mut cam = Camera::new(160.0, 144.0);
415 cam.smooth_factor = 0.2;
416 cam.follow_target(Vec2::new(200.0, 0.0));
417 for _ in 0..200 {
418 cam.update(0.016);
419 }
420 assert!((cam.position.x - 200.0).abs() < 1.0);
421 }
422
423 #[test]
428 fn world_to_screen_at_origin() {
429 let cam = Camera::new(160.0, 144.0);
430 let screen = cam.world_to_screen(Vec2::new(50.0, 30.0));
431 assert_eq!(screen, Vec2::new(50.0, 30.0));
432 }
433
434 #[test]
435 fn world_to_screen_offset() {
436 let mut cam = Camera::new(160.0, 144.0);
437 cam.position = Vec2::new(100.0, 50.0);
438 let screen = cam.world_to_screen(Vec2::new(150.0, 100.0));
439 assert_eq!(screen, Vec2::new(50.0, 50.0));
440 }
441
442 #[test]
443 fn world_to_screen_negative_when_world_is_left_of_camera() {
444 let mut cam = Camera::new(160.0, 144.0);
445 cam.position = Vec2::new(100.0, 50.0);
446 let screen = cam.world_to_screen(Vec2::new(50.0, 20.0));
447 assert_eq!(screen, Vec2::new(-50.0, -30.0));
448 }
449
450 #[test]
451 fn screen_to_world_at_origin() {
452 let cam = Camera::new(160.0, 144.0);
453 let world = cam.screen_to_world(Vec2::new(80.0, 72.0));
454 assert_eq!(world, Vec2::new(80.0, 72.0));
455 }
456
457 #[test]
458 fn screen_to_world_offset() {
459 let mut cam = Camera::new(160.0, 144.0);
460 cam.position = Vec2::new(100.0, 50.0);
461 let world = cam.screen_to_world(Vec2::new(20.0, 30.0));
462 assert_eq!(world, Vec2::new(120.0, 80.0));
463 }
464
465 #[test]
466 fn round_trip_world_to_screen_to_world() {
467 let mut cam = Camera::new(160.0, 144.0);
468 cam.position = Vec2::new(42.5, 17.3);
469 cam.zoom = 1.5;
470
471 let world = Vec2::new(123.4, 56.7);
472 let screen = cam.world_to_screen(world);
473 let round_tripped = cam.screen_to_world(screen);
474
475 assert!((round_tripped.x - world.x).abs() < 0.001);
476 assert!((round_tripped.y - world.y).abs() < 0.001);
477 }
478
479 #[test]
480 fn round_trip_screen_to_world_to_screen() {
481 let mut cam = Camera::new(160.0, 144.0);
482 cam.position = Vec2::new(50.0, 30.0);
483 cam.zoom = 2.0;
484
485 let screen = Vec2::new(80.0, 72.0);
486 let world = cam.screen_to_world(screen);
487 let round_tripped = cam.world_to_screen(world);
488
489 assert!((round_tripped.x - screen.x).abs() < 0.001);
490 assert!((round_tripped.y - screen.y).abs() < 0.001);
491 }
492
493 #[test]
498 fn zoom_affects_world_to_screen() {
499 let mut cam = Camera::new(160.0, 144.0);
500 cam.position = Vec2::new(100.0, 100.0);
501
502 cam.zoom = 2.0;
505 let screen = cam.world_to_screen(Vec2::new(200.0, 200.0));
506 assert_eq!(screen, Vec2::new(200.0, 200.0));
507 }
508
509 #[test]
510 fn zoom_affects_screen_to_world() {
511 let mut cam = Camera::new(160.0, 144.0);
512 cam.position = Vec2::new(100.0, 100.0);
513 cam.zoom = 2.0;
514
515 let world = cam.screen_to_world(Vec2::new(80.0, 72.0));
517 assert_eq!(world, Vec2::new(140.0, 136.0));
518 }
519
520 #[test]
521 fn set_zoom_clamps_to_range() {
522 let mut cam = Camera::new(160.0, 144.0);
523 cam.set_zoom(0.0);
524 assert_eq!(cam.zoom, 0.1);
525 cam.set_zoom(100.0);
526 assert_eq!(cam.zoom, 10.0);
527 cam.set_zoom(1.5);
528 assert_eq!(cam.zoom, 1.5);
529 }
530
531 #[test]
532 fn set_zoom_clamps_bounds() {
533 let mut cam = Camera::new(160.0, 144.0);
534 cam.position = Vec2::new(0.0, 0.0);
535 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 320.0, 288.0));
536
537 assert_eq!(cam.position, Vec2::new(0.0, 0.0));
539 cam.position = Vec2::new(200.0, 200.0);
540 cam.set_zoom(1.0);
541 assert_eq!(cam.position, Vec2::new(160.0, 144.0));
542 }
543
544 #[test]
549 fn clamp_keeps_camera_in_bounds() {
550 let mut cam = Camera::new(160.0, 144.0);
551 cam.position = Vec2::new(500.0, 500.0);
552 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 256.0, 256.0));
554 assert_eq!(cam.position, Vec2::new(96.0, 112.0));
555 }
556
557 #[test]
558 fn clamp_prevents_negative_position() {
559 let mut cam = Camera::new(160.0, 144.0);
560 cam.position = Vec2::new(-100.0, -50.0);
561 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 256.0, 256.0));
562 assert_eq!(cam.position, Vec2::new(0.0, 0.0));
563 }
564
565 #[test]
566 fn clamp_centers_when_viewport_larger_than_bounds() {
567 let mut cam = Camera::new(320.0, 240.0);
568 cam.position = Vec2::new(0.0, 0.0);
569 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 160.0, 120.0));
571 assert_eq!(cam.position, Vec2::new(-80.0, -60.0));
573 }
574
575 #[test]
576 fn clear_bounds_allows_any_position() {
577 let mut cam = Camera::new(160.0, 144.0);
578 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 100.0, 100.0));
580 assert_eq!(cam.position, Vec2::new(-30.0, -22.0));
581
582 cam.position = Vec2::new(500.0, 500.0);
583 cam.clear_bounds();
584 assert_eq!(cam.position, Vec2::new(500.0, 500.0));
585 }
586
587 #[test]
588 fn update_clamps_after_follow() {
589 let mut cam = Camera::new(160.0, 144.0);
590 cam.smooth_factor = 0.0;
591 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 200.0, 200.0));
593 cam.follow_target(Vec2::new(500.0, 500.0));
594 cam.update(0.016);
595 assert_eq!(cam.position, Vec2::new(40.0, 56.0));
596 }
597
598 #[test]
603 fn tile_x_returns_correct_column() {
604 let mut cam = Camera::new(160.0, 144.0);
605 cam.position = Vec2::new(0.0, 0.0);
606 assert_eq!(cam.tile_x(), 0);
607
608 cam.position = Vec2::new(8.0, 0.0);
609 assert_eq!(cam.tile_x(), 1);
610
611 cam.position = Vec2::new(15.0, 0.0);
612 assert_eq!(cam.tile_x(), 1); cam.position = Vec2::new(16.0, 0.0);
615 assert_eq!(cam.tile_x(), 2);
616
617 cam.position = Vec2::new(-5.0, 0.0);
618 assert_eq!(cam.tile_x(), -1); }
620
621 #[test]
622 fn tile_y_returns_correct_row() {
623 let mut cam = Camera::new(160.0, 144.0);
624 cam.position = Vec2::new(0.0, 0.0);
625 assert_eq!(cam.tile_y(), 0);
626
627 cam.position = Vec2::new(0.0, 8.0);
628 assert_eq!(cam.tile_y(), 1);
629
630 cam.position = Vec2::new(0.0, 72.0);
631 assert_eq!(cam.tile_y(), 9); }
633
634 #[test]
639 fn is_visible_in_viewport() {
640 let cam = Camera::new(160.0, 144.0);
641 assert!(cam.is_visible(Vec2::new(80.0, 72.0)));
643 assert!(cam.is_visible(Vec2::new(0.0, 0.0)));
645 assert!(cam.is_visible(Vec2::new(-7.0, 0.0))); assert!(!cam.is_visible(Vec2::new(1000.0, 1000.0)));
649 }
650
651 #[test]
652 fn is_rect_visible_intersects() {
653 let cam = Camera::new(160.0, 144.0);
654 assert!(cam.is_rect_visible(Rect::new(0.0, 0.0, 32.0, 32.0)));
655 assert!(cam.is_rect_visible(Rect::new(140.0, 120.0, 32.0, 32.0)));
656 }
657
658 #[test]
659 fn is_rect_visible_far_away() {
660 let cam = Camera::new(160.0, 144.0);
661 assert!(!cam.is_rect_visible(Rect::new(1000.0, 1000.0, 32.0, 32.0)));
662 }
663
664 #[test]
669 fn vec2_new() {
670 let v = Vec2::new(3.0, 4.0);
671 assert_eq!(v.x, 3.0);
672 assert_eq!(v.y, 4.0);
673 }
674
675 #[test]
676 fn rect_new() {
677 let r = Rect::new(1.0, 2.0, 10.0, 20.0);
678 assert_eq!(r.x, 1.0);
679 assert_eq!(r.y, 2.0);
680 assert_eq!(r.w, 10.0);
681 assert_eq!(r.h, 20.0);
682 }
683}