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.position.x.clamp(bounds.x, bounds.x + bounds.w - visible_w);
217 }
218
219 if visible_h >= bounds.h {
220 self.position.y = bounds.y + (bounds.h - visible_h) * 0.5;
221 } else {
222 self.position.y = self.position.y.clamp(bounds.y, bounds.y + bounds.h - visible_h);
223 }
224 }
225
226 pub fn set_zoom(&mut self, zoom: f32) {
232 self.zoom = zoom.clamp(0.1, 10.0);
233 self.apply_bounds();
235 }
236
237 pub fn tile_x(&self) -> i32 {
245 (self.position.x / TILE_SIZE).floor() as i32
246 }
247
248 pub fn tile_y(&self) -> i32 {
252 (self.position.y / TILE_SIZE).floor() as i32
253 }
254
255 pub fn is_visible(&self, world_pos: Vec2) -> bool {
262 let screen = self.world_to_screen(world_pos);
263 screen.x >= -TILE_SIZE
264 && screen.y >= -TILE_SIZE
265 && screen.x < self.viewport_size.x + TILE_SIZE
266 && screen.y < self.viewport_size.y + TILE_SIZE
267 }
268
269 pub fn is_rect_visible(&self, rect: Rect) -> bool {
272 let visible_w = self.viewport_size.x / self.zoom;
273 let visible_h = self.viewport_size.y / self.zoom;
274
275 let margin = TILE_SIZE / self.zoom;
277
278 !(rect.x + rect.w < self.position.x - margin
279 || rect.x > self.position.x + visible_w + margin
280 || rect.y + rect.h < self.position.y - margin
281 || rect.y > self.position.y + visible_h + margin)
282 }
283}
284
285
286#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
299 fn camera_starts_at_origin() {
300 let cam = Camera::new(160.0, 144.0);
301 assert_eq!(cam.position, Vec2::new(0.0, 0.0));
302 assert_eq!(cam.zoom, 1.0);
303 assert_eq!(cam.smooth_factor, 0.0);
304 assert!(cam.target.is_none());
305 assert!(cam.bounds.is_none());
306 }
307
308 #[test]
309 fn new_is_gameboy_screen() {
310 let cam = Camera::new(160.0, 144.0);
311 assert_eq!(cam.viewport_size, Vec2::new(160.0, 144.0));
312 }
313
314 #[test]
315 fn custom_viewport_size() {
316 let cam = Camera::new(640.0, 480.0);
317 assert_eq!(cam.viewport_size, Vec2::new(640.0, 480.0));
318 }
319
320 #[test]
325 fn follow_target_sets_target() {
326 let mut cam = Camera::new(160.0, 144.0);
327 cam.follow_target(Vec2::new(100.0, 50.0));
328 assert_eq!(cam.target, Some(Vec2::new(100.0, 50.0)));
329 }
330
331 #[test]
332 fn follow_target_overwrites_previous() {
333 let mut cam = Camera::new(160.0, 144.0);
334 cam.follow_target(Vec2::new(10.0, 20.0));
335 cam.follow_target(Vec2::new(30.0, 40.0));
336 assert_eq!(cam.target, Some(Vec2::new(30.0, 40.0)));
337 }
338
339 #[test]
340 fn stop_follow_clears_target() {
341 let mut cam = Camera::new(160.0, 144.0);
342 cam.follow_target(Vec2::new(100.0, 50.0));
343 cam.stop_follow();
344 assert!(cam.target.is_none());
345 }
346
347 #[test]
348 fn update_snaps_when_smooth_factor_zero() {
349 let mut cam = Camera::new(160.0, 144.0);
350 cam.smooth_factor = 0.0;
351 cam.follow_target(Vec2::new(100.0, 50.0));
352 cam.update(0.016);
353 assert_eq!(cam.position, Vec2::new(100.0, 50.0));
354 }
355
356 #[test]
357 fn update_moves_toward_target_with_smooth() {
358 let mut cam = Camera::new(160.0, 144.0);
359 cam.smooth_factor = 0.5;
360 cam.follow_target(Vec2::new(100.0, 0.0));
361 let start = cam.position;
362 cam.update(0.016);
363 assert!(cam.position.x > start.x);
364 assert!(cam.position.x < 100.0);
365 }
366
367 #[test]
368 fn update_no_movement_when_smooth_factor_one() {
369 let mut cam = Camera::new(160.0, 144.0);
370 cam.smooth_factor = 1.0;
371 cam.position = Vec2::new(50.0, 50.0);
372 cam.follow_target(Vec2::new(200.0, 200.0));
373 cam.update(0.016);
374 assert_eq!(cam.position.x, 50.0);
375 assert_eq!(cam.position.y, 50.0);
376 }
377
378 #[test]
379 fn update_no_target_does_nothing() {
380 let mut cam = Camera::new(160.0, 144.0);
381 cam.position = Vec2::new(30.0, 40.0);
382 cam.update(0.016);
383 assert_eq!(cam.position, Vec2::new(30.0, 40.0));
384 }
385
386 #[test]
387 fn update_converges_after_many_frames() {
388 let mut cam = Camera::new(160.0, 144.0);
389 cam.smooth_factor = 0.2;
390 cam.follow_target(Vec2::new(200.0, 0.0));
391 for _ in 0..200 {
392 cam.update(0.016);
393 }
394 assert!((cam.position.x - 200.0).abs() < 1.0);
395 }
396
397 #[test]
402 fn world_to_screen_at_origin() {
403 let cam = Camera::new(160.0, 144.0);
404 let screen = cam.world_to_screen(Vec2::new(50.0, 30.0));
405 assert_eq!(screen, Vec2::new(50.0, 30.0));
406 }
407
408 #[test]
409 fn world_to_screen_offset() {
410 let mut cam = Camera::new(160.0, 144.0);
411 cam.position = Vec2::new(100.0, 50.0);
412 let screen = cam.world_to_screen(Vec2::new(150.0, 100.0));
413 assert_eq!(screen, Vec2::new(50.0, 50.0));
414 }
415
416 #[test]
417 fn world_to_screen_negative_when_world_is_left_of_camera() {
418 let mut cam = Camera::new(160.0, 144.0);
419 cam.position = Vec2::new(100.0, 50.0);
420 let screen = cam.world_to_screen(Vec2::new(50.0, 20.0));
421 assert_eq!(screen, Vec2::new(-50.0, -30.0));
422 }
423
424 #[test]
425 fn screen_to_world_at_origin() {
426 let cam = Camera::new(160.0, 144.0);
427 let world = cam.screen_to_world(Vec2::new(80.0, 72.0));
428 assert_eq!(world, Vec2::new(80.0, 72.0));
429 }
430
431 #[test]
432 fn screen_to_world_offset() {
433 let mut cam = Camera::new(160.0, 144.0);
434 cam.position = Vec2::new(100.0, 50.0);
435 let world = cam.screen_to_world(Vec2::new(20.0, 30.0));
436 assert_eq!(world, Vec2::new(120.0, 80.0));
437 }
438
439 #[test]
440 fn round_trip_world_to_screen_to_world() {
441 let mut cam = Camera::new(160.0, 144.0);
442 cam.position = Vec2::new(42.5, 17.3);
443 cam.zoom = 1.5;
444
445 let world = Vec2::new(123.4, 56.7);
446 let screen = cam.world_to_screen(world);
447 let round_tripped = cam.screen_to_world(screen);
448
449 assert!((round_tripped.x - world.x).abs() < 0.001);
450 assert!((round_tripped.y - world.y).abs() < 0.001);
451 }
452
453 #[test]
454 fn round_trip_screen_to_world_to_screen() {
455 let mut cam = Camera::new(160.0, 144.0);
456 cam.position = Vec2::new(50.0, 30.0);
457 cam.zoom = 2.0;
458
459 let screen = Vec2::new(80.0, 72.0);
460 let world = cam.screen_to_world(screen);
461 let round_tripped = cam.world_to_screen(world);
462
463 assert!((round_tripped.x - screen.x).abs() < 0.001);
464 assert!((round_tripped.y - screen.y).abs() < 0.001);
465 }
466
467 #[test]
472 fn zoom_affects_world_to_screen() {
473 let mut cam = Camera::new(160.0, 144.0);
474 cam.position = Vec2::new(100.0, 100.0);
475
476 cam.zoom = 2.0;
479 let screen = cam.world_to_screen(Vec2::new(200.0, 200.0));
480 assert_eq!(screen, Vec2::new(200.0, 200.0));
481 }
482
483 #[test]
484 fn zoom_affects_screen_to_world() {
485 let mut cam = Camera::new(160.0, 144.0);
486 cam.position = Vec2::new(100.0, 100.0);
487 cam.zoom = 2.0;
488
489 let world = cam.screen_to_world(Vec2::new(80.0, 72.0));
491 assert_eq!(world, Vec2::new(140.0, 136.0));
492 }
493
494 #[test]
495 fn set_zoom_clamps_to_range() {
496 let mut cam = Camera::new(160.0, 144.0);
497 cam.set_zoom(0.0);
498 assert_eq!(cam.zoom, 0.1);
499 cam.set_zoom(100.0);
500 assert_eq!(cam.zoom, 10.0);
501 cam.set_zoom(1.5);
502 assert_eq!(cam.zoom, 1.5);
503 }
504
505 #[test]
506 fn set_zoom_clamps_bounds() {
507 let mut cam = Camera::new(160.0, 144.0);
508 cam.position = Vec2::new(0.0, 0.0);
509 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 320.0, 288.0));
510
511 assert_eq!(cam.position, Vec2::new(0.0, 0.0));
513 cam.position = Vec2::new(200.0, 200.0);
514 cam.set_zoom(1.0);
515 assert_eq!(cam.position, Vec2::new(160.0, 144.0));
516 }
517
518 #[test]
523 fn clamp_keeps_camera_in_bounds() {
524 let mut cam = Camera::new(160.0, 144.0);
525 cam.position = Vec2::new(500.0, 500.0);
526 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 256.0, 256.0));
528 assert_eq!(cam.position, Vec2::new(96.0, 112.0));
529 }
530
531 #[test]
532 fn clamp_prevents_negative_position() {
533 let mut cam = Camera::new(160.0, 144.0);
534 cam.position = Vec2::new(-100.0, -50.0);
535 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 256.0, 256.0));
536 assert_eq!(cam.position, Vec2::new(0.0, 0.0));
537 }
538
539 #[test]
540 fn clamp_centers_when_viewport_larger_than_bounds() {
541 let mut cam = Camera::new(320.0, 240.0);
542 cam.position = Vec2::new(0.0, 0.0);
543 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 160.0, 120.0));
545 assert_eq!(cam.position, Vec2::new(-80.0, -60.0));
547 }
548
549 #[test]
550 fn clear_bounds_allows_any_position() {
551 let mut cam = Camera::new(160.0, 144.0);
552 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 100.0, 100.0));
554 assert_eq!(cam.position, Vec2::new(-30.0, -22.0));
555
556 cam.position = Vec2::new(500.0, 500.0);
557 cam.clear_bounds();
558 assert_eq!(cam.position, Vec2::new(500.0, 500.0));
559 }
560
561 #[test]
562 fn update_clamps_after_follow() {
563 let mut cam = Camera::new(160.0, 144.0);
564 cam.smooth_factor = 0.0;
565 cam.clamp_to_bounds(Rect::new(0.0, 0.0, 200.0, 200.0));
567 cam.follow_target(Vec2::new(500.0, 500.0));
568 cam.update(0.016);
569 assert_eq!(cam.position, Vec2::new(40.0, 56.0));
570 }
571
572 #[test]
577 fn tile_x_returns_correct_column() {
578 let mut cam = Camera::new(160.0, 144.0);
579 cam.position = Vec2::new(0.0, 0.0);
580 assert_eq!(cam.tile_x(), 0);
581
582 cam.position = Vec2::new(8.0, 0.0);
583 assert_eq!(cam.tile_x(), 1);
584
585 cam.position = Vec2::new(15.0, 0.0);
586 assert_eq!(cam.tile_x(), 1); cam.position = Vec2::new(16.0, 0.0);
589 assert_eq!(cam.tile_x(), 2);
590
591 cam.position = Vec2::new(-5.0, 0.0);
592 assert_eq!(cam.tile_x(), -1); }
594
595 #[test]
596 fn tile_y_returns_correct_row() {
597 let mut cam = Camera::new(160.0, 144.0);
598 cam.position = Vec2::new(0.0, 0.0);
599 assert_eq!(cam.tile_y(), 0);
600
601 cam.position = Vec2::new(0.0, 8.0);
602 assert_eq!(cam.tile_y(), 1);
603
604 cam.position = Vec2::new(0.0, 72.0);
605 assert_eq!(cam.tile_y(), 9); }
607
608 #[test]
613 fn is_visible_in_viewport() {
614 let cam = Camera::new(160.0, 144.0);
615 assert!(cam.is_visible(Vec2::new(80.0, 72.0)));
617 assert!(cam.is_visible(Vec2::new(0.0, 0.0)));
619 assert!(cam.is_visible(Vec2::new(-7.0, 0.0))); assert!(!cam.is_visible(Vec2::new(1000.0, 1000.0)));
623 }
624
625 #[test]
626 fn is_rect_visible_intersects() {
627 let cam = Camera::new(160.0, 144.0);
628 assert!(cam.is_rect_visible(Rect::new(0.0, 0.0, 32.0, 32.0)));
629 assert!(cam.is_rect_visible(Rect::new(140.0, 120.0, 32.0, 32.0)));
630 }
631
632 #[test]
633 fn is_rect_visible_far_away() {
634 let cam = Camera::new(160.0, 144.0);
635 assert!(!cam.is_rect_visible(Rect::new(1000.0, 1000.0, 32.0, 32.0)));
636 }
637
638 #[test]
643 fn vec2_new() {
644 let v = Vec2::new(3.0, 4.0);
645 assert_eq!(v.x, 3.0);
646 assert_eq!(v.y, 4.0);
647 }
648
649 #[test]
650 fn rect_new() {
651 let r = Rect::new(1.0, 2.0, 10.0, 20.0);
652 assert_eq!(r.x, 1.0);
653 assert_eq!(r.y, 2.0);
654 assert_eq!(r.w, 10.0);
655 assert_eq!(r.h, 20.0);
656 }
657}