Skip to content

Commit

Permalink
LibWeb: Store computed SVG path data/transforms in LayoutState
Browse files Browse the repository at this point in the history
This removes the awkward hack to recompute the layout transform at paint
time, and makes it possible for path sizes to be computed during layout.

For example, it's possible to use relative units in SVG shapes (e.g.
<rect>), which can be resolved during layout, but would be hard to
resolve again during painting.
  • Loading branch information
MacDue authored and kalenikaliaksandr committed Oct 30, 2023
1 parent 1931394 commit dc9cb44
Show file tree
Hide file tree
Showing 12 changed files with 76 additions and 69 deletions.
2 changes: 1 addition & 1 deletion Tests/LibWeb/Ref/reference/simple-svg-mask-ref.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<svg width="200" viewBox="-10 -10 120 120">
<svg width="120" viewBox="-10 -10 120 120">
<rect x="-10" y="-10" width="120" height="120" fill="blue" />
<rect x="50" y="10" width="40" height="80" fill="red" />
</svg>
2 changes: 1 addition & 1 deletion Tests/LibWeb/Ref/simple-svg-mask.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<link rel="match" href="reference/simple-svg-mask-ref.html" />
<svg width="200" viewBox="-10 -10 120 120">
<svg width="120" viewBox="-10 -10 120 120">
<mask id="myMask">
<!-- Everything under a white pixel will be visible -->
<rect x="0" y="0" width="100" height="100" fill="white" />
Expand Down
2 changes: 1 addition & 1 deletion Tests/LibWeb/Ref/svg-alpha-mask.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<link rel="match" href="reference/simple-svg-mask-ref.html" />
<svg width="200" viewBox="-10 -10 120 120">
<svg width="120" viewBox="-10 -10 120 120">
<defs>
<mask id="myMask" style="mask-type:alpha">
<!-- Everything solid pixel (alpha=255) will be visible -->
Expand Down
2 changes: 1 addition & 1 deletion Tests/LibWeb/Ref/svg-mask-in-defs.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<link rel="match" href="reference/simple-svg-mask-ref.html" />
<svg width="200" viewBox="-10 -10 120 120">
<svg width="120" viewBox="-10 -10 120 120">
<defs>
<mask id="myMask">
<!-- Everything under a white pixel will be visible -->
Expand Down
2 changes: 1 addition & 1 deletion Tests/LibWeb/Ref/svg-mask-maskUnits-userSpaceOnUse.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<link rel="match" href="reference/simple-svg-mask-ref.html" />
<svg width="200" viewBox="-10 -10 120 120">
<svg width="120" viewBox="-10 -10 120 120">
<defs>
<mask id="myMask" maskUnits="userSpaceOnUse">
<!-- Everything under a white pixel will be visible -->
Expand Down
5 changes: 5 additions & 0 deletions Userland/Libraries/LibWeb/Layout/LayoutState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ void LayoutState::commit(Box& root)
paintable_with_lines.set_line_boxes(move(used_values.line_boxes));
paintables_with_lines.append(paintable_with_lines);
}

