1use crate::time::TimeBase;
28
29#[derive(Clone, Debug)]
37pub struct VectorFrame {
38 pub width: f32,
40 pub height: f32,
42 pub view_box: Option<ViewBox>,
44 pub root: Group,
46 pub pts: Option<i64>,
48 pub time_base: TimeBase,
51}
52
53impl VectorFrame {
54 pub fn new(width: f32, height: f32) -> Self {
57 Self {
58 width,
59 height,
60 view_box: None,
61 root: Group::default(),
62 pts: None,
63 time_base: TimeBase::new(1, 1),
64 }
65 }
66
67 pub fn with_view_box(mut self, view_box: ViewBox) -> Self {
69 self.view_box = Some(view_box);
70 self
71 }
72
73 pub fn with_root(mut self, root: Group) -> Self {
75 self.root = root;
76 self
77 }
78
79 pub fn with_pts(mut self, pts: i64) -> Self {
81 self.pts = Some(pts);
82 self
83 }
84
85 pub fn with_time_base(mut self, time_base: TimeBase) -> Self {
87 self.time_base = time_base;
88 self
89 }
90}
91
92impl Default for VectorFrame {
93 fn default() -> Self {
98 Self::new(0.0, 0.0)
99 }
100}
101
102#[derive(Clone, Copy, Debug, PartialEq)]
105pub struct ViewBox {
106 pub min_x: f32,
107 pub min_y: f32,
108 pub width: f32,
109 pub height: f32,
110}
111
112impl ViewBox {
113 pub const fn new(min_x: f32, min_y: f32, width: f32, height: f32) -> Self {
114 Self {
115 min_x,
116 min_y,
117 width,
118 height,
119 }
120 }
121}
122
123#[derive(Clone, Debug)]
128#[non_exhaustive]
129pub enum Node {
130 Path(PathNode),
131 Group(Group),
132 Image(ImageRef),
134 SoftMask {
140 mask: Box<Node>,
143 mask_kind: MaskKind,
145 content: Box<Node>,
147 },
148}
149
150#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
153pub enum MaskKind {
154 #[default]
159 Luminance,
160 Alpha,
163}
164
165#[derive(Clone, Debug)]
169pub struct Group {
170 pub transform: Transform2D,
172 pub opacity: f32,
174 pub clip: Option<Path>,
177 pub children: Vec<Node>,
178 pub cache_key: Option<u64>,
193}
194
195impl Default for Group {
196 fn default() -> Self {
197 Self {
198 transform: Transform2D::identity(),
199 opacity: 1.0,
200 clip: None,
201 children: Vec::new(),
202 cache_key: None,
203 }
204 }
205}
206
207impl Group {
208 pub fn new() -> Self {
211 Self::default()
212 }
213
214 pub fn with_transform(mut self, transform: Transform2D) -> Self {
216 self.transform = transform;
217 self
218 }
219
220 pub fn with_opacity(mut self, opacity: f32) -> Self {
222 self.opacity = opacity;
223 self
224 }
225
226 pub fn with_clip(mut self, clip: Path) -> Self {
228 self.clip = Some(clip);
229 self
230 }
231
232 pub fn with_child(mut self, child: Node) -> Self {
234 self.children.push(child);
235 self
236 }
237
238 pub fn with_children(mut self, children: Vec<Node>) -> Self {
240 self.children = children;
241 self
242 }
243
244 pub fn with_cache_key(mut self, key: u64) -> Self {
246 self.cache_key = Some(key);
247 self
248 }
249}
250
251#[derive(Clone, Debug)]
258pub struct PathNode {
259 pub path: Path,
260 pub fill: Option<Paint>,
261 pub stroke: Option<Stroke>,
262 pub fill_rule: FillRule,
263}
264
265impl PathNode {
266 pub fn new(path: Path) -> Self {
269 Self {
270 path,
271 fill: None,
272 stroke: None,
273 fill_rule: FillRule::NonZero,
274 }
275 }
276
277 pub fn with_fill(mut self, fill: Paint) -> Self {
279 self.fill = Some(fill);
280 self
281 }
282
283 pub fn with_stroke(mut self, stroke: Stroke) -> Self {
285 self.stroke = Some(stroke);
286 self
287 }
288
289 pub fn with_fill_rule(mut self, fill_rule: FillRule) -> Self {
291 self.fill_rule = fill_rule;
292 self
293 }
294}
295
296#[derive(Clone, Debug, Default)]
300pub struct Path {
301 pub commands: Vec<PathCommand>,
302}
303
304impl Path {
305 pub fn new() -> Self {
306 Self::default()
307 }
308
309 pub fn move_to(&mut self, p: Point) -> &mut Self {
310 self.commands.push(PathCommand::MoveTo(p));
311 self
312 }
313
314 pub fn line_to(&mut self, p: Point) -> &mut Self {
315 self.commands.push(PathCommand::LineTo(p));
316 self
317 }
318
319 pub fn quad_to(&mut self, control: Point, end: Point) -> &mut Self {
320 self.commands
321 .push(PathCommand::QuadCurveTo { control, end });
322 self
323 }
324
325 pub fn cubic_to(&mut self, c1: Point, c2: Point, end: Point) -> &mut Self {
326 self.commands
327 .push(PathCommand::CubicCurveTo { c1, c2, end });
328 self
329 }
330
331 pub fn close(&mut self) -> &mut Self {
332 self.commands.push(PathCommand::Close);
333 self
334 }
335}
336
337#[derive(Clone, Copy, Debug, PartialEq)]
349#[non_exhaustive]
350pub enum PathCommand {
351 MoveTo(Point),
352 LineTo(Point),
353 QuadCurveTo {
354 control: Point,
355 end: Point,
356 },
357 CubicCurveTo {
358 c1: Point,
359 c2: Point,
360 end: Point,
361 },
362 ArcTo {
366 rx: f32,
367 ry: f32,
368 x_axis_rot: f32,
369 large_arc: bool,
370 sweep: bool,
371 end: Point,
372 },
373 Close,
374}
375
376#[derive(Clone, Copy, Debug, Default, PartialEq)]
378pub struct Point {
379 pub x: f32,
380 pub y: f32,
381}
382
383impl Point {
384 pub const fn new(x: f32, y: f32) -> Self {
385 Self { x, y }
386 }
387}
388
389impl From<[f32; 2]> for Point {
390 fn from([x, y]: [f32; 2]) -> Self {
391 Self { x, y }
392 }
393}
394
395impl From<(f32, f32)> for Point {
396 fn from((x, y): (f32, f32)) -> Self {
397 Self { x, y }
398 }
399}
400
401#[derive(Clone, Debug)]
404#[non_exhaustive]
405pub enum Paint {
406 Solid(Rgba),
407 LinearGradient(LinearGradient),
408 RadialGradient(RadialGradient),
409}
410
411#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
417pub struct Rgba {
418 pub r: u8,
419 pub g: u8,
420 pub b: u8,
421 pub a: u8,
422}
423
424impl Rgba {
425 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
426 Self { r, g, b, a }
427 }
428
429 pub const fn opaque(r: u8, g: u8, b: u8) -> Self {
431 Self { r, g, b, a: 255 }
432 }
433}
434
435impl From<(u8, u8, u8, u8)> for Rgba {
436 fn from((r, g, b, a): (u8, u8, u8, u8)) -> Self {
437 Self { r, g, b, a }
438 }
439}
440
441impl From<(u8, u8, u8)> for Rgba {
442 fn from((r, g, b): (u8, u8, u8)) -> Self {
444 Self { r, g, b, a: 255 }
445 }
446}
447
448impl From<[u8; 4]> for Rgba {
449 fn from([r, g, b, a]: [u8; 4]) -> Self {
450 Self { r, g, b, a }
451 }
452}
453
454impl From<Rgba> for Paint {
455 fn from(color: Rgba) -> Self {
457 Paint::Solid(color)
458 }
459}
460
461#[derive(Clone, Debug)]
463pub struct LinearGradient {
464 pub start: Point,
465 pub end: Point,
466 pub stops: Vec<GradientStop>,
467 pub spread: SpreadMethod,
468}
469
470impl LinearGradient {
471 pub fn new(start: Point, end: Point) -> Self {
474 Self {
475 start,
476 end,
477 stops: Vec::new(),
478 spread: SpreadMethod::Pad,
479 }
480 }
481
482 pub fn with_stops(mut self, stops: Vec<GradientStop>) -> Self {
484 self.stops = stops;
485 self
486 }
487
488 pub fn with_stop(mut self, stop: GradientStop) -> Self {
490 self.stops.push(stop);
491 self
492 }
493
494 pub fn with_spread(mut self, spread: SpreadMethod) -> Self {
496 self.spread = spread;
497 self
498 }
499}
500
501#[derive(Clone, Debug)]
505pub struct RadialGradient {
506 pub center: Point,
507 pub radius: f32,
508 pub focal: Option<Point>,
509 pub stops: Vec<GradientStop>,
510 pub spread: SpreadMethod,
511}
512
513impl RadialGradient {
514 pub fn new(center: Point, radius: f32) -> Self {
517 Self {
518 center,
519 radius,
520 focal: None,
521 stops: Vec::new(),
522 spread: SpreadMethod::Pad,
523 }
524 }
525
526 pub fn with_focal(mut self, focal: Point) -> Self {
528 self.focal = Some(focal);
529 self
530 }
531
532 pub fn with_stops(mut self, stops: Vec<GradientStop>) -> Self {
534 self.stops = stops;
535 self
536 }
537
538 pub fn with_stop(mut self, stop: GradientStop) -> Self {
540 self.stops.push(stop);
541 self
542 }
543
544 pub fn with_spread(mut self, spread: SpreadMethod) -> Self {
546 self.spread = spread;
547 self
548 }
549}
550
551#[derive(Clone, Copy, Debug, PartialEq)]
553pub struct GradientStop {
554 pub offset: f32,
557 pub color: Rgba,
558}
559
560impl GradientStop {
561 pub const fn new(offset: f32, color: Rgba) -> Self {
562 Self { offset, color }
563 }
564}
565
566#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
569pub enum SpreadMethod {
570 #[default]
572 Pad,
573 Reflect,
575 Repeat,
577}
578
579#[derive(Clone, Debug)]
581pub struct Stroke {
582 pub width: f32,
583 pub paint: Paint,
584 pub cap: LineCap,
585 pub join: LineJoin,
586 pub miter_limit: f32,
588 pub dash: Option<DashPattern>,
589}
590
591impl Stroke {
592 pub fn solid(width: f32, color: Rgba) -> Self {
594 Self {
595 width,
596 paint: Paint::Solid(color),
597 cap: LineCap::Butt,
598 join: LineJoin::Miter,
599 miter_limit: 4.0,
600 dash: None,
601 }
602 }
603
604 pub fn new(width: f32, paint: Paint) -> Self {
608 Self {
609 width,
610 paint,
611 cap: LineCap::Butt,
612 join: LineJoin::Miter,
613 miter_limit: 4.0,
614 dash: None,
615 }
616 }
617
618 pub fn with_paint(mut self, paint: Paint) -> Self {
620 self.paint = paint;
621 self
622 }
623
624 pub fn with_cap(mut self, cap: LineCap) -> Self {
626 self.cap = cap;
627 self
628 }
629
630 pub fn with_join(mut self, join: LineJoin) -> Self {
632 self.join = join;
633 self
634 }
635
636 pub fn with_miter_limit(mut self, miter_limit: f32) -> Self {
638 self.miter_limit = miter_limit;
639 self
640 }
641
642 pub fn with_dash(mut self, dash: DashPattern) -> Self {
644 self.dash = Some(dash);
645 self
646 }
647}
648
649#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
651pub enum LineCap {
652 #[default]
653 Butt,
654 Round,
655 Square,
656}
657
658#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
660pub enum LineJoin {
661 #[default]
662 Miter,
663 Round,
664 Bevel,
665}
666
667#[derive(Clone, Debug, Default)]
671pub struct DashPattern {
672 pub array: Vec<f32>,
673 pub offset: f32,
674}
675
676impl DashPattern {
677 pub fn new(array: Vec<f32>) -> Self {
680 Self { array, offset: 0.0 }
681 }
682
683 pub fn with_offset(mut self, offset: f32) -> Self {
685 self.offset = offset;
686 self
687 }
688}
689
690#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
694pub enum FillRule {
695 #[default]
696 NonZero,
697 EvenOdd,
698}
699
700#[derive(Clone, Copy, Debug, PartialEq)]
712pub struct Transform2D {
713 pub a: f32,
714 pub b: f32,
715 pub c: f32,
716 pub d: f32,
717 pub e: f32,
718 pub f: f32,
719}
720
721impl Transform2D {
722 pub const fn identity() -> Self {
724 Self {
725 a: 1.0,
726 b: 0.0,
727 c: 0.0,
728 d: 1.0,
729 e: 0.0,
730 f: 0.0,
731 }
732 }
733
734 pub const fn translate(tx: f32, ty: f32) -> Self {
736 Self {
737 a: 1.0,
738 b: 0.0,
739 c: 0.0,
740 d: 1.0,
741 e: tx,
742 f: ty,
743 }
744 }
745
746 pub const fn scale(sx: f32, sy: f32) -> Self {
748 Self {
749 a: sx,
750 b: 0.0,
751 c: 0.0,
752 d: sy,
753 e: 0.0,
754 f: 0.0,
755 }
756 }
757
758 pub fn rotate(angle_radians: f32) -> Self {
762 let (s, c) = angle_radians.sin_cos();
763 Self {
764 a: c,
765 b: s,
766 c: -s,
767 d: c,
768 e: 0.0,
769 f: 0.0,
770 }
771 }
772
773 pub fn skew_x(angle_radians: f32) -> Self {
775 Self {
776 a: 1.0,
777 b: 0.0,
778 c: angle_radians.tan(),
779 d: 1.0,
780 e: 0.0,
781 f: 0.0,
782 }
783 }
784
785 pub fn skew_y(angle_radians: f32) -> Self {
787 Self {
788 a: 1.0,
789 b: angle_radians.tan(),
790 c: 0.0,
791 d: 1.0,
792 e: 0.0,
793 f: 0.0,
794 }
795 }
796
797 pub fn compose(&self, other: &Self) -> Self {
801 Self {
802 a: self.a * other.a + self.c * other.b,
803 b: self.b * other.a + self.d * other.b,
804 c: self.a * other.c + self.c * other.d,
805 d: self.b * other.c + self.d * other.d,
806 e: self.a * other.e + self.c * other.f + self.e,
807 f: self.b * other.e + self.d * other.f + self.f,
808 }
809 }
810
811 pub fn apply(&self, p: Point) -> Point {
813 Point {
814 x: self.a * p.x + self.c * p.y + self.e,
815 y: self.b * p.x + self.d * p.y + self.f,
816 }
817 }
818
819 pub fn is_identity(&self) -> bool {
823 *self == Self::identity()
824 }
825}
826
827impl Default for Transform2D {
828 fn default() -> Self {
829 Self::identity()
830 }
831}
832
833#[derive(Clone, Debug)]
840pub struct ImageRef {
841 pub frame: Box<crate::VideoFrame>,
844 pub bounds: Rect,
845 pub transform: Transform2D,
846}
847
848#[derive(Clone, Copy, Debug, Default, PartialEq)]
850pub struct Rect {
851 pub x: f32,
852 pub y: f32,
853 pub width: f32,
854 pub height: f32,
855}
856
857impl Rect {
858 pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
859 Self {
860 x,
861 y,
862 width,
863 height,
864 }
865 }
866}
867
868#[cfg(test)]
869mod tests {
870 use super::*;
871 use crate::time::TimeBase;
872
873 fn approx_point(a: Point, b: Point) -> bool {
874 (a.x - b.x).abs() < 1e-5 && (a.y - b.y).abs() < 1e-5
875 }
876
877 #[test]
878 fn path_builder_produces_command_sequence() {
879 let mut p = Path::new();
880 p.move_to(Point::new(0.0, 0.0))
881 .line_to(Point::new(10.0, 0.0))
882 .quad_to(Point::new(15.0, 5.0), Point::new(10.0, 10.0))
883 .cubic_to(
884 Point::new(5.0, 15.0),
885 Point::new(0.0, 10.0),
886 Point::new(0.0, 0.0),
887 )
888 .close();
889 assert_eq!(p.commands.len(), 5);
890 assert_eq!(p.commands[0], PathCommand::MoveTo(Point::new(0.0, 0.0)));
891 assert_eq!(p.commands[4], PathCommand::Close);
892 }
893
894 #[test]
895 fn transform_identity_round_trips() {
896 let id = Transform2D::identity();
897 assert!(id.is_identity());
898 let p = Point::new(3.5, -2.25);
899 assert_eq!(id.apply(p), p);
900 }
901
902 #[test]
903 fn transform_translate_round_trip() {
904 let t = Transform2D::translate(10.0, -5.0);
905 assert_eq!(t.apply(Point::new(0.0, 0.0)), Point::new(10.0, -5.0));
906 assert_eq!(t.apply(Point::new(1.0, 1.0)), Point::new(11.0, -4.0));
907 }
908
909 #[test]
910 fn transform_scale_round_trip() {
911 let s = Transform2D::scale(2.0, 3.0);
912 assert_eq!(s.apply(Point::new(1.0, 1.0)), Point::new(2.0, 3.0));
913 assert_eq!(s.apply(Point::new(0.0, 0.0)), Point::new(0.0, 0.0));
914 }
915
916 #[test]
917 fn transform_rotate_quarter_turn() {
918 let r = Transform2D::rotate(std::f32::consts::FRAC_PI_2);
919 assert!(approx_point(
922 r.apply(Point::new(1.0, 0.0)),
923 Point::new(0.0, 1.0)
924 ));
925 assert!(approx_point(
927 r.apply(Point::new(0.0, 1.0)),
928 Point::new(-1.0, 0.0)
929 ));
930 }
931
932 #[test]
933 fn transform_compose_identity_is_left_and_right_unit() {
934 let t = Transform2D::translate(7.0, 11.0);
935 let id = Transform2D::identity();
936 assert_eq!(id.compose(&t), t);
937 assert_eq!(t.compose(&id), t);
938 }
939
940 #[test]
941 fn transform_compose_translate_then_scale() {
942 let scale = Transform2D::scale(10.0, 10.0);
945 let translate = Transform2D::translate(2.0, 3.0);
946 let composed = scale.compose(&translate);
947 let result = composed.apply(Point::new(1.0, 1.0));
948 assert!(approx_point(result, Point::new(30.0, 40.0)));
949 }
950
951 #[test]
952 fn transform_compose_matches_sequential_apply() {
953 let a = Transform2D::rotate(0.5);
955 let b = Transform2D::translate(3.0, -1.0);
956 let composed = a.compose(&b);
957 let p = Point::new(2.0, 5.0);
958 let direct = composed.apply(p);
959 let stepwise = a.apply(b.apply(p));
960 assert!(approx_point(direct, stepwise));
961 }
962
963 #[test]
964 fn group_default_is_identity_opacity_one_no_clip() {
965 let g = Group::default();
966 assert!(g.transform.is_identity());
967 assert_eq!(g.opacity, 1.0);
968 assert!(g.clip.is_none());
969 assert!(g.children.is_empty());
970 }
971
972 #[test]
973 fn group_nesting_with_transforms() {
974 let inner = Group {
981 transform: Transform2D::scale(2.0, 2.0),
982 children: vec![Node::Path(PathNode {
983 path: {
984 let mut p = Path::new();
985 p.move_to(Point::new(1.0, 1.0));
986 p
987 },
988 fill: Some(Paint::Solid(Rgba::opaque(255, 0, 0))),
989 stroke: None,
990 fill_rule: FillRule::NonZero,
991 })],
992 ..Group::default()
993 };
994 let outer = Group {
995 transform: Transform2D::translate(10.0, 10.0),
996 children: vec![Node::Group(inner)],
997 ..Group::default()
998 };
999 match &outer.children[0] {
1000 Node::Group(g) => {
1001 assert_eq!(g.transform, Transform2D::scale(2.0, 2.0));
1002 assert_eq!(g.children.len(), 1);
1003 }
1004 _ => panic!("expected a Group child"),
1005 }
1006 assert_eq!(outer.transform, Transform2D::translate(10.0, 10.0));
1007 }
1008
1009 #[test]
1010 fn vector_frame_construction() {
1011 let frame = VectorFrame {
1012 width: 100.0,
1013 height: 50.0,
1014 view_box: Some(ViewBox {
1015 min_x: 0.0,
1016 min_y: 0.0,
1017 width: 100.0,
1018 height: 50.0,
1019 }),
1020 root: Group::default(),
1021 pts: Some(0),
1022 time_base: TimeBase::new(1, 1000),
1023 };
1024 assert_eq!(frame.width, 100.0);
1025 assert_eq!(frame.height, 50.0);
1026 assert!(frame.view_box.is_some());
1027 assert_eq!(frame.pts, Some(0));
1028 }
1029
1030 #[test]
1031 fn rgba_constructors() {
1032 let c = Rgba::opaque(10, 20, 30);
1033 assert_eq!(c.a, 255);
1034 let c2 = Rgba::new(10, 20, 30, 128);
1035 assert_eq!(c2.a, 128);
1036 }
1037
1038 #[test]
1039 fn gradient_stop_round_trips() {
1040 let s = GradientStop::new(0.5, Rgba::opaque(255, 0, 0));
1041 assert_eq!(s.offset, 0.5);
1042 let s2 = GradientStop::new(0.5, Rgba::opaque(255, 0, 0));
1043 assert_eq!(s, s2);
1044 }
1045
1046 #[test]
1047 fn stroke_solid_defaults() {
1048 let s = Stroke::solid(2.0, Rgba::opaque(0, 0, 0));
1049 assert_eq!(s.width, 2.0);
1050 assert_eq!(s.cap, LineCap::Butt);
1051 assert_eq!(s.join, LineJoin::Miter);
1052 assert_eq!(s.miter_limit, 4.0);
1053 assert!(s.dash.is_none());
1054 }
1055
1056 #[test]
1057 fn soft_mask_construction_and_inspection() {
1058 fn rect_path() -> PathNode {
1061 let mut p = Path::new();
1062 p.move_to(Point::new(0.0, 0.0))
1063 .line_to(Point::new(10.0, 0.0))
1064 .line_to(Point::new(10.0, 10.0))
1065 .line_to(Point::new(0.0, 10.0))
1066 .close();
1067 PathNode {
1068 path: p,
1069 fill: Some(Paint::Solid(Rgba::opaque(255, 255, 255))),
1070 stroke: None,
1071 fill_rule: FillRule::NonZero,
1072 }
1073 }
1074 let n = Node::SoftMask {
1075 mask: Box::new(Node::Path(rect_path())),
1076 mask_kind: MaskKind::Luminance,
1077 content: Box::new(Node::Path(rect_path())),
1078 };
1079 match &n {
1080 Node::SoftMask {
1081 mask_kind, content, ..
1082 } => {
1083 assert_eq!(*mask_kind, MaskKind::Luminance);
1084 match content.as_ref() {
1085 Node::Path(_) => {}
1086 _ => panic!("expected Path content"),
1087 }
1088 }
1089 _ => panic!("expected SoftMask"),
1090 }
1091 }
1092
1093 #[test]
1094 fn mask_kind_default_is_luminance() {
1095 assert_eq!(MaskKind::default(), MaskKind::Luminance);
1096 }
1097
1098 #[test]
1099 fn vector_frame_default_is_empty_zero_size() {
1100 let f = VectorFrame::default();
1101 assert_eq!(f.width, 0.0);
1102 assert_eq!(f.height, 0.0);
1103 assert!(f.view_box.is_none());
1104 assert!(f.root.children.is_empty());
1105 assert!(f.pts.is_none());
1106 assert_eq!(f.time_base, TimeBase::new(1, 1));
1107 }
1108
1109 #[test]
1110 fn vector_frame_new_sets_canvas_size() {
1111 let f = VectorFrame::new(640.0, 480.0);
1112 assert_eq!(f.width, 640.0);
1113 assert_eq!(f.height, 480.0);
1114 assert!(f.view_box.is_none());
1115 assert!(f.root.children.is_empty());
1116 assert!(f.pts.is_none());
1117 }
1118
1119 #[test]
1120 fn vector_frame_builder_chain() {
1121 let vb = ViewBox::new(0.0, 0.0, 100.0, 100.0);
1122 let f = VectorFrame::new(100.0, 100.0)
1123 .with_view_box(vb)
1124 .with_pts(42)
1125 .with_time_base(TimeBase::new(1, 90_000));
1126 assert_eq!(f.view_box, Some(vb));
1127 assert_eq!(f.pts, Some(42));
1128 assert_eq!(f.time_base, TimeBase::new(1, 90_000));
1129 }
1130
1131 #[test]
1132 fn vector_frame_with_root_replaces_root() {
1133 let root = Group::new().with_opacity(0.5);
1134 let f = VectorFrame::new(10.0, 10.0).with_root(root);
1135 assert_eq!(f.root.opacity, 0.5);
1136 }
1137
1138 #[test]
1139 fn view_box_new_round_trips_fields() {
1140 let vb = ViewBox::new(1.0, 2.0, 3.0, 4.0);
1141 assert_eq!(vb.min_x, 1.0);
1142 assert_eq!(vb.min_y, 2.0);
1143 assert_eq!(vb.width, 3.0);
1144 assert_eq!(vb.height, 4.0);
1145 }
1146
1147 #[test]
1148 fn rect_new_round_trips_fields() {
1149 let r = Rect::new(1.0, 2.0, 3.0, 4.0);
1150 assert_eq!(r.x, 1.0);
1151 assert_eq!(r.y, 2.0);
1152 assert_eq!(r.width, 3.0);
1153 assert_eq!(r.height, 4.0);
1154 }
1155
1156 #[test]
1157 fn group_new_matches_default() {
1158 let a = Group::new();
1159 let b = Group::default();
1160 assert!(a.transform.is_identity());
1161 assert_eq!(a.opacity, b.opacity);
1162 assert!(a.clip.is_none());
1163 assert_eq!(a.children.len(), b.children.len());
1164 assert_eq!(a.cache_key, b.cache_key);
1165 }
1166
1167 #[test]
1168 fn group_builder_chain() {
1169 let mut clip = Path::new();
1170 clip.move_to(Point::new(0.0, 0.0))
1171 .line_to(Point::new(1.0, 1.0))
1172 .close();
1173 let g = Group::new()
1174 .with_transform(Transform2D::translate(5.0, 7.0))
1175 .with_opacity(0.25)
1176 .with_clip(clip)
1177 .with_cache_key(0xdead_beef);
1178 assert_eq!(g.transform, Transform2D::translate(5.0, 7.0));
1179 assert_eq!(g.opacity, 0.25);
1180 assert!(g.clip.is_some());
1181 assert_eq!(g.cache_key, Some(0xdead_beef));
1182 }
1183
1184 #[test]
1185 fn group_with_child_appends() {
1186 let g = Group::new()
1187 .with_child(Node::Group(Group::new()))
1188 .with_child(Node::Group(Group::new().with_opacity(0.5)));
1189 assert_eq!(g.children.len(), 2);
1190 match &g.children[1] {
1191 Node::Group(inner) => assert_eq!(inner.opacity, 0.5),
1192 _ => panic!("expected Group child"),
1193 }
1194 }
1195
1196 #[test]
1197 fn group_with_children_replaces_list() {
1198 let g = Group::new()
1199 .with_child(Node::Group(Group::new()))
1200 .with_children(vec![Node::Group(Group::new().with_opacity(0.1))]);
1201 assert_eq!(g.children.len(), 1);
1202 match &g.children[0] {
1203 Node::Group(inner) => assert_eq!(inner.opacity, 0.1),
1204 _ => panic!("expected Group child"),
1205 }
1206 }
1207
1208 #[test]
1209 fn path_node_new_then_builder() {
1210 let mut p = Path::new();
1211 p.move_to(Point::new(0.0, 0.0))
1212 .line_to(Point::new(10.0, 0.0));
1213 let n = PathNode::new(p)
1214 .with_fill(Paint::Solid(Rgba::opaque(255, 0, 0)))
1215 .with_stroke(Stroke::solid(1.0, Rgba::opaque(0, 0, 0)))
1216 .with_fill_rule(FillRule::EvenOdd);
1217 assert!(n.fill.is_some());
1218 assert!(n.stroke.is_some());
1219 assert_eq!(n.fill_rule, FillRule::EvenOdd);
1220 }
1221
1222 #[test]
1223 fn path_node_new_defaults() {
1224 let n = PathNode::new(Path::new());
1225 assert!(n.fill.is_none());
1226 assert!(n.stroke.is_none());
1227 assert_eq!(n.fill_rule, FillRule::NonZero);
1228 }
1229
1230 #[test]
1231 fn point_from_array_and_tuple() {
1232 let p1: Point = [1.0_f32, 2.0_f32].into();
1233 let p2: Point = (3.0_f32, 4.0_f32).into();
1234 assert_eq!(p1, Point::new(1.0, 2.0));
1235 assert_eq!(p2, Point::new(3.0, 4.0));
1236 }
1237
1238 #[test]
1239 fn rgba_from_tuples_and_array() {
1240 let a: Rgba = (10u8, 20u8, 30u8, 40u8).into();
1241 let b: Rgba = (50u8, 60u8, 70u8).into();
1242 let c: Rgba = [1u8, 2u8, 3u8, 4u8].into();
1243 assert_eq!(a, Rgba::new(10, 20, 30, 40));
1244 assert_eq!(b, Rgba::opaque(50, 60, 70));
1245 assert_eq!(c, Rgba::new(1, 2, 3, 4));
1246 }
1247
1248 #[test]
1249 fn paint_from_rgba_wraps_solid() {
1250 let p: Paint = Rgba::opaque(1, 2, 3).into();
1251 match p {
1252 Paint::Solid(c) => assert_eq!(c, Rgba::opaque(1, 2, 3)),
1253 _ => panic!("expected Paint::Solid"),
1254 }
1255 }
1256
1257 #[test]
1258 fn linear_gradient_new_then_builder() {
1259 let g = LinearGradient::new(Point::new(0.0, 0.0), Point::new(1.0, 0.0))
1260 .with_stop(GradientStop::new(0.0, Rgba::opaque(0, 0, 0)))
1261 .with_stop(GradientStop::new(1.0, Rgba::opaque(255, 255, 255)))
1262 .with_spread(SpreadMethod::Reflect);
1263 assert_eq!(g.start, Point::new(0.0, 0.0));
1264 assert_eq!(g.end, Point::new(1.0, 0.0));
1265 assert_eq!(g.stops.len(), 2);
1266 assert_eq!(g.spread, SpreadMethod::Reflect);
1267 }
1268
1269 #[test]
1270 fn linear_gradient_with_stops_replaces() {
1271 let g = LinearGradient::new(Point::new(0.0, 0.0), Point::new(1.0, 0.0))
1272 .with_stop(GradientStop::new(0.5, Rgba::opaque(0, 0, 0)))
1273 .with_stops(vec![GradientStop::new(0.0, Rgba::opaque(1, 1, 1))]);
1274 assert_eq!(g.stops.len(), 1);
1275 assert_eq!(g.stops[0].offset, 0.0);
1276 }
1277
1278 #[test]
1279 fn radial_gradient_new_then_builder() {
1280 let g = RadialGradient::new(Point::new(5.0, 5.0), 10.0)
1281 .with_focal(Point::new(4.0, 4.0))
1282 .with_stop(GradientStop::new(0.0, Rgba::opaque(0, 0, 0)))
1283 .with_spread(SpreadMethod::Repeat);
1284 assert_eq!(g.center, Point::new(5.0, 5.0));
1285 assert_eq!(g.radius, 10.0);
1286 assert_eq!(g.focal, Some(Point::new(4.0, 4.0)));
1287 assert_eq!(g.stops.len(), 1);
1288 assert_eq!(g.spread, SpreadMethod::Repeat);
1289 }
1290
1291 #[test]
1292 fn radial_gradient_with_stops_replaces() {
1293 let g = RadialGradient::new(Point::new(0.0, 0.0), 1.0)
1294 .with_stop(GradientStop::new(0.5, Rgba::opaque(0, 0, 0)))
1295 .with_stops(vec![GradientStop::new(1.0, Rgba::opaque(1, 1, 1))]);
1296 assert_eq!(g.stops.len(), 1);
1297 assert_eq!(g.stops[0].offset, 1.0);
1298 }
1299
1300 #[test]
1301 fn stroke_new_defaults() {
1302 let s = Stroke::new(3.0, Paint::Solid(Rgba::opaque(0, 0, 0)));
1303 assert_eq!(s.width, 3.0);
1304 assert_eq!(s.cap, LineCap::Butt);
1305 assert_eq!(s.join, LineJoin::Miter);
1306 assert_eq!(s.miter_limit, 4.0);
1307 assert!(s.dash.is_none());
1308 }
1309
1310 #[test]
1311 fn stroke_builder_chain() {
1312 let s = Stroke::solid(1.0, Rgba::opaque(0, 0, 0))
1313 .with_cap(LineCap::Round)
1314 .with_join(LineJoin::Bevel)
1315 .with_miter_limit(10.0)
1316 .with_dash(DashPattern::new(vec![2.0, 1.0]).with_offset(0.5))
1317 .with_paint(Paint::Solid(Rgba::opaque(128, 128, 128)));
1318 assert_eq!(s.cap, LineCap::Round);
1319 assert_eq!(s.join, LineJoin::Bevel);
1320 assert_eq!(s.miter_limit, 10.0);
1321 let d = s.dash.expect("dash set");
1322 assert_eq!(d.array, vec![2.0, 1.0]);
1323 assert_eq!(d.offset, 0.5);
1324 match s.paint {
1325 Paint::Solid(c) => assert_eq!(c, Rgba::opaque(128, 128, 128)),
1326 _ => panic!("expected Paint::Solid"),
1327 }
1328 }
1329
1330 #[test]
1331 fn dash_pattern_new_zero_offset() {
1332 let d = DashPattern::new(vec![1.0, 2.0, 3.0]);
1333 assert_eq!(d.array, vec![1.0, 2.0, 3.0]);
1334 assert_eq!(d.offset, 0.0);
1335 }
1336
1337 #[test]
1338 fn dash_pattern_with_offset_sets_phase() {
1339 let d = DashPattern::new(vec![1.0]).with_offset(0.25);
1340 assert_eq!(d.offset, 0.25);
1341 }
1342}