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,
108 pub min_y: f32,
110 pub width: f32,
112 pub height: f32,
114}
115
116impl ViewBox {
117 pub const fn new(min_x: f32, min_y: f32, width: f32, height: f32) -> Self {
119 Self {
120 min_x,
121 min_y,
122 width,
123 height,
124 }
125 }
126}
127
128#[derive(Clone, Debug)]
133#[non_exhaustive]
134pub enum Node {
135 Path(PathNode),
137 Group(Group),
139 Image(ImageRef),
141 SoftMask {
147 mask: Box<Node>,
150 mask_kind: MaskKind,
152 content: Box<Node>,
154 },
155}
156
157#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
160pub enum MaskKind {
161 #[default]
166 Luminance,
167 Alpha,
170}
171
172#[derive(Clone, Debug)]
176pub struct Group {
177 pub transform: Transform2D,
179 pub opacity: f32,
181 pub clip: Option<Path>,
184 pub children: Vec<Node>,
186 pub cache_key: Option<u64>,
201}
202
203impl Default for Group {
204 fn default() -> Self {
205 Self {
206 transform: Transform2D::identity(),
207 opacity: 1.0,
208 clip: None,
209 children: Vec::new(),
210 cache_key: None,
211 }
212 }
213}
214
215impl Group {
216 pub fn new() -> Self {
219 Self::default()
220 }
221
222 pub fn with_transform(mut self, transform: Transform2D) -> Self {
224 self.transform = transform;
225 self
226 }
227
228 pub fn with_opacity(mut self, opacity: f32) -> Self {
230 self.opacity = opacity;
231 self
232 }
233
234 pub fn with_clip(mut self, clip: Path) -> Self {
236 self.clip = Some(clip);
237 self
238 }
239
240 pub fn with_child(mut self, child: Node) -> Self {
242 self.children.push(child);
243 self
244 }
245
246 pub fn with_children(mut self, children: Vec<Node>) -> Self {
248 self.children = children;
249 self
250 }
251
252 pub fn with_cache_key(mut self, key: u64) -> Self {
254 self.cache_key = Some(key);
255 self
256 }
257}
258
259#[derive(Clone, Debug)]
266pub struct PathNode {
267 pub path: Path,
269 pub fill: Option<Paint>,
271 pub stroke: Option<Stroke>,
273 pub fill_rule: FillRule,
275}
276
277impl PathNode {
278 pub fn new(path: Path) -> Self {
281 Self {
282 path,
283 fill: None,
284 stroke: None,
285 fill_rule: FillRule::NonZero,
286 }
287 }
288
289 pub fn with_fill(mut self, fill: Paint) -> Self {
291 self.fill = Some(fill);
292 self
293 }
294
295 pub fn with_stroke(mut self, stroke: Stroke) -> Self {
297 self.stroke = Some(stroke);
298 self
299 }
300
301 pub fn with_fill_rule(mut self, fill_rule: FillRule) -> Self {
303 self.fill_rule = fill_rule;
304 self
305 }
306}
307
308#[derive(Clone, Debug, Default)]
312pub struct Path {
313 pub commands: Vec<PathCommand>,
315}
316
317impl Path {
318 pub fn new() -> Self {
320 Self::default()
321 }
322
323 pub fn move_to(&mut self, p: Point) -> &mut Self {
325 self.commands.push(PathCommand::MoveTo(p));
326 self
327 }
328
329 pub fn line_to(&mut self, p: Point) -> &mut Self {
331 self.commands.push(PathCommand::LineTo(p));
332 self
333 }
334
335 pub fn quad_to(&mut self, control: Point, end: Point) -> &mut Self {
338 self.commands
339 .push(PathCommand::QuadCurveTo { control, end });
340 self
341 }
342
343 pub fn cubic_to(&mut self, c1: Point, c2: Point, end: Point) -> &mut Self {
346 self.commands
347 .push(PathCommand::CubicCurveTo { c1, c2, end });
348 self
349 }
350
351 pub fn close(&mut self) -> &mut Self {
353 self.commands.push(PathCommand::Close);
354 self
355 }
356}
357
358#[derive(Clone, Copy, Debug, PartialEq)]
370#[non_exhaustive]
371pub enum PathCommand {
372 MoveTo(Point),
374 LineTo(Point),
376 QuadCurveTo {
378 control: Point,
380 end: Point,
382 },
383 CubicCurveTo {
385 c1: Point,
387 c2: Point,
389 end: Point,
391 },
392 ArcTo {
396 rx: f32,
398 ry: f32,
400 x_axis_rot: f32,
403 large_arc: bool,
406 sweep: bool,
409 end: Point,
411 },
412 Close,
415}
416
417#[derive(Clone, Copy, Debug, Default, PartialEq)]
419pub struct Point {
420 pub x: f32,
422 pub y: f32,
425}
426
427impl Point {
428 pub const fn new(x: f32, y: f32) -> Self {
430 Self { x, y }
431 }
432}
433
434impl From<[f32; 2]> for Point {
435 fn from([x, y]: [f32; 2]) -> Self {
436 Self { x, y }
437 }
438}
439
440impl From<(f32, f32)> for Point {
441 fn from((x, y): (f32, f32)) -> Self {
442 Self { x, y }
443 }
444}
445
446#[derive(Clone, Debug)]
449#[non_exhaustive]
450pub enum Paint {
451 Solid(Rgba),
453 LinearGradient(LinearGradient),
455 RadialGradient(RadialGradient),
457}
458
459#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
465pub struct Rgba {
466 pub r: u8,
468 pub g: u8,
470 pub b: u8,
472 pub a: u8,
474}
475
476impl Rgba {
477 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
479 Self { r, g, b, a }
480 }
481
482 pub const fn opaque(r: u8, g: u8, b: u8) -> Self {
484 Self { r, g, b, a: 255 }
485 }
486}
487
488impl From<(u8, u8, u8, u8)> for Rgba {
489 fn from((r, g, b, a): (u8, u8, u8, u8)) -> Self {
490 Self { r, g, b, a }
491 }
492}
493
494impl From<(u8, u8, u8)> for Rgba {
495 fn from((r, g, b): (u8, u8, u8)) -> Self {
497 Self { r, g, b, a: 255 }
498 }
499}
500
501impl From<[u8; 4]> for Rgba {
502 fn from([r, g, b, a]: [u8; 4]) -> Self {
503 Self { r, g, b, a }
504 }
505}
506
507impl From<Rgba> for Paint {
508 fn from(color: Rgba) -> Self {
510 Paint::Solid(color)
511 }
512}
513
514#[derive(Clone, Debug)]
516pub struct LinearGradient {
517 pub start: Point,
519 pub end: Point,
521 pub stops: Vec<GradientStop>,
523 pub spread: SpreadMethod,
525}
526
527impl LinearGradient {
528 pub fn new(start: Point, end: Point) -> Self {
531 Self {
532 start,
533 end,
534 stops: Vec::new(),
535 spread: SpreadMethod::Pad,
536 }
537 }
538
539 pub fn with_stops(mut self, stops: Vec<GradientStop>) -> Self {
541 self.stops = stops;
542 self
543 }
544
545 pub fn with_stop(mut self, stop: GradientStop) -> Self {
547 self.stops.push(stop);
548 self
549 }
550
551 pub fn with_spread(mut self, spread: SpreadMethod) -> Self {
553 self.spread = spread;
554 self
555 }
556}
557
558#[derive(Clone, Debug)]
562pub struct RadialGradient {
563 pub center: Point,
565 pub radius: f32,
567 pub focal: Option<Point>,
570 pub stops: Vec<GradientStop>,
572 pub spread: SpreadMethod,
574}
575
576impl RadialGradient {
577 pub fn new(center: Point, radius: f32) -> Self {
580 Self {
581 center,
582 radius,
583 focal: None,
584 stops: Vec::new(),
585 spread: SpreadMethod::Pad,
586 }
587 }
588
589 pub fn with_focal(mut self, focal: Point) -> Self {
591 self.focal = Some(focal);
592 self
593 }
594
595 pub fn with_stops(mut self, stops: Vec<GradientStop>) -> Self {
597 self.stops = stops;
598 self
599 }
600
601 pub fn with_stop(mut self, stop: GradientStop) -> Self {
603 self.stops.push(stop);
604 self
605 }
606
607 pub fn with_spread(mut self, spread: SpreadMethod) -> Self {
609 self.spread = spread;
610 self
611 }
612}
613
614#[derive(Clone, Copy, Debug, PartialEq)]
616pub struct GradientStop {
617 pub offset: f32,
620 pub color: Rgba,
622}
623
624impl GradientStop {
625 pub const fn new(offset: f32, color: Rgba) -> Self {
627 Self { offset, color }
628 }
629}
630
631#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
634pub enum SpreadMethod {
635 #[default]
637 Pad,
638 Reflect,
640 Repeat,
642}
643
644#[derive(Clone, Debug)]
646pub struct Stroke {
647 pub width: f32,
649 pub paint: Paint,
651 pub cap: LineCap,
653 pub join: LineJoin,
655 pub miter_limit: f32,
657 pub dash: Option<DashPattern>,
659}
660
661impl Stroke {
662 pub fn solid(width: f32, color: Rgba) -> Self {
664 Self {
665 width,
666 paint: Paint::Solid(color),
667 cap: LineCap::Butt,
668 join: LineJoin::Miter,
669 miter_limit: 4.0,
670 dash: None,
671 }
672 }
673
674 pub fn new(width: f32, paint: Paint) -> Self {
678 Self {
679 width,
680 paint,
681 cap: LineCap::Butt,
682 join: LineJoin::Miter,
683 miter_limit: 4.0,
684 dash: None,
685 }
686 }
687
688 pub fn with_paint(mut self, paint: Paint) -> Self {
690 self.paint = paint;
691 self
692 }
693
694 pub fn with_cap(mut self, cap: LineCap) -> Self {
696 self.cap = cap;
697 self
698 }
699
700 pub fn with_join(mut self, join: LineJoin) -> Self {
702 self.join = join;
703 self
704 }
705
706 pub fn with_miter_limit(mut self, miter_limit: f32) -> Self {
708 self.miter_limit = miter_limit;
709 self
710 }
711
712 pub fn with_dash(mut self, dash: DashPattern) -> Self {
714 self.dash = Some(dash);
715 self
716 }
717}
718
719#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
721pub enum LineCap {
722 #[default]
724 Butt,
725 Round,
727 Square,
729}
730
731#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
733pub enum LineJoin {
734 #[default]
737 Miter,
738 Round,
740 Bevel,
742}
743
744#[derive(Clone, Debug, Default)]
748pub struct DashPattern {
749 pub array: Vec<f32>,
752 pub offset: f32,
754}
755
756impl DashPattern {
757 pub fn new(array: Vec<f32>) -> Self {
760 Self { array, offset: 0.0 }
761 }
762
763 pub fn with_offset(mut self, offset: f32) -> Self {
765 self.offset = offset;
766 self
767 }
768}
769
770#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
774pub enum FillRule {
775 #[default]
778 NonZero,
779 EvenOdd,
782}
783
784#[derive(Clone, Copy, Debug, PartialEq)]
796pub struct Transform2D {
797 pub a: f32,
799 pub b: f32,
801 pub c: f32,
803 pub d: f32,
805 pub e: f32,
807 pub f: f32,
809}
810
811impl Transform2D {
812 pub const fn identity() -> Self {
814 Self {
815 a: 1.0,
816 b: 0.0,
817 c: 0.0,
818 d: 1.0,
819 e: 0.0,
820 f: 0.0,
821 }
822 }
823
824 pub const fn translate(tx: f32, ty: f32) -> Self {
826 Self {
827 a: 1.0,
828 b: 0.0,
829 c: 0.0,
830 d: 1.0,
831 e: tx,
832 f: ty,
833 }
834 }
835
836 pub const fn scale(sx: f32, sy: f32) -> Self {
838 Self {
839 a: sx,
840 b: 0.0,
841 c: 0.0,
842 d: sy,
843 e: 0.0,
844 f: 0.0,
845 }
846 }
847
848 pub fn rotate(angle_radians: f32) -> Self {
852 let (s, c) = angle_radians.sin_cos();
853 Self {
854 a: c,
855 b: s,
856 c: -s,
857 d: c,
858 e: 0.0,
859 f: 0.0,
860 }
861 }
862
863 pub fn skew_x(angle_radians: f32) -> Self {
865 Self {
866 a: 1.0,
867 b: 0.0,
868 c: angle_radians.tan(),
869 d: 1.0,
870 e: 0.0,
871 f: 0.0,
872 }
873 }
874
875 pub fn skew_y(angle_radians: f32) -> Self {
877 Self {
878 a: 1.0,
879 b: angle_radians.tan(),
880 c: 0.0,
881 d: 1.0,
882 e: 0.0,
883 f: 0.0,
884 }
885 }
886
887 pub fn compose(&self, other: &Self) -> Self {
891 Self {
892 a: self.a * other.a + self.c * other.b,
893 b: self.b * other.a + self.d * other.b,
894 c: self.a * other.c + self.c * other.d,
895 d: self.b * other.c + self.d * other.d,
896 e: self.a * other.e + self.c * other.f + self.e,
897 f: self.b * other.e + self.d * other.f + self.f,
898 }
899 }
900
901 pub fn apply(&self, p: Point) -> Point {
903 Point {
904 x: self.a * p.x + self.c * p.y + self.e,
905 y: self.b * p.x + self.d * p.y + self.f,
906 }
907 }
908
909 pub fn is_identity(&self) -> bool {
913 *self == Self::identity()
914 }
915}
916
917impl Default for Transform2D {
918 fn default() -> Self {
919 Self::identity()
920 }
921}
922
923#[derive(Clone, Debug)]
930pub struct ImageRef {
931 pub frame: Box<crate::VideoFrame>,
934 pub bounds: Rect,
937 pub transform: Transform2D,
940}
941
942#[derive(Clone, Copy, Debug, Default, PartialEq)]
944pub struct Rect {
945 pub x: f32,
947 pub y: f32,
949 pub width: f32,
951 pub height: f32,
953}
954
955impl Rect {
956 pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
958 Self {
959 x,
960 y,
961 width,
962 height,
963 }
964 }
965}
966
967#[cfg(test)]
968mod tests {
969 use super::*;
970 use crate::time::TimeBase;
971
972 fn approx_point(a: Point, b: Point) -> bool {
973 (a.x - b.x).abs() < 1e-5 && (a.y - b.y).abs() < 1e-5
974 }
975
976 #[test]
977 fn path_builder_produces_command_sequence() {
978 let mut p = Path::new();
979 p.move_to(Point::new(0.0, 0.0))
980 .line_to(Point::new(10.0, 0.0))
981 .quad_to(Point::new(15.0, 5.0), Point::new(10.0, 10.0))
982 .cubic_to(
983 Point::new(5.0, 15.0),
984 Point::new(0.0, 10.0),
985 Point::new(0.0, 0.0),
986 )
987 .close();
988 assert_eq!(p.commands.len(), 5);
989 assert_eq!(p.commands[0], PathCommand::MoveTo(Point::new(0.0, 0.0)));
990 assert_eq!(p.commands[4], PathCommand::Close);
991 }
992
993 #[test]
994 fn transform_identity_round_trips() {
995 let id = Transform2D::identity();
996 assert!(id.is_identity());
997 let p = Point::new(3.5, -2.25);
998 assert_eq!(id.apply(p), p);
999 }
1000
1001 #[test]
1002 fn transform_translate_round_trip() {
1003 let t = Transform2D::translate(10.0, -5.0);
1004 assert_eq!(t.apply(Point::new(0.0, 0.0)), Point::new(10.0, -5.0));
1005 assert_eq!(t.apply(Point::new(1.0, 1.0)), Point::new(11.0, -4.0));
1006 }
1007
1008 #[test]
1009 fn transform_scale_round_trip() {
1010 let s = Transform2D::scale(2.0, 3.0);
1011 assert_eq!(s.apply(Point::new(1.0, 1.0)), Point::new(2.0, 3.0));
1012 assert_eq!(s.apply(Point::new(0.0, 0.0)), Point::new(0.0, 0.0));
1013 }
1014
1015 #[test]
1016 fn transform_rotate_quarter_turn() {
1017 let r = Transform2D::rotate(std::f32::consts::FRAC_PI_2);
1018 assert!(approx_point(
1021 r.apply(Point::new(1.0, 0.0)),
1022 Point::new(0.0, 1.0)
1023 ));
1024 assert!(approx_point(
1026 r.apply(Point::new(0.0, 1.0)),
1027 Point::new(-1.0, 0.0)
1028 ));
1029 }
1030
1031 #[test]
1032 fn transform_compose_identity_is_left_and_right_unit() {
1033 let t = Transform2D::translate(7.0, 11.0);
1034 let id = Transform2D::identity();
1035 assert_eq!(id.compose(&t), t);
1036 assert_eq!(t.compose(&id), t);
1037 }
1038
1039 #[test]
1040 fn transform_compose_translate_then_scale() {
1041 let scale = Transform2D::scale(10.0, 10.0);
1044 let translate = Transform2D::translate(2.0, 3.0);
1045 let composed = scale.compose(&translate);
1046 let result = composed.apply(Point::new(1.0, 1.0));
1047 assert!(approx_point(result, Point::new(30.0, 40.0)));
1048 }
1049
1050 #[test]
1051 fn transform_compose_matches_sequential_apply() {
1052 let a = Transform2D::rotate(0.5);
1054 let b = Transform2D::translate(3.0, -1.0);
1055 let composed = a.compose(&b);
1056 let p = Point::new(2.0, 5.0);
1057 let direct = composed.apply(p);
1058 let stepwise = a.apply(b.apply(p));
1059 assert!(approx_point(direct, stepwise));
1060 }
1061
1062 #[test]
1063 fn group_default_is_identity_opacity_one_no_clip() {
1064 let g = Group::default();
1065 assert!(g.transform.is_identity());
1066 assert_eq!(g.opacity, 1.0);
1067 assert!(g.clip.is_none());
1068 assert!(g.children.is_empty());
1069 }
1070
1071 #[test]
1072 fn group_nesting_with_transforms() {
1073 let inner = Group {
1080 transform: Transform2D::scale(2.0, 2.0),
1081 children: vec![Node::Path(PathNode {
1082 path: {
1083 let mut p = Path::new();
1084 p.move_to(Point::new(1.0, 1.0));
1085 p
1086 },
1087 fill: Some(Paint::Solid(Rgba::opaque(255, 0, 0))),
1088 stroke: None,
1089 fill_rule: FillRule::NonZero,
1090 })],
1091 ..Group::default()
1092 };
1093 let outer = Group {
1094 transform: Transform2D::translate(10.0, 10.0),
1095 children: vec![Node::Group(inner)],
1096 ..Group::default()
1097 };
1098 match &outer.children[0] {
1099 Node::Group(g) => {
1100 assert_eq!(g.transform, Transform2D::scale(2.0, 2.0));
1101 assert_eq!(g.children.len(), 1);
1102 }
1103 _ => panic!("expected a Group child"),
1104 }
1105 assert_eq!(outer.transform, Transform2D::translate(10.0, 10.0));
1106 }
1107
1108 #[test]
1109 fn vector_frame_construction() {
1110 let frame = VectorFrame {
1111 width: 100.0,
1112 height: 50.0,
1113 view_box: Some(ViewBox {
1114 min_x: 0.0,
1115 min_y: 0.0,
1116 width: 100.0,
1117 height: 50.0,
1118 }),
1119 root: Group::default(),
1120 pts: Some(0),
1121 time_base: TimeBase::new(1, 1000),
1122 };
1123 assert_eq!(frame.width, 100.0);
1124 assert_eq!(frame.height, 50.0);
1125 assert!(frame.view_box.is_some());
1126 assert_eq!(frame.pts, Some(0));
1127 }
1128
1129 #[test]
1130 fn rgba_constructors() {
1131 let c = Rgba::opaque(10, 20, 30);
1132 assert_eq!(c.a, 255);
1133 let c2 = Rgba::new(10, 20, 30, 128);
1134 assert_eq!(c2.a, 128);
1135 }
1136
1137 #[test]
1138 fn gradient_stop_round_trips() {
1139 let s = GradientStop::new(0.5, Rgba::opaque(255, 0, 0));
1140 assert_eq!(s.offset, 0.5);
1141 let s2 = GradientStop::new(0.5, Rgba::opaque(255, 0, 0));
1142 assert_eq!(s, s2);
1143 }
1144
1145 #[test]
1146 fn stroke_solid_defaults() {
1147 let s = Stroke::solid(2.0, Rgba::opaque(0, 0, 0));
1148 assert_eq!(s.width, 2.0);
1149 assert_eq!(s.cap, LineCap::Butt);
1150 assert_eq!(s.join, LineJoin::Miter);
1151 assert_eq!(s.miter_limit, 4.0);
1152 assert!(s.dash.is_none());
1153 }
1154
1155 #[test]
1156 fn soft_mask_construction_and_inspection() {
1157 fn rect_path() -> PathNode {
1160 let mut p = Path::new();
1161 p.move_to(Point::new(0.0, 0.0))
1162 .line_to(Point::new(10.0, 0.0))
1163 .line_to(Point::new(10.0, 10.0))
1164 .line_to(Point::new(0.0, 10.0))
1165 .close();
1166 PathNode {
1167 path: p,
1168 fill: Some(Paint::Solid(Rgba::opaque(255, 255, 255))),
1169 stroke: None,
1170 fill_rule: FillRule::NonZero,
1171 }
1172 }
1173 let n = Node::SoftMask {
1174 mask: Box::new(Node::Path(rect_path())),
1175 mask_kind: MaskKind::Luminance,
1176 content: Box::new(Node::Path(rect_path())),
1177 };
1178 match &n {
1179 Node::SoftMask {
1180 mask_kind, content, ..
1181 } => {
1182 assert_eq!(*mask_kind, MaskKind::Luminance);
1183 match content.as_ref() {
1184 Node::Path(_) => {}
1185 _ => panic!("expected Path content"),
1186 }
1187 }
1188 _ => panic!("expected SoftMask"),
1189 }
1190 }
1191
1192 #[test]
1193 fn mask_kind_default_is_luminance() {
1194 assert_eq!(MaskKind::default(), MaskKind::Luminance);
1195 }
1196
1197 #[test]
1198 fn vector_frame_default_is_empty_zero_size() {
1199 let f = VectorFrame::default();
1200 assert_eq!(f.width, 0.0);
1201 assert_eq!(f.height, 0.0);
1202 assert!(f.view_box.is_none());
1203 assert!(f.root.children.is_empty());
1204 assert!(f.pts.is_none());
1205 assert_eq!(f.time_base, TimeBase::new(1, 1));
1206 }
1207
1208 #[test]
1209 fn vector_frame_new_sets_canvas_size() {
1210 let f = VectorFrame::new(640.0, 480.0);
1211 assert_eq!(f.width, 640.0);
1212 assert_eq!(f.height, 480.0);
1213 assert!(f.view_box.is_none());
1214 assert!(f.root.children.is_empty());
1215 assert!(f.pts.is_none());
1216 }
1217
1218 #[test]
1219 fn vector_frame_builder_chain() {
1220 let vb = ViewBox::new(0.0, 0.0, 100.0, 100.0);
1221 let f = VectorFrame::new(100.0, 100.0)
1222 .with_view_box(vb)
1223 .with_pts(42)
1224 .with_time_base(TimeBase::new(1, 90_000));
1225 assert_eq!(f.view_box, Some(vb));
1226 assert_eq!(f.pts, Some(42));
1227 assert_eq!(f.time_base, TimeBase::new(1, 90_000));
1228 }
1229
1230 #[test]
1231 fn vector_frame_with_root_replaces_root() {
1232 let root = Group::new().with_opacity(0.5);
1233 let f = VectorFrame::new(10.0, 10.0).with_root(root);
1234 assert_eq!(f.root.opacity, 0.5);
1235 }
1236
1237 #[test]
1238 fn view_box_new_round_trips_fields() {
1239 let vb = ViewBox::new(1.0, 2.0, 3.0, 4.0);
1240 assert_eq!(vb.min_x, 1.0);
1241 assert_eq!(vb.min_y, 2.0);
1242 assert_eq!(vb.width, 3.0);
1243 assert_eq!(vb.height, 4.0);
1244 }
1245
1246 #[test]
1247 fn rect_new_round_trips_fields() {
1248 let r = Rect::new(1.0, 2.0, 3.0, 4.0);
1249 assert_eq!(r.x, 1.0);
1250 assert_eq!(r.y, 2.0);
1251 assert_eq!(r.width, 3.0);
1252 assert_eq!(r.height, 4.0);
1253 }
1254
1255 #[test]
1256 fn group_new_matches_default() {
1257 let a = Group::new();
1258 let b = Group::default();
1259 assert!(a.transform.is_identity());
1260 assert_eq!(a.opacity, b.opacity);
1261 assert!(a.clip.is_none());
1262 assert_eq!(a.children.len(), b.children.len());
1263 assert_eq!(a.cache_key, b.cache_key);
1264 }
1265
1266 #[test]
1267 fn group_builder_chain() {
1268 let mut clip = Path::new();
1269 clip.move_to(Point::new(0.0, 0.0))
1270 .line_to(Point::new(1.0, 1.0))
1271 .close();
1272 let g = Group::new()
1273 .with_transform(Transform2D::translate(5.0, 7.0))
1274 .with_opacity(0.25)
1275 .with_clip(clip)
1276 .with_cache_key(0xdead_beef);
1277 assert_eq!(g.transform, Transform2D::translate(5.0, 7.0));
1278 assert_eq!(g.opacity, 0.25);
1279 assert!(g.clip.is_some());
1280 assert_eq!(g.cache_key, Some(0xdead_beef));
1281 }
1282
1283 #[test]
1284 fn group_with_child_appends() {
1285 let g = Group::new()
1286 .with_child(Node::Group(Group::new()))
1287 .with_child(Node::Group(Group::new().with_opacity(0.5)));
1288 assert_eq!(g.children.len(), 2);
1289 match &g.children[1] {
1290 Node::Group(inner) => assert_eq!(inner.opacity, 0.5),
1291 _ => panic!("expected Group child"),
1292 }
1293 }
1294
1295 #[test]
1296 fn group_with_children_replaces_list() {
1297 let g = Group::new()
1298 .with_child(Node::Group(Group::new()))
1299 .with_children(vec![Node::Group(Group::new().with_opacity(0.1))]);
1300 assert_eq!(g.children.len(), 1);
1301 match &g.children[0] {
1302 Node::Group(inner) => assert_eq!(inner.opacity, 0.1),
1303 _ => panic!("expected Group child"),
1304 }
1305 }
1306
1307 #[test]
1308 fn path_node_new_then_builder() {
1309 let mut p = Path::new();
1310 p.move_to(Point::new(0.0, 0.0))
1311 .line_to(Point::new(10.0, 0.0));
1312 let n = PathNode::new(p)
1313 .with_fill(Paint::Solid(Rgba::opaque(255, 0, 0)))
1314 .with_stroke(Stroke::solid(1.0, Rgba::opaque(0, 0, 0)))
1315 .with_fill_rule(FillRule::EvenOdd);
1316 assert!(n.fill.is_some());
1317 assert!(n.stroke.is_some());
1318 assert_eq!(n.fill_rule, FillRule::EvenOdd);
1319 }
1320
1321 #[test]
1322 fn path_node_new_defaults() {
1323 let n = PathNode::new(Path::new());
1324 assert!(n.fill.is_none());
1325 assert!(n.stroke.is_none());
1326 assert_eq!(n.fill_rule, FillRule::NonZero);
1327 }
1328
1329 #[test]
1330 fn point_from_array_and_tuple() {
1331 let p1: Point = [1.0_f32, 2.0_f32].into();
1332 let p2: Point = (3.0_f32, 4.0_f32).into();
1333 assert_eq!(p1, Point::new(1.0, 2.0));
1334 assert_eq!(p2, Point::new(3.0, 4.0));
1335 }
1336
1337 #[test]
1338 fn rgba_from_tuples_and_array() {
1339 let a: Rgba = (10u8, 20u8, 30u8, 40u8).into();
1340 let b: Rgba = (50u8, 60u8, 70u8).into();
1341 let c: Rgba = [1u8, 2u8, 3u8, 4u8].into();
1342 assert_eq!(a, Rgba::new(10, 20, 30, 40));
1343 assert_eq!(b, Rgba::opaque(50, 60, 70));
1344 assert_eq!(c, Rgba::new(1, 2, 3, 4));
1345 }
1346
1347 #[test]
1348 fn paint_from_rgba_wraps_solid() {
1349 let p: Paint = Rgba::opaque(1, 2, 3).into();
1350 match p {
1351 Paint::Solid(c) => assert_eq!(c, Rgba::opaque(1, 2, 3)),
1352 _ => panic!("expected Paint::Solid"),
1353 }
1354 }
1355
1356 #[test]
1357 fn linear_gradient_new_then_builder() {
1358 let g = LinearGradient::new(Point::new(0.0, 0.0), Point::new(1.0, 0.0))
1359 .with_stop(GradientStop::new(0.0, Rgba::opaque(0, 0, 0)))
1360 .with_stop(GradientStop::new(1.0, Rgba::opaque(255, 255, 255)))
1361 .with_spread(SpreadMethod::Reflect);
1362 assert_eq!(g.start, Point::new(0.0, 0.0));
1363 assert_eq!(g.end, Point::new(1.0, 0.0));
1364 assert_eq!(g.stops.len(), 2);
1365 assert_eq!(g.spread, SpreadMethod::Reflect);
1366 }
1367
1368 #[test]
1369 fn linear_gradient_with_stops_replaces() {
1370 let g = LinearGradient::new(Point::new(0.0, 0.0), Point::new(1.0, 0.0))
1371 .with_stop(GradientStop::new(0.5, Rgba::opaque(0, 0, 0)))
1372 .with_stops(vec![GradientStop::new(0.0, Rgba::opaque(1, 1, 1))]);
1373 assert_eq!(g.stops.len(), 1);
1374 assert_eq!(g.stops[0].offset, 0.0);
1375 }
1376
1377 #[test]
1378 fn radial_gradient_new_then_builder() {
1379 let g = RadialGradient::new(Point::new(5.0, 5.0), 10.0)
1380 .with_focal(Point::new(4.0, 4.0))
1381 .with_stop(GradientStop::new(0.0, Rgba::opaque(0, 0, 0)))
1382 .with_spread(SpreadMethod::Repeat);
1383 assert_eq!(g.center, Point::new(5.0, 5.0));
1384 assert_eq!(g.radius, 10.0);
1385 assert_eq!(g.focal, Some(Point::new(4.0, 4.0)));
1386 assert_eq!(g.stops.len(), 1);
1387 assert_eq!(g.spread, SpreadMethod::Repeat);
1388 }
1389
1390 #[test]
1391 fn radial_gradient_with_stops_replaces() {
1392 let g = RadialGradient::new(Point::new(0.0, 0.0), 1.0)
1393 .with_stop(GradientStop::new(0.5, Rgba::opaque(0, 0, 0)))
1394 .with_stops(vec![GradientStop::new(1.0, Rgba::opaque(1, 1, 1))]);
1395 assert_eq!(g.stops.len(), 1);
1396 assert_eq!(g.stops[0].offset, 1.0);
1397 }
1398
1399 #[test]
1400 fn stroke_new_defaults() {
1401 let s = Stroke::new(3.0, Paint::Solid(Rgba::opaque(0, 0, 0)));
1402 assert_eq!(s.width, 3.0);
1403 assert_eq!(s.cap, LineCap::Butt);
1404 assert_eq!(s.join, LineJoin::Miter);
1405 assert_eq!(s.miter_limit, 4.0);
1406 assert!(s.dash.is_none());
1407 }
1408
1409 #[test]
1410 fn stroke_builder_chain() {
1411 let s = Stroke::solid(1.0, Rgba::opaque(0, 0, 0))
1412 .with_cap(LineCap::Round)
1413 .with_join(LineJoin::Bevel)
1414 .with_miter_limit(10.0)
1415 .with_dash(DashPattern::new(vec![2.0, 1.0]).with_offset(0.5))
1416 .with_paint(Paint::Solid(Rgba::opaque(128, 128, 128)));
1417 assert_eq!(s.cap, LineCap::Round);
1418 assert_eq!(s.join, LineJoin::Bevel);
1419 assert_eq!(s.miter_limit, 10.0);
1420 let d = s.dash.expect("dash set");
1421 assert_eq!(d.array, vec![2.0, 1.0]);
1422 assert_eq!(d.offset, 0.5);
1423 match s.paint {
1424 Paint::Solid(c) => assert_eq!(c, Rgba::opaque(128, 128, 128)),
1425 _ => panic!("expected Paint::Solid"),
1426 }
1427 }
1428
1429 #[test]
1430 fn dash_pattern_new_zero_offset() {
1431 let d = DashPattern::new(vec![1.0, 2.0, 3.0]);
1432 assert_eq!(d.array, vec![1.0, 2.0, 3.0]);
1433 assert_eq!(d.offset, 0.0);
1434 }
1435
1436 #[test]
1437 fn dash_pattern_with_offset_sets_phase() {
1438 let d = DashPattern::new(vec![1.0]).with_offset(0.25);
1439 assert_eq!(d.offset, 0.25);
1440 }
1441}