diff options
Diffstat (limited to 'rust/crates/mask-defs/tests')
| -rw-r--r-- | rust/crates/mask-defs/tests/render.rs | 372 |
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); +} |
