diff --git a/crates/obscura-render/src/paint.rs b/crates/obscura-render/src/paint.rs index 5dce5bc..f5d4050 100644 --- a/crates/obscura-render/src/paint.rs +++ b/crates/obscura-render/src/paint.rs @@ -775,6 +775,7 @@ fn paint_laid_dom_scrolled( base_url, &visible_rect, fill, + style.background_radial_gradient.as_ref(), style.background_gradient.as_ref(), style.background_conic_gradient.as_ref(), style.mask_size, @@ -2593,6 +2594,31 @@ fn linear_color_at( sample_normalized_stops(stops, position) } +fn radial_color_at( + rect: &crate::Rect, + center: (f32, f32), + stops: &[(f32, [u8; 4])], + x: f32, + y: f32, +) -> [u8; 4] { + let center_x = rect.x + rect.width * center.0; + let center_y = rect.y + rect.height * center.1; + let radius = [ + (rect.x - center_x).hypot(rect.y - center_y), + (rect.x + rect.width - center_x).hypot(rect.y - center_y), + (rect.x - center_x).hypot(rect.y + rect.height - center_y), + (rect.x + rect.width - center_x).hypot(rect.y + rect.height - center_y), + ] + .into_iter() + .fold(0.0, f32::max); + let position = if radius <= f32::EPSILON { + 0.0 + } else { + (x - center_x).hypot(y - center_y) / radius + }; + sample_normalized_stops(stops, position) +} + fn premultiplied(color: [u8; 4]) -> tiny_skia::PremultipliedColorU8 { let alpha = color[3] as u32; tiny_skia::PremultipliedColorU8::from_rgba( @@ -2836,6 +2862,7 @@ fn paint_in_flow_generated_box( base_url, &visible, fill, + style.background_radial_gradient.as_ref(), style.background_gradient.as_ref(), style.background_conic_gradient.as_ref(), style.mask_size, @@ -3025,6 +3052,7 @@ fn paint_positioned_pseudo( base_url, &visible, fill, + style.background_radial_gradient.as_ref(), style.background_gradient.as_ref(), style.background_conic_gradient.as_ref(), style.mask_size, @@ -4257,6 +4285,7 @@ fn paint_mask( base_url: Option<&str>, rect: &crate::Rect, fill: [u8; 4], + radial_gradient: Option<&((f32, f32), Vec<([u8; 4], Option)>)>, linear_gradient: Option<&(f32, Vec<([u8; 4], Option)>)>, conic_gradient: Option<&(f32, (f32, f32), Vec<([u8; 4], Option)>)>, mask_size: Option<(f32, f32)>, @@ -4293,6 +4322,8 @@ fn paint_mask( linear_gradient.map(|(_, stops)| normalized_stops(stops)); let normalized_conic = conic_gradient.map(|(_, _, stops)| normalized_stops(stops)); + let normalized_radial = + radial_gradient.map(|(_, stops)| normalized_stops(stops)); let Some(mut recolored) = Pixmap::new(box_width, box_height) else { return false; }; @@ -4321,6 +4352,10 @@ fn paint_mask( (linear_gradient, normalized_linear.as_deref()) { linear_color_at(rect, *angle, stops, sample_x, sample_y) + } else if let (Some((center, _)), Some(stops)) = + (radial_gradient, normalized_radial.as_deref()) + { + radial_color_at(rect, *center, stops, sample_x, sample_y) } else { fill }; @@ -4825,6 +4860,84 @@ mod tests { ); } + #[test] + fn repeated_data_svg_masks_sample_radial_sources_on_every_box_path() { + let tree = parse_html( + r##" +
+
+
+
+
+
+ "##, + ); + let pixmap = paint_dom(&tree, (300.0, 160.0), None).expect("pixmap"); + let count_pixels = |left: u32, top: u32, predicate: fn(u8, u8, u8) -> bool| { + (top..top + 66) + .flat_map(|y| (left..left + 88).map(move |x| (x, y))) + .filter(|&(x, y)| { + let pixel = pixmap.pixel(x, y).expect("pixel"); + predicate(pixel.red(), pixel.green(), pixel.blue()) + }) + .count() + }; + let is_radial_color = |red, green, blue| { + red > 70 && blue > 100 && blue as u16 > green as u16 * 2 + }; + for (name, left) in [("ordinary element", 0), ("in-flow pseudo", 100), ("positioned pseudo", 200)] { + let colored = count_pixels(left, 0, is_radial_color); + assert!( + colored > 20, + "{name} must sample the radial source through the repeated SVG mask, found {colored} colored pixels" + ); + let black = count_pixels(left, 0, |red, green, blue| red < 20 && green < 20 && blue < 20); + assert_eq!( + black, 0, + "{name} must not fall back to the default black mask fill" + ); + } + assert!( + count_pixels(0, 80, |red, green, blue| green > 100 && red < 40 && blue < 40) > 20, + "solid mask sources must keep painting" + ); + assert!( + count_pixels(100, 80, |red, green, blue| (red > 100 || blue > 100) && green < 80) > 20, + "linear-gradient mask sources must keep painting" + ); + assert!( + count_pixels(200, 80, |red, green, blue| (red > 100 || blue > 100) && green < 80) > 20, + "conic-gradient mask sources must keep painting" + ); + } + #[test] fn paints_empty_in_flow_generated_block_at_its_layout_rect() { let tree = parse_html(