diff --git a/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.cpp b/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.cpp index 06bf2ee494..0341ac86c3 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.cpp +++ b/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.cpp @@ -25,7 +25,7 @@ CSSPixelPoint SVGGeometryBox::viewbox_origin() const return { svg_box->view_box().value().min_x, svg_box->view_box().value().min_y }; } -Optional SVGGeometryBox::layout_transform() const +Optional SVGGeometryBox::layout_transform(Gfx::AffineTransform additional_svg_transform) const { auto& geometry_element = dom_node(); auto transform = geometry_element.get_transform(); @@ -49,7 +49,9 @@ Optional SVGGeometryBox::layout_transform() const auto scaled_bounding_box = original_bounding_box.scaled(scaling, scaling); paint_offset = (paintable_box()->absolute_rect().location() - svg_box->paintable_box()->absolute_rect().location()).to_type() - scaled_bounding_box.location(); } - return Gfx::AffineTransform {}.translate(paint_offset).scale(scaling, scaling).translate(-origin).multiply(transform); + // Note: The "additional_svg_transform" is applied during mask painting to transform the mask element to match its target. + // It has to be applied while still in the SVG coordinate space. + return Gfx::AffineTransform {}.translate(paint_offset).scale(scaling, scaling).translate(-origin).multiply(additional_svg_transform).multiply(transform); } JS::GCPtr SVGGeometryBox::create_paintable() const diff --git a/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.h b/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.h index 42b177d0fa..ac615bbd5b 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.h +++ b/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.h @@ -22,7 +22,7 @@ public: SVG::SVGGeometryElement& dom_node() { return static_cast(SVGGraphicsBox::dom_node()); } SVG::SVGGeometryElement const& dom_node() const { return static_cast(SVGGraphicsBox::dom_node()); } - Optional layout_transform() const; + Optional layout_transform(Gfx::AffineTransform additional_svg_transform) const; virtual JS::GCPtr create_paintable() const override; diff --git a/Userland/Libraries/LibWeb/Painting/PaintContext.h b/Userland/Libraries/LibWeb/Painting/PaintContext.h index 343d46502a..fd427d0c89 100644 --- a/Userland/Libraries/LibWeb/Painting/PaintContext.h +++ b/Userland/Libraries/LibWeb/Painting/PaintContext.h @@ -34,6 +34,16 @@ public: bool has_focus() const { return m_focus; } void set_has_focus(bool focus) { m_focus = focus; } + void set_svg_transform(Gfx::AffineTransform transform) + { + m_svg_transform = transform; + } + + Gfx::AffineTransform const& svg_transform() const + { + return m_svg_transform; + } + DevicePixels enclosing_device_pixels(CSSPixels css_pixels) const; DevicePixels floored_device_pixels(CSSPixels css_pixels) const; DevicePixels rounded_device_pixels(CSSPixels css_pixels) const; @@ -70,6 +80,7 @@ private: bool m_should_show_line_box_borders { false }; bool m_focus { false }; CSSPixelPoint m_scroll_offset; + Gfx::AffineTransform m_svg_transform; }; } diff --git a/Userland/Libraries/LibWeb/Painting/PaintableBox.h b/Userland/Libraries/LibWeb/Painting/PaintableBox.h index 330887da22..1aa699d653 100644 --- a/Userland/Libraries/LibWeb/Painting/PaintableBox.h +++ b/Userland/Libraries/LibWeb/Painting/PaintableBox.h @@ -27,6 +27,9 @@ public: [[nodiscard]] bool is_visible() const; + virtual Optional get_masking_area() const { return {}; } + virtual void apply_mask(PaintContext&, Gfx::Bitmap&, CSSPixelRect const&) const {}; + Layout::Box& layout_box() { return static_cast(Paintable::layout_node()); } Layout::Box const& layout_box() const { return static_cast(Paintable::layout_node()); } diff --git a/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp b/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp index da636a2791..beb80fd7c8 100644 --- a/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp +++ b/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp @@ -33,7 +33,7 @@ Optional SVGGeometryPaintable::hit_test(CSSPixelPoint position, H if (!result.has_value()) return {}; auto& geometry_element = layout_box().dom_node(); - if (auto transform = layout_box().layout_transform(); transform.has_value()) { + if (auto transform = layout_box().layout_transform({}); transform.has_value()) { auto transformed_bounding_box = transform->map_to_quad( const_cast(geometry_element).get_path().bounding_box()); if (!transformed_bounding_box.contains(position.to_type())) @@ -78,7 +78,7 @@ void SVGGeometryPaintable::paint(PaintContext& context, PaintPhase phase) const auto maybe_view_box = geometry_element.view_box(); - auto transform = layout_box().layout_transform(); + auto transform = layout_box().layout_transform(context.svg_transform()); if (!transform.has_value()) return; diff --git a/Userland/Libraries/LibWeb/Painting/SVGGraphicsPaintable.cpp b/Userland/Libraries/LibWeb/Painting/SVGGraphicsPaintable.cpp index 22f6c0ef1a..ddad7ec223 100644 --- a/Userland/Libraries/LibWeb/Painting/SVGGraphicsPaintable.cpp +++ b/Userland/Libraries/LibWeb/Painting/SVGGraphicsPaintable.cpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace Web::Painting { @@ -31,4 +32,43 @@ Layout::SVGGraphicsBox const& SVGGraphicsPaintable::layout_box() const return static_cast(layout_node()); } +Optional SVGGraphicsPaintable::get_masking_area() const +{ + auto const& graphics_element = verify_cast(*dom_node()); + if (auto mask = graphics_element.mask()) + return mask->resolve_masking_area(absolute_border_box_rect()); + return {}; +} + +void SVGGraphicsPaintable::apply_mask(PaintContext& context, Gfx::Bitmap& target, CSSPixelRect const& masking_area) const +{ + auto const& graphics_element = verify_cast(*dom_node()); + auto mask = graphics_element.mask(); + VERIFY(mask); + if (mask->mask_content_units() != SVG::MaskContentUnits::UserSpaceOnUse) { + dbgln("SVG: maskContentUnits=objectBoundingBox is not supported"); + return; + } + auto mask_rect = context.enclosing_device_rect(masking_area); + RefPtr mask_bitmap = {}; + if (mask && mask->layout_node() && is(mask->layout_node()->paintable())) { + auto& mask_paintable = static_cast(*mask->layout_node()->paintable()); + auto mask_bitmap_or_error = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, mask_rect.size().to_type()); + if (mask_bitmap_or_error.is_error()) + return; + mask_bitmap = mask_bitmap_or_error.release_value(); + { + Gfx::Painter painter(*mask_bitmap); + painter.translate(-mask_rect.location().to_type()); + auto paint_context = context.clone(painter); + paint_context.set_svg_transform(graphics_element.get_transform()); + StackingContext::paint_node_as_stacking_context(mask_paintable, paint_context); + } + } + // TODO: Follow mask-type attribute to select between alpha/luminance masks. + if (mask_bitmap) + target.apply_mask(*mask_bitmap, Gfx::Bitmap::MaskKind::Luminance); + return; +} + } diff --git a/Userland/Libraries/LibWeb/Painting/SVGGraphicsPaintable.h b/Userland/Libraries/LibWeb/Painting/SVGGraphicsPaintable.h index e1d8c16d06..a64f60d205 100644 --- a/Userland/Libraries/LibWeb/Painting/SVGGraphicsPaintable.h +++ b/Userland/Libraries/LibWeb/Painting/SVGGraphicsPaintable.h @@ -21,6 +21,9 @@ public: virtual bool forms_unconnected_subtree() const override; + virtual Optional get_masking_area() const override; + virtual void apply_mask(PaintContext&, Gfx::Bitmap& target, CSSPixelRect const& masking_area) const override; + protected: SVGGraphicsPaintable(Layout::SVGGraphicsBox const&); }; diff --git a/Userland/Libraries/LibWeb/Painting/SVGPaintable.cpp b/Userland/Libraries/LibWeb/Painting/SVGPaintable.cpp index ba95e0bcfe..1030102be8 100644 --- a/Userland/Libraries/LibWeb/Painting/SVGPaintable.cpp +++ b/Userland/Libraries/LibWeb/Painting/SVGPaintable.cpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace Web::Painting { diff --git a/Userland/Libraries/LibWeb/Painting/SVGTextPaintable.cpp b/Userland/Libraries/LibWeb/Painting/SVGTextPaintable.cpp index 11990063c1..e4f746353c 100644 --- a/Userland/Libraries/LibWeb/Painting/SVGTextPaintable.cpp +++ b/Userland/Libraries/LibWeb/Painting/SVGTextPaintable.cpp @@ -58,16 +58,18 @@ void SVGTextPaintable::paint(PaintContext& context, PaintPhase phase) const auto child_text_content = dom_node.child_text_content(); - auto transform = layout_box().layout_transform(); - if (!transform.has_value()) + auto maybe_transform = layout_box().layout_transform(); + if (!maybe_transform.has_value()) return; + auto transform = Gfx::AffineTransform(context.svg_transform()).multiply(*maybe_transform); + // FIXME: Support arbitrary path transforms for fonts. // FIMXE: This assumes transform->x_scale() == transform->y_scale(). - auto& scaled_font = layout_node().scaled_font(static_cast(context.device_pixels_per_css_pixel()) * transform->x_scale()); + auto& scaled_font = layout_node().scaled_font(static_cast(context.device_pixels_per_css_pixel()) * transform.x_scale()); Utf8View text_content { child_text_content }; - auto text_offset = context.floored_device_point(dom_node.get_offset().transformed(*transform).to_type()); + auto text_offset = context.floored_device_point(dom_node.get_offset().transformed(transform).to_type()); // FIXME: Once SVGFormattingContext does text layout this logic should move there. // https://svgwg.org/svg2-draft/text.html#TextAnchoringProperties diff --git a/Userland/Libraries/LibWeb/Painting/StackingContext.cpp b/Userland/Libraries/LibWeb/Painting/StackingContext.cpp index bc8a004a2e..f902d18438 100644 --- a/Userland/Libraries/LibWeb/Painting/StackingContext.cpp +++ b/Userland/Libraries/LibWeb/Painting/StackingContext.cpp @@ -19,8 +19,10 @@ #include #include #include +#include #include #include +#include namespace Web::Painting { @@ -435,6 +437,27 @@ void StackingContext::paint(PaintContext& context) const if (opacity == 0.0f) return; + if (auto masking_area = paintable_box().get_masking_area(); masking_area.has_value()) { + // TODO: Support masks and CSS transforms at the same time. + // Note: Currently only SVG masking is implemented (which does not use CSS transforms anyway). + if (masking_area->is_empty()) + return; + auto paint_rect = context.enclosing_device_rect(*masking_area); + auto bitmap_or_error = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, paint_rect.size().to_type()); + if (bitmap_or_error.is_error()) + return; + auto bitmap = bitmap_or_error.release_value(); + { + Gfx::Painter painter(bitmap); + painter.translate(-paint_rect.location().to_type()); + auto paint_context = context.clone(painter); + paint_internal(paint_context); + } + paintable_box().apply_mask(context, bitmap, *masking_area); + context.painter().blit(paint_rect.location().to_type(), *bitmap, bitmap->rect(), opacity); + return; + } + auto affine_transform = affine_transform_matrix(); auto translation = context.rounded_device_point(affine_transform.translation().to_type()).to_type().to_type(); affine_transform.set_translation(translation); diff --git a/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp b/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp index 3145954fa5..14d111124c 100644 --- a/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp +++ b/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -46,14 +47,8 @@ Optional SVGGraphicsElement::svg_paint_computed_value_to // FIXME: This entire function is an ad-hoc hack: if (!paint_value.has_value() || !paint_value->is_url()) return {}; - auto const& url = paint_value->as_url(); - if (!url.fragment().has_value()) - return {}; - auto gradient = document().get_element_by_id(url.fragment().value()); - if (!gradient) - return {}; - if (is(*gradient)) - return static_cast(*gradient).to_gfx_paint_style(paint_context); + if (auto gradient = try_resolve_url_to(paint_value->as_url())) + return gradient->to_gfx_paint_style(paint_context); return {}; } @@ -71,6 +66,14 @@ Optional SVGGraphicsElement::stroke_paint_style(SVGPaint return svg_paint_computed_value_to_gfx_paint_style(paint_context, layout_node()->computed_values().stroke()); } +JS::GCPtr SVGGraphicsElement::mask() const +{ + auto const& mask_reference = layout_node()->computed_values().mask(); + if (!mask_reference.has_value()) + return {}; + return try_resolve_url_to(mask_reference->url()); +} + Gfx::AffineTransform transform_from_transform_list(ReadonlySpan transform_list) { Gfx::AffineTransform affine_transform; @@ -149,6 +152,9 @@ void SVGGraphicsElement::apply_presentational_hints(CSS::StyleProperties& style) } else if (name.equals_ignoring_ascii_case("font-size"sv)) { if (auto font_size_value = parse_css_value(parsing_context, value, CSS::PropertyID::FontSize)) style.set_property(CSS::PropertyID::FontSize, font_size_value.release_nonnull()); + } else if (name.equals_ignoring_ascii_case("mask"sv)) { + if (auto mask_value = parse_css_value(parsing_context, value, CSS::PropertyID::Mask)) + style.set_property(CSS::PropertyID::Mask, mask_value.release_nonnull()); } }); } diff --git a/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.h b/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.h index 996f9d35ba..806452a79d 100644 --- a/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.h +++ b/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.h @@ -46,6 +46,8 @@ public: Optional fill_paint_style(SVGPaintContext const&) const; Optional stroke_paint_style(SVGPaintContext const&) const; + JS::GCPtr mask() const; + Optional view_box() const; protected: @@ -62,6 +64,19 @@ protected: Gfx::AffineTransform m_transform = {}; + template + JS::GCPtr try_resolve_url_to(AK::URL const& url) const + { + if (!url.fragment().has_value()) + return {}; + auto node = document().get_element_by_id(*url.fragment()); + if (!node) + return {}; + if (is(*node)) + return static_cast(*node); + return {}; + } + private: virtual bool is_svg_graphics_element() const final { return true; } };