summaryrefslogtreecommitdiff
path: root/rust/crates/mask-defs/tests/render.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rust/crates/mask-defs/tests/render.rs')
-rw-r--r--rust/crates/mask-defs/tests/render.rs372
1 files changed, 372 insertions, 0 deletions
diff --git a/rust/crates/mask-defs/tests/render.rs b/rust/crates/mask-defs/tests/render.rs
new file mode 100644
index 00000000..ca8e843a
--- /dev/null
+++ b/rust/crates/mask-defs/tests/render.rs
@@ -0,0 +1,372 @@
+use mask_defs::{feather_for_gpu, is_active, render_mask_body, render_mask_stroke};
+use scene::{
+ FreeformPathMaskParams, FreeformPathPoint, Mask, RectangleMaskParams, SplitMaskParams,
+ StrokeAlign, TextDecoration, TextFontStyle, TextFontWeight, TextMaskParams,
+};
+use tiny_skia::Pixmap;
+
+fn box_params() -> RectangleMaskParams {
+ RectangleMaskParams {
+ feather: 0.0,
+ inverted: false,
+ stroke_color: "#ffffff".to_string(),
+ stroke_width: 0.0,
+ stroke_align: StrokeAlign::Center,
+ center_x: 0.0,
+ center_y: 0.0,
+ width: 0.6,
+ height: 0.6,
+ rotation: 0.0,
+ scale: 1.0,
+ }
+}
+
+fn split_params() -> SplitMaskParams {
+ SplitMaskParams {
+ feather: 0.0,
+ inverted: false,
+ stroke_color: "#ffffff".to_string(),
+ stroke_width: 0.0,
+ stroke_align: StrokeAlign::Center,
+ center_x: 0.0,
+ center_y: 0.0,
+ rotation: 0.0,
+ }
+}
+
+fn text_params(content: &str) -> TextMaskParams {
+ TextMaskParams {
+ feather: 0.0,
+ inverted: false,
+ stroke_color: "#ffffff".to_string(),
+ stroke_width: 0.0,
+ stroke_align: StrokeAlign::Center,
+ content: content.to_string(),
+ font_size: 15.0,
+ font_family: "Arial".to_string(),
+ font_weight: TextFontWeight::Normal,
+ font_style: TextFontStyle::Normal,
+ text_decoration: TextDecoration::None,
+ letter_spacing: 0.0,
+ line_height: 1.2,
+ center_x: 0.0,
+ center_y: 0.0,
+ rotation: 0.0,
+ scale: 1.0,
+ }
+}
+
+fn freeform_params(points: Vec<FreeformPathPoint>, closed: bool) -> FreeformPathMaskParams {
+ FreeformPathMaskParams {
+ feather: 0.0,
+ inverted: false,
+ stroke_color: "#ffffff".to_string(),
+ stroke_width: 0.0,
+ stroke_align: StrokeAlign::Center,
+ path: points,
+ closed,
+ center_x: 0.0,
+ center_y: 0.0,
+ rotation: 0.0,
+ scale: 1.0,
+ }
+}
+
+fn point(id: &str, x: f64, y: f64) -> FreeformPathPoint {
+ FreeformPathPoint {
+ id: id.to_string(),
+ x,
+ y,
+ in_x: 0.0,
+ in_y: 0.0,
+ out_x: 0.0,
+ out_y: 0.0,
+ }
+}
+
+fn alpha_at(pixmap: &Pixmap, x: u32, y: u32) -> u8 {
+ pixmap.pixel(x, y).map(|p| p.alpha()).unwrap_or(0)
+}
+
+fn assert_opaque(pixmap: &Pixmap, x: u32, y: u32) {
+ assert!(
+ alpha_at(pixmap, x, y) > 250,
+ "expected opaque at ({x}, {y}), got {}",
+ alpha_at(pixmap, x, y)
+ );
+}
+
+fn assert_transparent(pixmap: &Pixmap, x: u32, y: u32) {
+ assert!(
+ alpha_at(pixmap, x, y) < 5,
+ "expected transparent at ({x}, {y}), got {}",
+ alpha_at(pixmap, x, y)
+ );
+}
+
+fn box_mask(kind: &str, params: RectangleMaskParams) -> Mask {
+ match kind {
+ "cinematic-bars" => Mask::CinematicBars { id: "m".into(), params },
+ "rectangle" => Mask::Rectangle { id: "m".into(), params },
+ "ellipse" => Mask::Ellipse { id: "m".into(), params },
+ "heart" => Mask::Heart { id: "m".into(), params },
+ "diamond" => Mask::Diamond { id: "m".into(), params },
+ _ => Mask::Star { id: "m".into(), params },
+ }
+}
+
+#[test]
+fn rectangle_body_center_opaque_corners_transparent() {
+ let mask = box_mask("rectangle", box_params());
+ let pixmap = render_mask_body(&mask, 200, 200).unwrap();
+ assert_opaque(&pixmap, 100, 100);
+ assert_opaque(&pixmap, 50, 50);
+ assert_transparent(&pixmap, 20, 20);
+ assert_transparent(&pixmap, 180, 180);
+}
+
+#[test]
+fn rectangle_body_rotated_90_swaps_extents() {
+ let mut params = box_params();
+ params.width = 0.6;
+ params.height = 0.3;
+ params.rotation = 90.0;
+ let mask = box_mask("rectangle", params);
+ let pixmap = render_mask_body(&mask, 200, 100).unwrap();
+ assert_opaque(&pixmap, 100, 50);
+ assert_opaque(&pixmap, 100, 10);
+ assert_transparent(&pixmap, 150, 50);
+}
+
+#[test]
+fn rectangle_stroke_ring_present_when_stroke_width_positive() {
+ let mut params = box_params();
+ params.stroke_width = 10.0;
+ let mask = box_mask("rectangle", params);
+ let stroke = render_mask_stroke(&mask, 200, 200).unwrap().unwrap();
+ assert_opaque(&stroke, 100, 40);
+ assert_transparent(&stroke, 100, 100);
+ assert_transparent(&stroke, 10, 10);
+}
+
+#[test]
+fn stroke_is_none_when_stroke_width_zero() {
+ let mask = box_mask("rectangle", box_params());
+ assert!(render_mask_stroke(&mask, 200, 200).unwrap().is_none());
+}
+
+#[test]
+fn ellipse_body_center_opaque_corners_transparent() {
+ let mask = box_mask("ellipse", box_params());
+ let pixmap = render_mask_body(&mask, 200, 200).unwrap();
+ assert_opaque(&pixmap, 100, 100);
+ assert_opaque(&pixmap, 130, 100);
+ assert_transparent(&pixmap, 45, 45);
+ assert_transparent(&pixmap, 10, 10);
+}
+
+#[test]
+fn heart_body_region_coverage() {
+ let mask = box_mask("heart", box_params());
+ let pixmap = render_mask_body(&mask, 200, 200).unwrap();
+ assert_opaque(&pixmap, 100, 110);
+ assert_opaque(&pixmap, 75, 80);
+ assert_transparent(&pixmap, 100, 60);
+ assert_transparent(&pixmap, 10, 10);
+}
+
+#[test]
+fn diamond_body_region_coverage() {
+ let mask = box_mask("diamond", box_params());
+ let pixmap = render_mask_body(&mask, 200, 200).unwrap();
+ assert_opaque(&pixmap, 100, 100);
+ assert_opaque(&pixmap, 130, 100);
+ assert_transparent(&pixmap, 145, 145);
+ assert_transparent(&pixmap, 10, 10);
+}
+
+#[test]
+fn star_body_region_coverage() {
+ let mask = box_mask("star", box_params());
+ let pixmap = render_mask_body(&mask, 200, 200).unwrap();
+ assert_opaque(&pixmap, 100, 100);
+ assert_opaque(&pixmap, 100, 52);
+ assert_transparent(&pixmap, 121, 71);
+ assert_transparent(&pixmap, 10, 10);
+}
+
+#[test]
+fn cinematic_bars_body_full_width_band() {
+ let mut params = box_params();
+ params.width = 1.5;
+ params.height = 0.6;
+ let mask = box_mask("cinematic-bars", params);
+ let pixmap = render_mask_body(&mask, 200, 100).unwrap();
+ assert_opaque(&pixmap, 100, 50);
+ assert_opaque(&pixmap, 5, 50);
+ assert_transparent(&pixmap, 100, 10);
+ assert_transparent(&pixmap, 100, 95);
+}
+
+#[test]
+fn split_body_zero_feather_hard_edge() {
+ let mask = Mask::Split { id: "m".into(), params: split_params() };
+ let pixmap = render_mask_body(&mask, 200, 100).unwrap();
+ assert_opaque(&pixmap, 150, 50);
+ assert_opaque(&pixmap, 101, 50);
+ assert_transparent(&pixmap, 99, 50);
+ assert_transparent(&pixmap, 50, 50);
+}
+
+#[test]
+fn split_body_feather_gradient_band() {
+ let mut params = split_params();
+ params.feather = 40.0;
+ let mask = Mask::Split { id: "m".into(), params };
+ let pixmap = render_mask_body(&mask, 200, 100).unwrap();
+ assert_opaque(&pixmap, 150, 50);
+ assert_transparent(&pixmap, 50, 50);
+ let mid = alpha_at(&pixmap, 100, 50);
+ assert!(
+ (108..=148).contains(&mid),
+ "expected mid alpha ~128 at split line, got {mid}"
+ );
+ let quarter = alpha_at(&pixmap, 110, 50) as u16;
+ assert!(
+ (170..=215).contains(&quarter),
+ "expected alpha ~191 at feather quarter, got {quarter}"
+ );
+}
+
+#[test]
+fn split_stroke_segment_vertical_and_horizontal() {
+ let mut params = split_params();
+ params.stroke_width = 4.0;
+ let mask = Mask::Split { id: "m".into(), params };
+ let stroke = render_mask_stroke(&mask, 200, 100).unwrap().unwrap();
+ assert_opaque(&stroke, 100, 50);
+ assert_transparent(&stroke, 106, 50);
+
+ let mut rotated = split_params();
+ rotated.rotation = 90.0;
+ rotated.stroke_width = 4.0;
+ let mask = Mask::Split { id: "m".into(), params: rotated };
+ let stroke = render_mask_stroke(&mask, 200, 100).unwrap().unwrap();
+ assert_opaque(&stroke, 50, 50);
+ assert_transparent(&stroke, 50, 56);
+}
+
+#[test]
+fn freeform_closed_triangle_region_coverage() {
+ let points = vec![
+ point("a", -0.4, -0.4),
+ point("b", 0.4, -0.4),
+ point("c", 0.0, 0.4),
+ ];
+ let mask = Mask::Freeform { id: "m".into(), params: freeform_params(points, true) };
+ let pixmap = render_mask_body(&mask, 200, 200).unwrap();
+ assert_opaque(&pixmap, 100, 110);
+ assert_transparent(&pixmap, 10, 10);
+ assert_transparent(&pixmap, 100, 180);
+}
+
+#[test]
+fn freeform_open_path_is_inactive_and_empty() {
+ let points = vec![
+ point("a", -0.4, -0.4),
+ point("b", 0.4, -0.4),
+ point("c", 0.0, 0.4),
+ ];
+ let mask = Mask::Freeform { id: "m".into(), params: freeform_params(points, false) };
+ assert!(!is_active(&mask));
+ let pixmap = render_mask_body(&mask, 200, 200).unwrap();
+ for pixel in pixmap.pixels() {
+ assert_eq!(pixel.alpha(), 0);
+ }
+ let mut stroke_params = freeform_params(
+ vec![point("a", -0.4, -0.4), point("b", 0.4, -0.4), point("c", 0.0, 0.4)],
+ false,
+ );
+ stroke_params.stroke_width = 5.0;
+ let mask = Mask::Freeform { id: "m".into(), params: stroke_params };
+ let stroke = render_mask_stroke(&mask, 200, 200).unwrap().unwrap();
+ for pixel in stroke.pixels() {
+ assert_eq!(pixel.alpha(), 0);
+ }
+}
+
+#[test]
+fn text_body_rasterizes_glyphs() {
+ let mask = Mask::Text { id: "m".into(), params: text_params("Hi") };
+ let pixmap = render_mask_body(&mask, 200, 200).unwrap();
+ let opaque_count = pixmap.pixels().iter().filter(|p| p.alpha() > 200).count();
+ assert!(opaque_count > 20, "expected rasterized glyphs, got {opaque_count} opaque pixels");
+ assert_transparent(&pixmap, 2, 2);
+ assert_transparent(&pixmap, 197, 197);
+}
+
+#[test]
+fn text_stroke_rasterizes_ring() {
+ let mut params = text_params("Hi");
+ params.stroke_width = 3.0;
+ let mask = Mask::Text { id: "m".into(), params };
+ let stroke = render_mask_stroke(&mask, 200, 200).unwrap().unwrap();
+ let opaque_count = stroke.pixels().iter().filter(|p| p.alpha() > 200).count();
+ assert!(opaque_count > 20, "expected stroked glyphs, got {opaque_count} opaque pixels");
+}
+
+#[test]
+fn is_active_rules_match_web() {
+ let text = Mask::Text { id: "m".into(), params: text_params(" ") };
+ assert!(!is_active(&text));
+ let text = Mask::Text { id: "m".into(), params: text_params("Mask") };
+ assert!(is_active(&text));
+ assert!(is_active(&box_mask("rectangle", box_params())));
+ assert!(is_active(&Mask::Split { id: "m".into(), params: split_params() }));
+}
+
+#[test]
+fn feather_for_gpu_zeroes_draw_with_feather_bodies() {
+ let mut split = split_params();
+ split.feather = 40.0;
+ let mask = Mask::Split { id: "m".into(), params: split };
+ assert_eq!(feather_for_gpu(&mask), 0.0);
+
+ let mut rect = box_params();
+ rect.feather = 40.0;
+ let mask = box_mask("rectangle", rect);
+ assert_eq!(feather_for_gpu(&mask), 40.0);
+
+ let mut text = text_params("Hi");
+ text.feather = 25.0;
+ let mask = Mask::Text { id: "m".into(), params: text };
+ assert_eq!(feather_for_gpu(&mask), 25.0);
+}
+
+#[test]
+fn stroke_align_outside_keeps_outer_ring() {
+ let mut params = box_params();
+ params.stroke_width = 10.0;
+ params.stroke_align = StrokeAlign::Outside;
+ let mask = Mask::Freeform {
+ id: "m".into(),
+ params: FreeformPathMaskParams {
+ stroke_width: 10.0,
+ stroke_align: StrokeAlign::Outside,
+ ..freeform_params(
+ vec![
+ point("a", -0.3, -0.3),
+ point("b", 0.3, -0.3),
+ point("c", 0.3, 0.3),
+ point("d", -0.3, 0.3),
+ ],
+ true,
+ )
+ },
+ };
+ let _ = params;
+ let stroke = render_mask_stroke(&mask, 200, 200).unwrap().unwrap();
+ assert_opaque(&stroke, 100, 35);
+ assert_transparent(&stroke, 100, 100);
+ assert_transparent(&stroke, 100, 43);
+}