if (used_values.svg_path_data().has_value() && is<Painting::SVGGeometryPaintable>(paintable_box)) {
auto& svg_geometry_paintable = static_cast<Painting::SVGGeometryPaintable&>(paintable_box);
svg_geometry_paintable.set_path_data(move(*used_values.svg_path_data()));
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions Userland/Libraries/LibWeb/Layout/LayoutState.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <LibWeb/Layout/Box.h>
#include <LibWeb/Layout/LineBox.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/SVGGeometryPaintable.h>

namespace Web::Layout {

Expand Down Expand Up @@ -123,6 +124,9 @@ struct LayoutState {
void set_table_cell_coordinates(Painting::PaintableBox::TableCellCoordinates const& table_cell_coordinates) { m_table_cell_coordinates = table_cell_coordinates; }
auto const& table_cell_coordinates() const { return m_table_cell_coordinates; }

void set_svg_path_data(Painting::SVGGeometryPaintable::PathData const& svg_path_data) { m_svg_path_data = svg_path_data; }
auto& svg_path_data() const { return m_svg_path_data; }

private:
AvailableSize available_width_inside() const;
AvailableSize available_height_inside() const;
Expand All @@ -146,6 +150,8 @@ struct LayoutState {

Optional<Painting::PaintableBox::BordersDataWithElementKind> m_override_borders_data;
Optional<Painting::PaintableBox::TableCellCoordinates> m_table_cell_coordinates;

Optional<Painting::SVGGeometryPaintable::PathData> m_svg_path_data;
};

// Commits the used values produced by layout and builds a paintable tree.
Expand Down
12 changes: 8 additions & 4 deletions Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available
auto& dom_node = const_cast<SVGGeometryBox&>(geometry_box).dom_node();

auto& path = dom_node.get_path();
auto path_transform = dom_node.get_transform();
auto svg_transform = dom_node.get_transform();
Gfx::AffineTransform viewbox_transform;

double viewbox_scale = 1;
auto maybe_view_box = dom_node.view_box();
Expand All @@ -190,18 +191,21 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available

// The initial value for preserveAspectRatio is xMidYMid meet.
auto preserve_aspect_ratio = svg_svg_element.preserve_aspect_ratio().value_or(SVG::PreserveAspectRatio {});
auto viewbox_transform = scale_and_align_viewbox_content(preserve_aspect_ratio, view_box, { scale_width, scale_height }, svg_box_state);
path_transform = Gfx::AffineTransform {}.translate(viewbox_transform.offset.to_type<float>()).scale(viewbox_transform.scale_factor, viewbox_transform.scale_factor).translate({ -view_box.min_x, -view_box.min_y }).multiply(path_transform);
viewbox_scale = viewbox_transform.scale_factor;
auto viewbox_offset_and_scale = scale_and_align_viewbox_content(preserve_aspect_ratio, view_box, { scale_width, scale_height }, svg_box_state);
viewbox_transform = Gfx::AffineTransform {}.translate(viewbox_offset_and_scale.offset.to_type<float>()).scale(viewbox_offset_and_scale.scale_factor, viewbox_offset_and_scale.scale_factor).translate({ -view_box.min_x, -view_box.min_y });

viewbox_scale = viewbox_offset_and_scale.scale_factor;
}

// Stroke increases the path's size by stroke_width/2 per side.
auto path_transform = Gfx::AffineTransform {}.multiply(viewbox_transform).multiply(svg_transform);
auto path_bounding_box = path_transform.map(path.bounding_box()).to_type<CSSPixels>();
CSSPixels stroke_width = CSSPixels::nearest_value_for(static_cast<double>(geometry_box.dom_node().visible_stroke_width()) * viewbox_scale);
path_bounding_box.inflate(stroke_width, stroke_width);
geometry_box_state.set_content_offset(path_bounding_box.top_left());
geometry_box_state.set_content_width(path_bounding_box.width());
geometry_box_state.set_content_height(path_bounding_box.height());
geometry_box_state.set_svg_path_data(Painting::SVGGeometryPaintable::PathData(path, viewbox_transform, svg_transform));
} else if (is<SVGSVGBox>(descendant)) {
SVGFormattingContext nested_context(m_state, static_cast<SVGSVGBox const&>(descendant), this);
nested_context.run(static_cast<SVGSVGBox const&>(descendant), layout_mode, available_space);
Expand Down
37 changes: 0 additions & 37 deletions Userland/Libraries/LibWeb/Layout/SVGGeometryBox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,6 @@ SVGGeometryBox::SVGGeometryBox(DOM::Document& document, SVG::SVGGeometryElement&
{
}

CSSPixelPoint SVGGeometryBox::viewbox_origin() const
{
auto* svg_box = dom_node().shadow_including_first_ancestor_of_type<SVG::SVGSVGElement>();
if (!svg_box || !svg_box->view_box().has_value())
return { 0, 0 };
return { svg_box->view_box().value().min_x, svg_box->view_box().value().min_y };
}

Optional<Gfx::AffineTransform> SVGGeometryBox::layout_transform(Gfx::AffineTransform additional_svg_transform) const
{
auto& geometry_element = dom_node();
auto transform = geometry_element.get_transform();
auto* svg_box = geometry_element.shadow_including_first_ancestor_of_type<SVG::SVGSVGElement>();
double scaling = 1;
auto origin = viewbox_origin().to_type<float>();
Gfx::FloatPoint paint_offset = {};
if (svg_box && geometry_element.view_box().has_value()) {
// Note: SVGFormattingContext has already done the scaling based on the viewbox,
// we now have to derive what it was from the original bounding box size.
// FIXME: It would be nice if we could store the transform from layout somewhere, so we don't have to solve for it here.
auto original_bounding_box = Gfx::AffineTransform {}.translate(-origin).multiply(transform).map(const_cast<SVG::SVGGeometryElement&>(geometry_element).get_path().bounding_box());
float stroke_width = geometry_element.visible_stroke_width();
original_bounding_box.inflate(stroke_width, stroke_width);
// If the transform (or path) results in a empty box we can't display this.
if (original_bounding_box.is_empty())
return {};
auto scaled_width = paintable_box()->content_width().to_double();
auto scaled_height = paintable_box()->content_height().to_double();
scaling = min(scaled_width / static_cast<double>(original_bounding_box.width()), scaled_height / static_cast<double>(original_bounding_box.height()));
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<float>() - scaled_bounding_box.location();
}
// 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<Painting::Paintable> SVGGeometryBox::create_paintable() const
{
return Painting::SVGGeometryPaintable::create(*this);
Expand Down
4 changes: 0 additions & 4 deletions Userland/Libraries/LibWeb/Layout/SVGGeometryBox.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,9 @@ class SVGGeometryBox final : public SVGGraphicsBox {
SVG::SVGGeometryElement& dom_node() { return static_cast<SVG::SVGGeometryElement&>(SVGGraphicsBox::dom_node()); }
SVG::SVGGeometryElement const& dom_node() const { return static_cast<SVG::SVGGeometryElement const&>(SVGGraphicsBox::dom_node()); }

Optional<Gfx::AffineTransform> layout_transform(Gfx::AffineTransform additional_svg_transform) const;

virtual JS::GCPtr<Painting::Paintable> create_paintable() const override;

private:
CSSPixelPoint viewbox_origin() const;

virtual bool is_svg_geometry_box() const final { return true; }
};

Expand Down
27 changes: 8 additions & 19 deletions Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,11 @@ Layout::SVGGeometryBox const& SVGGeometryPaintable::layout_box() const
Optional<HitTestResult> SVGGeometryPaintable::hit_test(CSSPixelPoint position, HitTestType type) const
{
auto result = SVGGraphicsPaintable::hit_test(position, type);
if (!result.has_value())
if (!result.has_value() || !path_data().has_value())
return {};
auto transformed_bounding_box = path_data()->svg_to_css_pixels_transform().map_to_quad(path_data()->computed_path().bounding_box());
if (!transformed_bounding_box.contains(position.to_type<float>()))
return {};
auto& geometry_element = layout_box().dom_node();
if (auto transform = layout_box().layout_transform({}); transform.has_value()) {
auto transformed_bounding_box = transform->map_to_quad(
const_cast<SVG::SVGGeometryElement&>(geometry_element).get_path().bounding_box());
if (!transformed_bounding_box.contains(position.to_type<float>()))
return {};
}
return result;
}

Expand All @@ -56,7 +52,7 @@ static Gfx::Painter::WindingRule to_gfx_winding_rule(SVG::FillRule fill_rule)

void SVGGeometryPaintable::paint(PaintContext& context, PaintPhase phase) const
{
if (!is_visible())
if (!is_visible() || !path_data().has_value())
return;

SVGGraphicsPaintable::paint(context, phase);
Expand All @@ -73,17 +69,10 @@ void SVGGeometryPaintable::paint(PaintContext& context, PaintPhase phase) const
RecordingPainterStateSaver save_painter { context.painter() };

auto offset = context.floored_device_point(svg_element_rect.location()).to_type<int>().to_type<float>();

auto maybe_view_box = geometry_element.view_box();

auto transform = layout_box().layout_transform(context.svg_transform());
if (!transform.has_value())
return;

auto css_scale = context.device_pixels_per_css_pixel();
auto paint_transform = Gfx::AffineTransform {}.scale(css_scale, css_scale).multiply(*transform);
auto const& original_path = const_cast<SVG::SVGGeometryElement&>(geometry_element).get_path();
Gfx::Path path = original_path.copy_transformed(paint_transform);
auto paint_transform = path_data()->svg_to_device_pixels_transform(context, context.svg_transform());
Gfx::Path path = path_data()->computed_path().copy_transformed(paint_transform);

// Fills are computed as though all subpaths are closed (https://svgwg.org/svg2-draft/painting.html#FillProperties)
auto closed_path = [&] {
Expand All @@ -106,7 +95,7 @@ void SVGGeometryPaintable::paint(PaintContext& context, PaintPhase phase) const

SVG::SVGPaintContext paint_context {
.viewport = svg_viewport,
.path_bounding_box = original_path.bounding_box(),
.path_bounding_box = path_data()->computed_path().bounding_box(),
.transform = paint_transform
};

Expand Down
44 changes: 44 additions & 0 deletions Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,41 @@ class SVGGeometryPaintable final : public SVGGraphicsPaintable {
JS_CELL(SVGGeometryPaintable, SVGGraphicsPaintable);

public:
class PathData {
public:
PathData(Gfx::Path path, Gfx::AffineTransform svg_to_viewbox_transform, Gfx::AffineTransform svg_transform)
: m_computed_path(move(path))
, m_svg_to_viewbox_transform(svg_to_viewbox_transform)
, m_svg_transform(svg_transform)
{
}

Gfx::Path const& computed_path() const { return m_computed_path; }

Gfx::AffineTransform const& svg_to_viewbox_transform() const { return m_svg_to_viewbox_transform; }

Gfx::AffineTransform const& svg_transform() const { return m_svg_transform; }

Gfx::AffineTransform svg_to_css_pixels_transform(
Optional<Gfx::AffineTransform const&> additional_svg_transform = {}) const
{
return Gfx::AffineTransform {}.multiply(svg_to_viewbox_transform()).multiply(additional_svg_transform.value_or(Gfx::AffineTransform {})).multiply(svg_transform());
}

Gfx::AffineTransform svg_to_device_pixels_transform(
PaintContext const& context,
Gfx::AffineTransform const& additional_svg_transform) const
{
auto css_scale = context.device_pixels_per_css_pixel();
return Gfx::AffineTransform {}.scale({ css_scale, css_scale }).multiply(svg_to_css_pixels_transform(additional_svg_transform));
}

private:
Gfx::Path m_computed_path;
Gfx::AffineTransform m_svg_to_viewbox_transform;
Gfx::AffineTransform m_svg_transform;
};

static JS::NonnullGCPtr<SVGGeometryPaintable> create(Layout::SVGGeometryBox const&);

virtual Optional<HitTestResult> hit_test(CSSPixelPoint, HitTestType) const override;
Expand All @@ -23,8 +58,17 @@ class SVGGeometryPaintable final : public SVGGraphicsPaintable {

Layout::SVGGeometryBox const& layout_box() const;

void set_path_data(PathData path_data)
{
m_path_data = move(path_data);
}

Optional<PathData> const& path_data() const { return m_path_data; }

protected:
SVGGeometryPaintable(Layout::SVGGeometryBox const&);

Optional<PathData> m_path_data = {};
};

}

0 comments on commit dc9cb44

Please sign in to comment.