1#![warn(missing_docs)]
36
37use gravita_math::Vec2;
38
39pub fn clear(frame: &mut [u8], color: [u8; 4]) {
41 for px in frame.chunks_exact_mut(4) {
42 px.copy_from_slice(&color);
43 }
44}
45
46pub fn draw_circle(
48 frame: &mut [u8],
49 center: Vec2,
50 radius: f32,
51 color: [u8; 4],
52 width: u32,
53 height: u32,
54) {
55 let cx = center.x.round() as i32;
56 let cy = center.y.round() as i32;
57 let r = radius.round() as i32;
58
59 for y in (cy - r).max(0)..(cy + r).min(height as i32) {
60 for x in (cx - r).max(0)..(cx + r).min(width as i32) {
61 let dx = x - cx;
62 let dy = y - cy;
63 if dx * dx + dy * dy <= r * r {
64 let idx = ((y as u32 * width + x as u32) * 4) as usize;
65 if idx + 3 < frame.len() {
66 frame[idx..idx + 4].copy_from_slice(&color);
67 }
68 }
69 }
70 }
71}
72
73pub fn draw_line(
75 frame: &mut [u8],
76 start: Vec2,
77 end: Vec2,
78 color: [u8; 4],
79 width: u32,
80 height: u32,
81) {
82 let x0 = start.x.round() as i32;
83 let y0 = start.y.round() as i32;
84 let x1 = end.x.round() as i32;
85 let y1 = end.y.round() as i32;
86
87 let dx = (x1 - x0).abs();
88 let dy = (y1 - y0).abs();
89 let sx = if x0 < x1 { 1 } else { -1 };
90 let sy = if y0 < y1 { 1 } else { -1 };
91 let mut err = dx - dy;
92
93 let mut x = x0;
94 let mut y = y0;
95
96 loop {
97 if x >= 0 && x < width as i32 && y >= 0 && y < height as i32 {
98 let idx = ((y as u32 * width + x as u32) * 4) as usize;
99 if idx + 3 < frame.len() {
100 frame[idx..idx + 4].copy_from_slice(&color);
101 }
102 }
103
104 if x == x1 && y == y1 {
105 break;
106 }
107
108 let e2 = 2 * err;
109 if e2 > -dy {
110 err -= dy;
111 x += sx;
112 }
113 if e2 < dx {
114 err += dx;
115 y += sy;
116 }
117 }
118}
119
120pub fn draw_axes(frame: &mut [u8], origin: Vec2, color: [u8; 4], width: u32, height: u32) {
122 let origin = Vec2::new(origin.x, origin.y);
123 draw_line(
125 frame,
126 Vec2::new(0.0, origin.y),
127 Vec2::new(width as f32, origin.y),
128 color,
129 width,
130 height,
131 );
132 draw_line(
134 frame,
135 Vec2::new(origin.x, 0.0),
136 Vec2::new(origin.x, height as f32),
137 color,
138 width,
139 height,
140 );
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 const WIDTH: u32 = 100;
148 const HEIGHT: u32 = 100;
149
150 fn create_frame() -> Vec<u8> {
151 vec![0u8; (WIDTH * HEIGHT * 4) as usize]
152 }
153
154 fn pixel_at(frame: &[u8], x: u32, y: u32) -> [u8; 4] {
155 let idx = ((y * WIDTH + x) * 4) as usize;
156 [frame[idx], frame[idx + 1], frame[idx + 2], frame[idx + 3]]
157 }
158
159 #[test]
164 fn clear_fills_entire_frame() {
165 let mut frame = create_frame();
166 let color = [0x12, 0x34, 0x56, 0x78];
167 clear(&mut frame, color);
168
169 assert_eq!(pixel_at(&frame, 0, 0), color);
171 assert_eq!(pixel_at(&frame, WIDTH - 1, HEIGHT - 1), color);
173 assert_eq!(pixel_at(&frame, 50, 50), color);
175 }
176
177 #[test]
178 fn clear_with_black() {
179 let mut frame = create_frame();
180 clear(&mut frame, [0xFF, 0xFF, 0xFF, 0xFF]);
182 clear(&mut frame, [0x00, 0x00, 0x00, 0xFF]);
184 assert_eq!(pixel_at(&frame, 50, 50), [0x00, 0x00, 0x00, 0xFF]);
185 }
186
187 #[test]
192 fn draw_circle_at_center() {
193 let mut frame = create_frame();
194 let color = [0xFF, 0x00, 0x00, 0xFF];
195 draw_circle(
196 &mut frame,
197 Vec2::new(50.0, 50.0),
198 10.0,
199 color,
200 WIDTH,
201 HEIGHT,
202 );
203
204 assert_eq!(pixel_at(&frame, 50, 50), color);
206 }
207
208 #[test]
209 fn draw_circle_does_not_affect_outside() {
210 let mut frame = create_frame();
211 let color = [0xFF, 0x00, 0x00, 0xFF];
212 draw_circle(&mut frame, Vec2::new(50.0, 50.0), 5.0, color, WIDTH, HEIGHT);
213
214 assert_eq!(pixel_at(&frame, 0, 0), [0, 0, 0, 0]);
216 }
217
218 #[test]
219 fn draw_circle_clipped_at_edge() {
220 let mut frame = create_frame();
221 let color = [0xFF, 0x00, 0x00, 0xFF];
222 draw_circle(
224 &mut frame,
225 Vec2::new(-5.0, 50.0),
226 10.0,
227 color,
228 WIDTH,
229 HEIGHT,
230 );
231
232 assert_eq!(pixel_at(&frame, 0, 50), color);
234 }
235
236 #[test]
237 fn draw_circle_completely_outside_does_not_panic() {
238 let mut frame = create_frame();
239 let color = [0xFF, 0x00, 0x00, 0xFF];
240 draw_circle(
242 &mut frame,
243 Vec2::new(-100.0, -100.0),
244 10.0,
245 color,
246 WIDTH,
247 HEIGHT,
248 );
249 assert_eq!(pixel_at(&frame, 0, 0), [0, 0, 0, 0]);
251 }
252
253 #[test]
254 fn draw_circle_zero_radius() {
255 let mut frame = create_frame();
256 let color = [0xFF, 0x00, 0x00, 0xFF];
257 draw_circle(&mut frame, Vec2::new(50.0, 50.0), 0.0, color, WIDTH, HEIGHT);
259 }
261
262 #[test]
267 fn draw_line_horizontal() {
268 let mut frame = create_frame();
269 let color = [0x00, 0xFF, 0x00, 0xFF];
270 draw_line(
271 &mut frame,
272 Vec2::new(10.0, 50.0),
273 Vec2::new(90.0, 50.0),
274 color,
275 WIDTH,
276 HEIGHT,
277 );
278
279 assert_eq!(pixel_at(&frame, 10, 50), color);
281 assert_eq!(pixel_at(&frame, 90, 50), color);
283 assert_eq!(pixel_at(&frame, 50, 50), color);
285 }
286
287 #[test]
288 fn draw_line_vertical() {
289 let mut frame = create_frame();
290 let color = [0x00, 0xFF, 0x00, 0xFF];
291 draw_line(
292 &mut frame,
293 Vec2::new(50.0, 10.0),
294 Vec2::new(50.0, 90.0),
295 color,
296 WIDTH,
297 HEIGHT,
298 );
299
300 assert_eq!(pixel_at(&frame, 50, 10), color);
301 assert_eq!(pixel_at(&frame, 50, 90), color);
302 assert_eq!(pixel_at(&frame, 50, 50), color);
303 }
304
305 #[test]
306 fn draw_line_diagonal() {
307 let mut frame = create_frame();
308 let color = [0x00, 0xFF, 0x00, 0xFF];
309 draw_line(
310 &mut frame,
311 Vec2::new(10.0, 10.0),
312 Vec2::new(90.0, 90.0),
313 color,
314 WIDTH,
315 HEIGHT,
316 );
317
318 assert_eq!(pixel_at(&frame, 10, 10), color);
320 assert_eq!(pixel_at(&frame, 90, 90), color);
321 }
322
323 #[test]
324 fn draw_line_clipped_does_not_panic() {
325 let mut frame = create_frame();
326 let color = [0x00, 0xFF, 0x00, 0xFF];
327 draw_line(
329 &mut frame,
330 Vec2::new(-50.0, 50.0),
331 Vec2::new(150.0, 50.0),
332 color,
333 WIDTH,
334 HEIGHT,
335 );
336 assert_eq!(pixel_at(&frame, 0, 50), color);
338 assert_eq!(pixel_at(&frame, 99, 50), color);
339 }
340
341 #[test]
342 fn draw_line_completely_outside_does_not_panic() {
343 let mut frame = create_frame();
344 let color = [0x00, 0xFF, 0x00, 0xFF];
345 draw_line(
346 &mut frame,
347 Vec2::new(-50.0, -50.0),
348 Vec2::new(-10.0, -10.0),
349 color,
350 WIDTH,
351 HEIGHT,
352 );
353 assert_eq!(pixel_at(&frame, 0, 0), [0, 0, 0, 0]);
355 }
356
357 #[test]
358 fn draw_line_single_point() {
359 let mut frame = create_frame();
360 let color = [0x00, 0xFF, 0x00, 0xFF];
361 draw_line(
363 &mut frame,
364 Vec2::new(50.0, 50.0),
365 Vec2::new(50.0, 50.0),
366 color,
367 WIDTH,
368 HEIGHT,
369 );
370 assert_eq!(pixel_at(&frame, 50, 50), color);
371 }
372
373 #[test]
378 fn draw_axes_draws_cross() {
379 let mut frame = create_frame();
380 let color = [0x00, 0x00, 0xFF, 0xFF];
381 draw_axes(&mut frame, Vec2::new(50.0, 50.0), color, WIDTH, HEIGHT);
382
383 assert_eq!(pixel_at(&frame, 0, 50), color);
385 assert_eq!(pixel_at(&frame, 99, 50), color);
386 assert_eq!(pixel_at(&frame, 50, 0), color);
388 assert_eq!(pixel_at(&frame, 50, 99), color);
389 }
390
391 #[test]
392 fn draw_axes_at_origin() {
393 let mut frame = create_frame();
394 let color = [0x00, 0x00, 0xFF, 0xFF];
395 draw_axes(&mut frame, Vec2::new(0.0, 0.0), color, WIDTH, HEIGHT);
396 }
398
399 fn count_pixels_with_color(frame: &[u8], color: [u8; 4]) -> usize {
405 frame.chunks_exact(4).filter(|px| px == &color).count()
406 }
407
408 #[allow(dead_code)]
410 fn is_pixel_colored(frame: &[u8], x: u32, y: u32, width: u32) -> bool {
411 let idx = ((y * width + x) * 4) as usize;
412 frame[idx] != 0 || frame[idx + 1] != 0 || frame[idx + 2] != 0 || frame[idx + 3] != 0
413 }
414
415 #[test]
416 fn visual_circle_fills_approximate_area() {
417 let mut frame = create_frame();
418 let color = [0xFF, 0x00, 0x00, 0xFF];
419 let radius = 10.0;
420 draw_circle(
421 &mut frame,
422 Vec2::new(50.0, 50.0),
423 radius,
424 color,
425 WIDTH,
426 HEIGHT,
427 );
428
429 let filled_pixels = count_pixels_with_color(&frame, color);
430 let expected_area = std::f32::consts::PI * radius * radius;
433 let tolerance = expected_area * 0.15; assert!(
436 (filled_pixels as f32 - expected_area).abs() < tolerance,
437 "Circle area mismatch: got {filled_pixels} pixels, expected ~{expected_area:.0} \
438 (±{tolerance:.0})"
439 );
440 }
441
442 #[test]
443 fn visual_circle_is_symmetric() {
444 let mut frame = create_frame();
445 let color = [0xFF, 0x00, 0x00, 0xFF];
446 draw_circle(
447 &mut frame,
448 Vec2::new(50.0, 50.0),
449 15.0,
450 color,
451 WIDTH,
452 HEIGHT,
453 );
454
455 for offset in 1..=14 {
457 let left = pixel_at(&frame, 50 - offset, 50);
458 let right = pixel_at(&frame, 50 + offset, 50);
459 assert_eq!(left, right, "Horizontal asymmetry at offset {offset}");
460 }
461
462 for offset in 1..=14 {
464 let up = pixel_at(&frame, 50, 50 - offset);
465 let down = pixel_at(&frame, 50, 50 + offset);
466 assert_eq!(up, down, "Vertical asymmetry at offset {offset}");
467 }
468 }
469
470 #[test]
471 fn visual_circle_boundary_is_correct() {
472 let mut frame = create_frame();
473 let color = [0xFF, 0x00, 0x00, 0xFF];
474 let radius = 10.0;
475 draw_circle(
476 &mut frame,
477 Vec2::new(50.0, 50.0),
478 radius,
479 color,
480 WIDTH,
481 HEIGHT,
482 );
483
484 assert_eq!(pixel_at(&frame, 50, 41), color); assert_eq!(pixel_at(&frame, 50, 59), color); assert_eq!(pixel_at(&frame, 41, 50), color); assert_eq!(pixel_at(&frame, 59, 50), color); assert_eq!(pixel_at(&frame, 50, 38), [0, 0, 0, 0]); assert_eq!(pixel_at(&frame, 50, 62), [0, 0, 0, 0]); }
494
495 #[test]
496 fn visual_line_is_continuous() {
497 let mut frame = create_frame();
498 let color = [0x00, 0xFF, 0x00, 0xFF];
499 draw_line(
500 &mut frame,
501 Vec2::new(10.0, 10.0),
502 Vec2::new(90.0, 90.0),
503 color,
504 WIDTH,
505 HEIGHT,
506 );
507
508 let mut colored_count = 0;
511 for i in 10..=90 {
512 if pixel_at(&frame, i, i) == color {
515 colored_count += 1;
516 }
517 }
518
519 assert!(
521 colored_count >= 65,
522 "Line should be continuous: only {colored_count} of 81 pixels colored"
523 );
524 }
525
526 #[test]
527 fn visual_line_no_gaps_horizontal() {
528 let mut frame = create_frame();
529 let color = [0x00, 0xFF, 0x00, 0xFF];
530 draw_line(
531 &mut frame,
532 Vec2::new(10.0, 50.0),
533 Vec2::new(90.0, 50.0),
534 color,
535 WIDTH,
536 HEIGHT,
537 );
538
539 for x in 10..=90 {
541 assert_eq!(
542 pixel_at(&frame, x, 50),
543 color,
544 "Gap in horizontal line at x={x}"
545 );
546 }
547 }
548
549 #[test]
550 fn visual_line_no_gaps_vertical() {
551 let mut frame = create_frame();
552 let color = [0x00, 0xFF, 0x00, 0xFF];
553 draw_line(
554 &mut frame,
555 Vec2::new(50.0, 10.0),
556 Vec2::new(50.0, 90.0),
557 color,
558 WIDTH,
559 HEIGHT,
560 );
561
562 for y in 10..=90 {
564 assert_eq!(
565 pixel_at(&frame, 50, y),
566 color,
567 "Gap in vertical line at y={y}"
568 );
569 }
570 }
571
572 #[test]
573 fn visual_line_steep_slope_continuous() {
574 let mut frame = create_frame();
575 let color = [0x00, 0xFF, 0x00, 0xFF];
576 draw_line(
578 &mut frame,
579 Vec2::new(45.0, 10.0),
580 Vec2::new(55.0, 90.0),
581 color,
582 WIDTH,
583 HEIGHT,
584 );
585
586 let filled = count_pixels_with_color(&frame, color);
588 assert!(
590 filled >= 75,
591 "Steep line should have ~80 pixels, got {filled}"
592 );
593 }
594
595 #[test]
596 fn visual_color_channels_independent() {
597 let mut frame = create_frame();
598
599 draw_circle(
601 &mut frame,
602 Vec2::new(25.0, 50.0),
603 5.0,
604 [0xFF, 0x00, 0x00, 0xFF],
605 WIDTH,
606 HEIGHT,
607 );
608 draw_circle(
610 &mut frame,
611 Vec2::new(50.0, 50.0),
612 5.0,
613 [0x00, 0xFF, 0x00, 0xFF],
614 WIDTH,
615 HEIGHT,
616 );
617 draw_circle(
619 &mut frame,
620 Vec2::new(75.0, 50.0),
621 5.0,
622 [0x00, 0x00, 0xFF, 0xFF],
623 WIDTH,
624 HEIGHT,
625 );
626
627 let red_px = pixel_at(&frame, 25, 50);
629 let green_px = pixel_at(&frame, 50, 50);
630 let blue_px = pixel_at(&frame, 75, 50);
631
632 assert_eq!(red_px, [0xFF, 0x00, 0x00, 0xFF], "Red channel incorrect");
633 assert_eq!(
634 green_px,
635 [0x00, 0xFF, 0x00, 0xFF],
636 "Green channel incorrect"
637 );
638 assert_eq!(blue_px, [0x00, 0x00, 0xFF, 0xFF], "Blue channel incorrect");
639 }
640
641 #[test]
642 fn visual_alpha_channel_preserved() {
643 let mut frame = create_frame();
644 let semi_transparent = [0xFF, 0x00, 0x00, 0x80]; draw_circle(
646 &mut frame,
647 Vec2::new(50.0, 50.0),
648 10.0,
649 semi_transparent,
650 WIDTH,
651 HEIGHT,
652 );
653
654 let px = pixel_at(&frame, 50, 50);
655 assert_eq!(px[3], 0x80, "Alpha channel not preserved");
656 }
657
658 #[test]
659 fn visual_overdraw_replaces_pixels() {
660 let mut frame = create_frame();
661
662 draw_circle(
664 &mut frame,
665 Vec2::new(50.0, 50.0),
666 20.0,
667 [0xFF, 0x00, 0x00, 0xFF],
668 WIDTH,
669 HEIGHT,
670 );
671 draw_circle(
673 &mut frame,
674 Vec2::new(50.0, 50.0),
675 10.0,
676 [0x00, 0x00, 0xFF, 0xFF],
677 WIDTH,
678 HEIGHT,
679 );
680
681 assert_eq!(pixel_at(&frame, 50, 50), [0x00, 0x00, 0xFF, 0xFF]);
683 assert_eq!(pixel_at(&frame, 50, 35), [0xFF, 0x00, 0x00, 0xFF]);
685 }
686
687 #[test]
688 fn visual_multiple_shapes_composite_correctly() {
689 let mut frame = create_frame();
690 let bg = [0x20, 0x20, 0x20, 0xFF];
691 let circle_color = [0xFF, 0x00, 0x00, 0xFF];
692 let line_color = [0x00, 0xFF, 0x00, 0xFF];
693
694 clear(&mut frame, bg);
695 draw_circle(
696 &mut frame,
697 Vec2::new(50.0, 50.0),
698 30.0,
699 circle_color,
700 WIDTH,
701 HEIGHT,
702 );
703 draw_line(
704 &mut frame,
705 Vec2::new(0.0, 50.0),
706 Vec2::new(99.0, 50.0),
707 line_color,
708 WIDTH,
709 HEIGHT,
710 );
711
712 assert_eq!(pixel_at(&frame, 50, 50), line_color);
714 assert_eq!(pixel_at(&frame, 50, 30), circle_color);
716 assert_eq!(pixel_at(&frame, 5, 5), bg);
718 }
719
720 #[test]
721 fn visual_frame_buffer_integrity() {
722 let mut frame = create_frame();
723 let original_len = frame.len();
724
725 clear(&mut frame, [0x10, 0x20, 0x30, 0xFF]);
727 draw_circle(
728 &mut frame,
729 Vec2::new(50.0, 50.0),
730 25.0,
731 [0xFF, 0x00, 0x00, 0xFF],
732 WIDTH,
733 HEIGHT,
734 );
735 draw_line(
736 &mut frame,
737 Vec2::new(0.0, 0.0),
738 Vec2::new(99.0, 99.0),
739 [0x00, 0xFF, 0x00, 0xFF],
740 WIDTH,
741 HEIGHT,
742 );
743 draw_axes(
744 &mut frame,
745 Vec2::new(50.0, 50.0),
746 [0x00, 0x00, 0xFF, 0xFF],
747 WIDTH,
748 HEIGHT,
749 );
750
751 assert_eq!(
753 frame.len(),
754 original_len,
755 "Frame buffer size changed during rendering"
756 );
757
758 for (i, chunk) in frame.chunks_exact(4).enumerate() {
760 assert_eq!(chunk.len(), 4, "Pixel {i} should have exactly 4 bytes");
761 }
762 }
763}