1#![allow(clippy::module_name_repetitions)]
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct Color {
14 pub r: f32,
16 pub g: f32,
18 pub b: f32,
20 pub a: f32,
22}
23
24impl Color {
25 #[must_use]
27 pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
28 Self { r, g, b, a }
29 }
30
31 #[must_use]
33 pub const fn from_array(rgba: [f32; 4]) -> Self {
34 Self {
35 r: rgba[0],
36 g: rgba[1],
37 b: rgba[2],
38 a: rgba[3],
39 }
40 }
41
42 #[must_use]
44 pub const fn to_array(self) -> [f32; 4] {
45 [self.r, self.g, self.b, self.a]
46 }
47
48 #[must_use]
50 pub fn to_css_rgba(self) -> String {
51 format!(
52 "rgba({}, {}, {}, {})",
53 (self.r * 255.0) as u8,
54 (self.g * 255.0) as u8,
55 (self.b * 255.0) as u8,
56 self.a
57 )
58 }
59
60 pub const BLACK: Self = Self::new(0.0, 0.0, 0.0, 1.0);
62
63 pub const WHITE: Self = Self::new(1.0, 1.0, 1.0, 1.0);
65
66 pub const TRANSPARENT: Self = Self::new(0.0, 0.0, 0.0, 0.0);
68
69 pub const RED: Self = Self::new(1.0, 0.0, 0.0, 1.0);
71
72 pub const GREEN: Self = Self::new(0.0, 1.0, 0.0, 1.0);
74
75 pub const BLUE: Self = Self::new(0.0, 0.0, 1.0, 1.0);
77}
78
79impl Default for Color {
80 fn default() -> Self {
81 Self::BLACK
82 }
83}
84
85impl From<[f32; 4]> for Color {
86 fn from(rgba: [f32; 4]) -> Self {
87 Self::from_array(rgba)
88 }
89}
90
91impl From<Color> for [f32; 4] {
92 fn from(color: Color) -> Self {
93 color.to_array()
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
99#[serde(rename_all = "lowercase")]
100pub enum TextAlign {
101 #[default]
103 Left,
104 Center,
106 Right,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
112#[serde(rename_all = "lowercase")]
113pub enum TextBaseline {
114 Top,
116 #[default]
118 Middle,
119 Bottom,
121 Alphabetic,
123}
124
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130#[serde(tag = "type")]
131pub enum Canvas2DCommand {
132 Clear {
134 color: Color,
136 },
137
138 FillRect {
140 x: f32,
142 y: f32,
144 width: f32,
146 height: f32,
148 color: Color,
150 },
151
152 StrokeRect {
154 x: f32,
156 y: f32,
158 width: f32,
160 height: f32,
162 color: Color,
164 line_width: f32,
166 },
167
168 FillCircle {
170 x: f32,
172 y: f32,
174 radius: f32,
176 color: Color,
178 },
179
180 StrokeCircle {
182 x: f32,
184 y: f32,
186 radius: f32,
188 color: Color,
190 line_width: f32,
192 },
193
194 Line {
196 x1: f32,
198 y1: f32,
200 x2: f32,
202 y2: f32,
204 color: Color,
206 line_width: f32,
208 },
209
210 FillText {
212 text: String,
214 x: f32,
216 y: f32,
218 font: String,
220 color: Color,
222 align: TextAlign,
224 baseline: TextBaseline,
226 },
227
228 DrawImage {
230 texture_id: u32,
232 x: f32,
234 y: f32,
236 width: f32,
238 height: f32,
240 },
241
242 DrawImageSlice {
244 texture_id: u32,
246 src_x: f32,
248 src_y: f32,
250 src_width: f32,
252 src_height: f32,
254 dst_x: f32,
256 dst_y: f32,
258 dst_width: f32,
260 dst_height: f32,
262 },
263
264 Save,
266
267 Restore,
269
270 Translate {
272 x: f32,
274 y: f32,
276 },
277
278 Rotate {
280 angle: f32,
282 },
283
284 Scale {
286 x: f32,
288 y: f32,
290 },
291
292 SetAlpha {
294 alpha: f32,
296 },
297}
298
299#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
301pub struct RenderFrame {
302 pub commands: Vec<Canvas2DCommand>,
304}
305
306impl RenderFrame {
307 #[must_use]
309 pub fn new() -> Self {
310 Self::default()
311 }
312
313 #[must_use]
315 pub fn with_capacity(capacity: usize) -> Self {
316 Self {
317 commands: Vec::with_capacity(capacity),
318 }
319 }
320
321 pub fn clear(&mut self) {
323 self.commands.clear();
324 }
325
326 pub fn push(&mut self, cmd: Canvas2DCommand) {
328 self.commands.push(cmd);
329 }
330
331 pub fn clear_screen(&mut self, color: Color) {
333 self.push(Canvas2DCommand::Clear { color });
334 }
335
336 pub fn fill_rect(&mut self, x: f32, y: f32, width: f32, height: f32, color: Color) {
338 self.push(Canvas2DCommand::FillRect {
339 x,
340 y,
341 width,
342 height,
343 color,
344 });
345 }
346
347 pub fn fill_circle(&mut self, x: f32, y: f32, radius: f32, color: Color) {
349 self.push(Canvas2DCommand::FillCircle {
350 x,
351 y,
352 radius,
353 color,
354 });
355 }
356
357 pub fn fill_text(&mut self, text: &str, x: f32, y: f32, font: &str, color: Color) {
359 self.push(Canvas2DCommand::FillText {
360 text: text.to_string(),
361 x,
362 y,
363 font: font.to_string(),
364 color,
365 align: TextAlign::default(),
366 baseline: TextBaseline::default(),
367 });
368 }
369
370 #[allow(clippy::too_many_arguments)]
372 pub fn fill_text_aligned(
373 &mut self,
374 text: &str,
375 x: f32,
376 y: f32,
377 font: &str,
378 color: Color,
379 align: TextAlign,
380 baseline: TextBaseline,
381 ) {
382 self.push(Canvas2DCommand::FillText {
383 text: text.to_string(),
384 x,
385 y,
386 font: font.to_string(),
387 color,
388 align,
389 baseline,
390 });
391 }
392
393 pub fn line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: Color, line_width: f32) {
395 self.push(Canvas2DCommand::Line {
396 x1,
397 y1,
398 x2,
399 y2,
400 color,
401 line_width,
402 });
403 }
404
405 pub fn stroke_rect(
407 &mut self,
408 x: f32,
409 y: f32,
410 width: f32,
411 height: f32,
412 color: Color,
413 line_width: f32,
414 ) {
415 self.push(Canvas2DCommand::StrokeRect {
416 x,
417 y,
418 width,
419 height,
420 color,
421 line_width,
422 });
423 }
424
425 #[must_use]
427 #[allow(clippy::missing_const_for_fn)] pub fn len(&self) -> usize {
429 self.commands.len()
430 }
431
432 #[must_use]
434 #[allow(clippy::missing_const_for_fn)] pub fn is_empty(&self) -> bool {
436 self.commands.is_empty()
437 }
438
439 pub fn to_json(&self) -> Result<String, serde_json::Error> {
445 serde_json::to_string(&self.commands)
446 }
447
448 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
454 serde_json::to_string_pretty(&self.commands)
455 }
456}
457
458#[must_use]
462#[allow(clippy::missing_const_for_fn)]
463pub fn convert_render_command(cmd: &jugar_render::RenderCommand) -> Option<Canvas2DCommand> {
464 match cmd {
465 jugar_render::RenderCommand::Clear { color } => Some(Canvas2DCommand::Clear {
466 color: Color::from_array(*color),
467 }),
468 jugar_render::RenderCommand::DrawRect { rect, color } => Some(Canvas2DCommand::FillRect {
469 x: rect.x,
470 y: rect.y,
471 width: rect.width,
472 height: rect.height,
473 color: Color::from_array(*color),
474 }),
475 jugar_render::RenderCommand::DrawSprite { .. } => {
476 None
478 }
479 }
480}
481
482#[must_use]
484pub fn convert_render_queue(commands: &[jugar_render::RenderCommand]) -> RenderFrame {
485 let mut frame = RenderFrame::with_capacity(commands.len());
486 for cmd in commands {
487 if let Some(canvas_cmd) = convert_render_command(cmd) {
488 frame.push(canvas_cmd);
489 }
490 }
491 frame
492}
493
494#[cfg(test)]
495#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn test_color_new() {
501 let color = Color::new(0.5, 0.25, 0.75, 1.0);
502 assert!((color.r - 0.5).abs() < f32::EPSILON);
503 assert!((color.g - 0.25).abs() < f32::EPSILON);
504 assert!((color.b - 0.75).abs() < f32::EPSILON);
505 assert!((color.a - 1.0).abs() < f32::EPSILON);
506 }
507
508 #[test]
509 fn test_color_from_array() {
510 let color = Color::from_array([0.1, 0.2, 0.3, 0.4]);
511 assert!((color.r - 0.1).abs() < f32::EPSILON);
512 assert!((color.g - 0.2).abs() < f32::EPSILON);
513 assert!((color.b - 0.3).abs() < f32::EPSILON);
514 assert!((color.a - 0.4).abs() < f32::EPSILON);
515 }
516
517 #[test]
518 fn test_color_to_array() {
519 let color = Color::new(0.1, 0.2, 0.3, 0.4);
520 let arr = color.to_array();
521 assert!((arr[0] - 0.1).abs() < f32::EPSILON);
522 assert!((arr[1] - 0.2).abs() < f32::EPSILON);
523 assert!((arr[2] - 0.3).abs() < f32::EPSILON);
524 assert!((arr[3] - 0.4).abs() < f32::EPSILON);
525 }
526
527 #[test]
528 fn test_color_to_css_rgba() {
529 let color = Color::new(1.0, 0.5, 0.0, 0.8);
530 let css = color.to_css_rgba();
531 assert_eq!(css, "rgba(255, 127, 0, 0.8)");
532 }
533
534 #[test]
535 fn test_color_constants() {
536 assert_eq!(Color::BLACK, Color::new(0.0, 0.0, 0.0, 1.0));
537 assert_eq!(Color::WHITE, Color::new(1.0, 1.0, 1.0, 1.0));
538 assert_eq!(Color::RED, Color::new(1.0, 0.0, 0.0, 1.0));
539 assert_eq!(Color::GREEN, Color::new(0.0, 1.0, 0.0, 1.0));
540 assert_eq!(Color::BLUE, Color::new(0.0, 0.0, 1.0, 1.0));
541 assert_eq!(Color::TRANSPARENT, Color::new(0.0, 0.0, 0.0, 0.0));
542 }
543
544 #[test]
545 fn test_color_default() {
546 let color = Color::default();
547 assert_eq!(color, Color::BLACK);
548 }
549
550 #[test]
551 fn test_color_from_trait() {
552 let color: Color = [0.5, 0.5, 0.5, 1.0].into();
553 assert!((color.r - 0.5).abs() < f32::EPSILON);
554 }
555
556 #[test]
557 fn test_color_into_array() {
558 let color = Color::new(0.5, 0.5, 0.5, 1.0);
559 let arr: [f32; 4] = color.into();
560 assert!((arr[0] - 0.5).abs() < f32::EPSILON);
561 }
562
563 #[test]
564 fn test_text_align_default() {
565 assert_eq!(TextAlign::default(), TextAlign::Left);
566 }
567
568 #[test]
569 fn test_text_baseline_default() {
570 assert_eq!(TextBaseline::default(), TextBaseline::Middle);
571 }
572
573 #[test]
574 fn test_canvas2d_command_clear_serialization() {
575 let cmd = Canvas2DCommand::Clear {
576 color: Color::BLACK,
577 };
578 let json = serde_json::to_string(&cmd).unwrap();
579 assert!(json.contains("\"type\":\"Clear\""));
580
581 let deserialized: Canvas2DCommand = serde_json::from_str(&json).unwrap();
582 assert_eq!(cmd, deserialized);
583 }
584
585 #[test]
586 fn test_canvas2d_command_fill_rect_serialization() {
587 let cmd = Canvas2DCommand::FillRect {
588 x: 10.0,
589 y: 20.0,
590 width: 100.0,
591 height: 50.0,
592 color: Color::WHITE,
593 };
594 let json = serde_json::to_string(&cmd).unwrap();
595 assert!(json.contains("\"type\":\"FillRect\""));
596 assert!(json.contains("\"x\":10"));
597 assert!(json.contains("\"width\":100"));
598
599 let deserialized: Canvas2DCommand = serde_json::from_str(&json).unwrap();
600 assert_eq!(cmd, deserialized);
601 }
602
603 #[test]
604 fn test_canvas2d_command_fill_circle_serialization() {
605 let cmd = Canvas2DCommand::FillCircle {
606 x: 50.0,
607 y: 50.0,
608 radius: 25.0,
609 color: Color::RED,
610 };
611 let json = serde_json::to_string(&cmd).unwrap();
612 assert!(json.contains("\"type\":\"FillCircle\""));
613 assert!(json.contains("\"radius\":25"));
614
615 let deserialized: Canvas2DCommand = serde_json::from_str(&json).unwrap();
616 assert_eq!(cmd, deserialized);
617 }
618
619 #[test]
620 fn test_canvas2d_command_stroke_rect_serialization() {
621 let cmd = Canvas2DCommand::StrokeRect {
622 x: 0.0,
623 y: 0.0,
624 width: 200.0,
625 height: 150.0,
626 color: Color::GREEN,
627 line_width: 2.0,
628 };
629 let json = serde_json::to_string(&cmd).unwrap();
630 assert!(json.contains("\"type\":\"StrokeRect\""));
631 assert!(json.contains("\"line_width\":2"));
632
633 let deserialized: Canvas2DCommand = serde_json::from_str(&json).unwrap();
634 assert_eq!(cmd, deserialized);
635 }
636
637 #[test]
638 fn test_canvas2d_command_stroke_circle_serialization() {
639 let cmd = Canvas2DCommand::StrokeCircle {
640 x: 100.0,
641 y: 100.0,
642 radius: 50.0,
643 color: Color::BLUE,
644 line_width: 3.0,
645 };
646 let json = serde_json::to_string(&cmd).unwrap();
647 assert!(json.contains("\"type\":\"StrokeCircle\""));
648
649 let deserialized: Canvas2DCommand = serde_json::from_str(&json).unwrap();
650 assert_eq!(cmd, deserialized);
651 }
652
653 #[test]
654 fn test_canvas2d_command_line_serialization() {
655 let cmd = Canvas2DCommand::Line {
656 x1: 0.0,
657 y1: 0.0,
658 x2: 100.0,
659 y2: 100.0,
660 color: Color::WHITE,
661 line_width: 1.0,
662 };
663 let json = serde_json::to_string(&cmd).unwrap();
664 assert!(json.contains("\"type\":\"Line\""));
665 assert!(json.contains("\"x1\":0"));
666 assert!(json.contains("\"x2\":100"));
667
668 let deserialized: Canvas2DCommand = serde_json::from_str(&json).unwrap();
669 assert_eq!(cmd, deserialized);
670 }
671
672 #[test]
673 fn test_canvas2d_command_fill_text_serialization() {
674 let cmd = Canvas2DCommand::FillText {
675 text: "Score: 42".to_string(),
676 x: 10.0,
677 y: 30.0,
678 font: "24px monospace".to_string(),
679 color: Color::WHITE,
680 align: TextAlign::Left,
681 baseline: TextBaseline::Top,
682 };
683 let json = serde_json::to_string(&cmd).unwrap();
684 assert!(json.contains("\"type\":\"FillText\""));
685 assert!(json.contains("\"text\":\"Score: 42\""));
686 assert!(json.contains("\"font\":\"24px monospace\""));
687 assert!(json.contains("\"align\":\"left\""));
688 assert!(json.contains("\"baseline\":\"top\""));
689
690 let deserialized: Canvas2DCommand = serde_json::from_str(&json).unwrap();
691 assert_eq!(cmd, deserialized);
692 }
693
694 #[test]
695 fn test_canvas2d_command_draw_image_serialization() {
696 let cmd = Canvas2DCommand::DrawImage {
697 texture_id: 0,
698 x: 100.0,
699 y: 100.0,
700 width: 64.0,
701 height: 64.0,
702 };
703 let json = serde_json::to_string(&cmd).unwrap();
704 assert!(json.contains("\"type\":\"DrawImage\""));
705 assert!(json.contains("\"texture_id\":0"));
706
707 let deserialized: Canvas2DCommand = serde_json::from_str(&json).unwrap();
708 assert_eq!(cmd, deserialized);
709 }
710
711 #[test]
712 fn test_canvas2d_command_draw_image_slice_serialization() {
713 let cmd = Canvas2DCommand::DrawImageSlice {
714 texture_id: 1,
715 src_x: 0.0,
716 src_y: 0.0,
717 src_width: 32.0,
718 src_height: 32.0,
719 dst_x: 200.0,
720 dst_y: 200.0,
721 dst_width: 64.0,
722 dst_height: 64.0,
723 };
724 let json = serde_json::to_string(&cmd).unwrap();
725 assert!(json.contains("\"type\":\"DrawImageSlice\""));
726 assert!(json.contains("\"src_width\":32"));
727 assert!(json.contains("\"dst_width\":64"));
728
729 let deserialized: Canvas2DCommand = serde_json::from_str(&json).unwrap();
730 assert_eq!(cmd, deserialized);
731 }
732
733 #[test]
734 fn test_canvas2d_command_transform_serialization() {
735 let save = Canvas2DCommand::Save;
737 let json = serde_json::to_string(&save).unwrap();
738 assert!(json.contains("\"type\":\"Save\""));
739
740 let restore = Canvas2DCommand::Restore;
742 let json = serde_json::to_string(&restore).unwrap();
743 assert!(json.contains("\"type\":\"Restore\""));
744
745 let translate = Canvas2DCommand::Translate { x: 50.0, y: 100.0 };
747 let json = serde_json::to_string(&translate).unwrap();
748 assert!(json.contains("\"type\":\"Translate\""));
749
750 let rotate = Canvas2DCommand::Rotate {
752 angle: core::f32::consts::PI,
753 };
754 let json = serde_json::to_string(&rotate).unwrap();
755 assert!(json.contains("\"type\":\"Rotate\""));
756
757 let scale = Canvas2DCommand::Scale { x: 2.0, y: 2.0 };
759 let json = serde_json::to_string(&scale).unwrap();
760 assert!(json.contains("\"type\":\"Scale\""));
761 }
762
763 #[test]
764 fn test_canvas2d_command_set_alpha_serialization() {
765 let cmd = Canvas2DCommand::SetAlpha { alpha: 0.5 };
766 let json = serde_json::to_string(&cmd).unwrap();
767 assert!(json.contains("\"type\":\"SetAlpha\""));
768 assert!(json.contains("\"alpha\":0.5"));
769
770 let deserialized: Canvas2DCommand = serde_json::from_str(&json).unwrap();
771 assert_eq!(cmd, deserialized);
772 }
773
774 #[test]
775 fn test_render_frame_new() {
776 let frame = RenderFrame::new();
777 assert!(frame.is_empty());
778 assert_eq!(frame.len(), 0);
779 }
780
781 #[test]
782 fn test_render_frame_with_capacity() {
783 let frame = RenderFrame::with_capacity(100);
784 assert!(frame.is_empty());
785 assert!(frame.commands.capacity() >= 100);
786 }
787
788 #[test]
789 fn test_render_frame_push() {
790 let mut frame = RenderFrame::new();
791 frame.push(Canvas2DCommand::Clear {
792 color: Color::BLACK,
793 });
794 assert_eq!(frame.len(), 1);
795 assert!(!frame.is_empty());
796 }
797
798 #[test]
799 fn test_render_frame_clear() {
800 let mut frame = RenderFrame::new();
801 frame.push(Canvas2DCommand::Clear {
802 color: Color::BLACK,
803 });
804 frame.clear();
805 assert!(frame.is_empty());
806 }
807
808 #[test]
809 fn test_render_frame_clear_screen() {
810 let mut frame = RenderFrame::new();
811 frame.clear_screen(Color::BLACK);
812 assert_eq!(frame.len(), 1);
813 assert_eq!(
814 frame.commands[0],
815 Canvas2DCommand::Clear {
816 color: Color::BLACK
817 }
818 );
819 }
820
821 #[test]
822 fn test_render_frame_fill_rect() {
823 let mut frame = RenderFrame::new();
824 frame.fill_rect(10.0, 20.0, 100.0, 50.0, Color::WHITE);
825 assert_eq!(frame.len(), 1);
826 match &frame.commands[0] {
827 Canvas2DCommand::FillRect {
828 x,
829 y,
830 width,
831 height,
832 ..
833 } => {
834 assert!((x - 10.0).abs() < f32::EPSILON);
835 assert!((y - 20.0).abs() < f32::EPSILON);
836 assert!((width - 100.0).abs() < f32::EPSILON);
837 assert!((height - 50.0).abs() < f32::EPSILON);
838 }
839 _ => panic!("Expected FillRect"),
840 }
841 }
842
843 #[test]
844 fn test_render_frame_fill_circle() {
845 let mut frame = RenderFrame::new();
846 frame.fill_circle(50.0, 50.0, 25.0, Color::RED);
847 assert_eq!(frame.len(), 1);
848 match &frame.commands[0] {
849 Canvas2DCommand::FillCircle { x, y, radius, .. } => {
850 assert!((x - 50.0).abs() < f32::EPSILON);
851 assert!((y - 50.0).abs() < f32::EPSILON);
852 assert!((radius - 25.0).abs() < f32::EPSILON);
853 }
854 _ => panic!("Expected FillCircle"),
855 }
856 }
857
858 #[test]
859 fn test_render_frame_fill_text() {
860 let mut frame = RenderFrame::new();
861 frame.fill_text("Hello", 10.0, 20.0, "24px sans-serif", Color::WHITE);
862 assert_eq!(frame.len(), 1);
863 match &frame.commands[0] {
864 Canvas2DCommand::FillText { text, font, .. } => {
865 assert_eq!(text, "Hello");
866 assert_eq!(font, "24px sans-serif");
867 }
868 _ => panic!("Expected FillText"),
869 }
870 }
871
872 #[test]
873 fn test_render_frame_fill_text_aligned() {
874 let mut frame = RenderFrame::new();
875 frame.fill_text_aligned(
876 "Centered",
877 100.0,
878 50.0,
879 "32px monospace",
880 Color::WHITE,
881 TextAlign::Center,
882 TextBaseline::Middle,
883 );
884 assert_eq!(frame.len(), 1);
885 match &frame.commands[0] {
886 Canvas2DCommand::FillText {
887 align, baseline, ..
888 } => {
889 assert_eq!(*align, TextAlign::Center);
890 assert_eq!(*baseline, TextBaseline::Middle);
891 }
892 _ => panic!("Expected FillText"),
893 }
894 }
895
896 #[test]
897 fn test_render_frame_line() {
898 let mut frame = RenderFrame::new();
899 frame.line(0.0, 0.0, 100.0, 100.0, Color::WHITE, 2.0);
900 assert_eq!(frame.len(), 1);
901 match &frame.commands[0] {
902 Canvas2DCommand::Line {
903 x1,
904 y1,
905 x2,
906 y2,
907 line_width,
908 ..
909 } => {
910 assert!((x1 - 0.0).abs() < f32::EPSILON);
911 assert!((y1 - 0.0).abs() < f32::EPSILON);
912 assert!((x2 - 100.0).abs() < f32::EPSILON);
913 assert!((y2 - 100.0).abs() < f32::EPSILON);
914 assert!((line_width - 2.0).abs() < f32::EPSILON);
915 }
916 _ => panic!("Expected Line"),
917 }
918 }
919
920 #[test]
921 fn test_render_frame_to_json() {
922 let mut frame = RenderFrame::new();
923 frame.clear_screen(Color::BLACK);
924 frame.fill_rect(50.0, 200.0, 20.0, 120.0, Color::WHITE);
925
926 let json = frame.to_json().unwrap();
927 assert!(json.contains("\"type\":\"Clear\""));
928 assert!(json.contains("\"type\":\"FillRect\""));
929 }
930
931 #[test]
932 fn test_render_frame_to_json_pretty() {
933 let mut frame = RenderFrame::new();
934 frame.clear_screen(Color::BLACK);
935
936 let json = frame.to_json_pretty().unwrap();
937 assert!(json.contains('\n')); }
939
940 #[test]
941 fn test_render_frame_default() {
942 let frame = RenderFrame::default();
943 assert!(frame.is_empty());
944 }
945
946 #[test]
947 fn test_pong_like_frame() {
948 let mut frame = RenderFrame::new();
950
951 frame.clear_screen(Color::BLACK);
953
954 frame.line(400.0, 0.0, 400.0, 600.0, Color::WHITE, 2.0);
956
957 frame.fill_rect(20.0, 250.0, 10.0, 100.0, Color::WHITE);
959
960 frame.fill_rect(770.0, 250.0, 10.0, 100.0, Color::WHITE);
962
963 frame.fill_circle(400.0, 300.0, 10.0, Color::WHITE);
965
966 frame.fill_text_aligned(
968 "3",
969 200.0,
970 50.0,
971 "48px monospace",
972 Color::WHITE,
973 TextAlign::Center,
974 TextBaseline::Top,
975 );
976 frame.fill_text_aligned(
977 "5",
978 600.0,
979 50.0,
980 "48px monospace",
981 Color::WHITE,
982 TextAlign::Center,
983 TextBaseline::Top,
984 );
985
986 assert_eq!(frame.len(), 7);
987
988 let json = frame.to_json().unwrap();
989 let commands: Vec<Canvas2DCommand> = serde_json::from_str(&json).unwrap();
991 assert_eq!(commands.len(), 7);
992 }
993
994 #[test]
995 fn test_convert_render_command_clear() {
996 let cmd = jugar_render::RenderCommand::Clear {
997 color: [0.0, 0.0, 0.0, 1.0],
998 };
999 let converted = convert_render_command(&cmd).unwrap();
1000 assert!(matches!(converted, Canvas2DCommand::Clear { .. }));
1001 }
1002
1003 #[test]
1004 fn test_convert_render_command_draw_rect() {
1005 let cmd = jugar_render::RenderCommand::DrawRect {
1006 rect: jugar_core::Rect::new(10.0, 20.0, 100.0, 50.0),
1007 color: [1.0, 1.0, 1.0, 1.0],
1008 };
1009 let converted = convert_render_command(&cmd).unwrap();
1010 match converted {
1011 Canvas2DCommand::FillRect {
1012 x,
1013 y,
1014 width,
1015 height,
1016 ..
1017 } => {
1018 assert!((x - 10.0).abs() < f32::EPSILON);
1019 assert!((y - 20.0).abs() < f32::EPSILON);
1020 assert!((width - 100.0).abs() < f32::EPSILON);
1021 assert!((height - 50.0).abs() < f32::EPSILON);
1022 }
1023 _ => panic!("Expected FillRect"),
1024 }
1025 }
1026
1027 #[test]
1028 fn test_convert_render_command_sprite_returns_none() {
1029 use glam::Vec2;
1030 use jugar_core::Position;
1031 let cmd = jugar_render::RenderCommand::DrawSprite {
1032 texture_id: 0,
1033 position: Position::zero(),
1034 size: Vec2::new(64.0, 64.0),
1035 source: None,
1036 color: [1.0, 1.0, 1.0, 1.0],
1037 };
1038 assert!(convert_render_command(&cmd).is_none());
1039 }
1040
1041 #[test]
1042 fn test_convert_render_queue() {
1043 let commands = vec![
1044 jugar_render::RenderCommand::Clear {
1045 color: [0.0, 0.0, 0.0, 1.0],
1046 },
1047 jugar_render::RenderCommand::DrawRect {
1048 rect: jugar_core::Rect::new(0.0, 0.0, 100.0, 100.0),
1049 color: [1.0, 1.0, 1.0, 1.0],
1050 },
1051 ];
1052
1053 let frame = convert_render_queue(&commands);
1054 assert_eq!(frame.len(), 2);
1055 }
1056
1057 #[test]
1058 fn test_convert_render_queue_skips_sprites() {
1059 use glam::Vec2;
1060 use jugar_core::Position;
1061 let commands = vec![
1062 jugar_render::RenderCommand::Clear {
1063 color: [0.0, 0.0, 0.0, 1.0],
1064 },
1065 jugar_render::RenderCommand::DrawSprite {
1066 texture_id: 0,
1067 position: Position::zero(),
1068 size: Vec2::new(64.0, 64.0),
1069 source: None,
1070 color: [1.0, 1.0, 1.0, 1.0],
1071 },
1072 jugar_render::RenderCommand::DrawRect {
1073 rect: jugar_core::Rect::new(0.0, 0.0, 100.0, 100.0),
1074 color: [1.0, 1.0, 1.0, 1.0],
1075 },
1076 ];
1077
1078 let frame = convert_render_queue(&commands);
1079 assert_eq!(frame.len(), 2); }
1081
1082 #[test]
1083 fn test_text_align_serialization() {
1084 assert_eq!(serde_json::to_string(&TextAlign::Left).unwrap(), "\"left\"");
1085 assert_eq!(
1086 serde_json::to_string(&TextAlign::Center).unwrap(),
1087 "\"center\""
1088 );
1089 assert_eq!(
1090 serde_json::to_string(&TextAlign::Right).unwrap(),
1091 "\"right\""
1092 );
1093 }
1094
1095 #[test]
1096 fn test_text_baseline_serialization() {
1097 assert_eq!(
1098 serde_json::to_string(&TextBaseline::Top).unwrap(),
1099 "\"top\""
1100 );
1101 assert_eq!(
1102 serde_json::to_string(&TextBaseline::Middle).unwrap(),
1103 "\"middle\""
1104 );
1105 assert_eq!(
1106 serde_json::to_string(&TextBaseline::Bottom).unwrap(),
1107 "\"bottom\""
1108 );
1109 assert_eq!(
1110 serde_json::to_string(&TextBaseline::Alphabetic).unwrap(),
1111 "\"alphabetic\""
1112 );
1113 }
1114}