/src/serenity/Userland/Libraries/LibWeb/Painting/SVGPathPaintable.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2023, MacDue <macdue@dueutil.tech> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <LibGfx/AntiAliasingPainter.h> |
9 | | #include <LibWeb/Painting/SVGPathPaintable.h> |
10 | | #include <LibWeb/Painting/SVGSVGPaintable.h> |
11 | | |
12 | | namespace Web::Painting { |
13 | | |
14 | | JS_DEFINE_ALLOCATOR(SVGPathPaintable); |
15 | | |
16 | | JS::NonnullGCPtr<SVGPathPaintable> SVGPathPaintable::create(Layout::SVGGraphicsBox const& layout_box) |
17 | 0 | { |
18 | 0 | return layout_box.heap().allocate_without_realm<SVGPathPaintable>(layout_box); |
19 | 0 | } |
20 | | |
21 | | SVGPathPaintable::SVGPathPaintable(Layout::SVGGraphicsBox const& layout_box) |
22 | 0 | : SVGGraphicsPaintable(layout_box) |
23 | 0 | { |
24 | 0 | } |
25 | | |
26 | | Layout::SVGGraphicsBox const& SVGPathPaintable::layout_box() const |
27 | 0 | { |
28 | 0 | return static_cast<Layout::SVGGraphicsBox const&>(layout_node()); |
29 | 0 | } |
30 | | |
31 | | TraversalDecision SVGPathPaintable::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const |
32 | 0 | { |
33 | 0 | if (!computed_path().has_value()) |
34 | 0 | return TraversalDecision::Continue; |
35 | 0 | auto transformed_bounding_box = computed_transforms().svg_to_css_pixels_transform().map_to_quad(computed_path()->bounding_box()); |
36 | 0 | if (!transformed_bounding_box.contains(position.to_type<float>())) |
37 | 0 | return TraversalDecision::Continue; |
38 | 0 | return SVGGraphicsPaintable::hit_test(position, type, callback); |
39 | 0 | } |
40 | | |
41 | | static Gfx::WindingRule to_gfx_winding_rule(SVG::FillRule fill_rule) |
42 | 0 | { |
43 | 0 | switch (fill_rule) { |
44 | 0 | case SVG::FillRule::Nonzero: |
45 | 0 | return Gfx::WindingRule::Nonzero; |
46 | 0 | case SVG::FillRule::Evenodd: |
47 | 0 | return Gfx::WindingRule::EvenOdd; |
48 | 0 | default: |
49 | 0 | VERIFY_NOT_REACHED(); |
50 | 0 | } |
51 | 0 | } |
52 | | |
53 | | void SVGPathPaintable::paint(PaintContext& context, PaintPhase phase) const |
54 | 0 | { |
55 | 0 | if (!is_visible() || !computed_path().has_value()) |
56 | 0 | return; |
57 | | |
58 | 0 | SVGGraphicsPaintable::paint(context, phase); |
59 | |
|
60 | 0 | if (phase != PaintPhase::Foreground) |
61 | 0 | return; |
62 | | |
63 | 0 | auto& graphics_element = layout_box().dom_node(); |
64 | |
|
65 | 0 | auto const* svg_node = layout_box().first_ancestor_of_type<Layout::SVGSVGBox>(); |
66 | 0 | auto svg_element_rect = svg_node->paintable_box()->absolute_rect(); |
67 | | |
68 | | // FIXME: This should not be trucated to an int. |
69 | 0 | DisplayListRecorderStateSaver save_painter { context.display_list_recorder() }; |
70 | |
|
71 | 0 | auto offset = context.floored_device_point(svg_element_rect.location()).to_type<int>().to_type<float>(); |
72 | 0 | auto maybe_view_box = svg_node->dom_node().view_box(); |
73 | |
|
74 | 0 | auto paint_transform = computed_transforms().svg_to_device_pixels_transform(context); |
75 | 0 | Gfx::Path path = computed_path()->copy_transformed(paint_transform); |
76 | | |
77 | | // Fills are computed as though all subpaths are closed (https://svgwg.org/svg2-draft/painting.html#FillProperties) |
78 | 0 | auto closed_path = [&] { |
79 | | // We need to fill the path before applying the stroke, however the filled |
80 | | // path must be closed, whereas the stroke path may not necessary be closed. |
81 | | // Copy the path and close it for filling, but use the previous path for stroke |
82 | 0 | auto copy = path; |
83 | 0 | copy.close_all_subpaths(); |
84 | 0 | return copy; |
85 | 0 | }; |
86 | | |
87 | | // Note: This is assuming .x_scale() == .y_scale() (which it does currently). |
88 | 0 | auto viewbox_scale = paint_transform.x_scale(); |
89 | |
|
90 | 0 | auto svg_viewport = [&] { |
91 | 0 | if (maybe_view_box.has_value()) |
92 | 0 | return Gfx::FloatRect { maybe_view_box->min_x, maybe_view_box->min_y, maybe_view_box->width, maybe_view_box->height }; |
93 | 0 | return Gfx::FloatRect { { 0, 0 }, svg_element_rect.size().to_type<float>() }; |
94 | 0 | }(); |
95 | |
|
96 | 0 | if (context.draw_svg_geometry_for_clip_path()) { |
97 | | // https://drafts.fxtf.org/css-masking/#ClipPathElement: |
98 | | // The raw geometry of each child element exclusive of rendering properties such as fill, stroke, stroke-width |
99 | | // within a clipPath conceptually defines a 1-bit mask (with the possible exception of anti-aliasing along |
100 | | // the edge of the geometry) which represents the silhouette of the graphics associated with that element. |
101 | 0 | context.display_list_recorder().fill_path({ |
102 | 0 | .path = closed_path(), |
103 | 0 | .color = Color::Black, |
104 | 0 | .winding_rule = to_gfx_winding_rule(graphics_element.clip_rule().value_or(SVG::ClipRule::Nonzero)), |
105 | 0 | .translation = offset, |
106 | 0 | }); |
107 | 0 | return; |
108 | 0 | } |
109 | | |
110 | 0 | SVG::SVGPaintContext paint_context { |
111 | 0 | .viewport = svg_viewport, |
112 | 0 | .path_bounding_box = computed_path()->bounding_box(), |
113 | 0 | .transform = paint_transform |
114 | 0 | }; |
115 | |
|
116 | 0 | auto fill_opacity = graphics_element.fill_opacity().value_or(1); |
117 | 0 | auto winding_rule = to_gfx_winding_rule(graphics_element.fill_rule().value_or(SVG::FillRule::Nonzero)); |
118 | 0 | if (auto paint_style = graphics_element.fill_paint_style(paint_context); paint_style.has_value()) { |
119 | 0 | context.display_list_recorder().fill_path({ |
120 | 0 | .path = closed_path(), |
121 | 0 | .paint_style = *paint_style, |
122 | 0 | .winding_rule = winding_rule, |
123 | 0 | .opacity = fill_opacity, |
124 | 0 | .translation = offset, |
125 | 0 | }); |
126 | 0 | } else if (auto fill_color = graphics_element.fill_color(); fill_color.has_value()) { |
127 | 0 | context.display_list_recorder().fill_path({ |
128 | 0 | .path = closed_path(), |
129 | 0 | .color = fill_color->with_opacity(fill_opacity), |
130 | 0 | .winding_rule = winding_rule, |
131 | 0 | .translation = offset, |
132 | 0 | }); |
133 | 0 | } |
134 | |
|
135 | 0 | Gfx::Path::CapStyle cap_style; |
136 | 0 | switch (graphics_element.stroke_linecap().value_or(CSS::InitialValues::stroke_linecap())) { |
137 | 0 | case CSS::StrokeLinecap::Butt: |
138 | 0 | cap_style = Gfx::Path::CapStyle::Butt; |
139 | 0 | break; |
140 | 0 | case CSS::StrokeLinecap::Round: |
141 | 0 | cap_style = Gfx::Path::CapStyle::Round; |
142 | 0 | break; |
143 | 0 | case CSS::StrokeLinecap::Square: |
144 | 0 | cap_style = Gfx::Path::CapStyle::Square; |
145 | 0 | break; |
146 | 0 | } |
147 | | |
148 | 0 | Gfx::Path::JoinStyle join_style; |
149 | 0 | switch (graphics_element.stroke_linejoin().value_or(CSS::InitialValues::stroke_linejoin())) { |
150 | 0 | case CSS::StrokeLinejoin::Miter: |
151 | 0 | join_style = Gfx::Path::JoinStyle::Miter; |
152 | 0 | break; |
153 | 0 | case CSS::StrokeLinejoin::Round: |
154 | 0 | join_style = Gfx::Path::JoinStyle::Round; |
155 | 0 | break; |
156 | 0 | case CSS::StrokeLinejoin::Bevel: |
157 | 0 | join_style = Gfx::Path::JoinStyle::Bevel; |
158 | 0 | break; |
159 | 0 | } |
160 | | |
161 | 0 | auto miter_limit = graphics_element.stroke_miterlimit().value_or(CSS::InitialValues::stroke_miterlimit()).resolved(layout_node()); |
162 | |
|
163 | 0 | auto stroke_opacity = graphics_element.stroke_opacity().value_or(1); |
164 | | |
165 | | // Note: This is assuming .x_scale() == .y_scale() (which it does currently). |
166 | 0 | float stroke_thickness = graphics_element.stroke_width().value_or(1) * viewbox_scale; |
167 | |
|
168 | 0 | if (auto paint_style = graphics_element.stroke_paint_style(paint_context); paint_style.has_value()) { |
169 | 0 | context.display_list_recorder().stroke_path({ |
170 | 0 | .cap_style = cap_style, |
171 | 0 | .join_style = join_style, |
172 | 0 | .miter_limit = static_cast<float>(miter_limit), |
173 | 0 | .path = path, |
174 | 0 | .paint_style = *paint_style, |
175 | 0 | .thickness = stroke_thickness, |
176 | 0 | .opacity = stroke_opacity, |
177 | 0 | .translation = offset, |
178 | 0 | }); |
179 | 0 | } else if (auto stroke_color = graphics_element.stroke_color(); stroke_color.has_value()) { |
180 | 0 | context.display_list_recorder().stroke_path({ |
181 | 0 | .cap_style = cap_style, |
182 | 0 | .join_style = join_style, |
183 | 0 | .miter_limit = static_cast<float>(miter_limit), |
184 | 0 | .path = path, |
185 | 0 | .color = stroke_color->with_opacity(stroke_opacity), |
186 | 0 | .thickness = stroke_thickness, |
187 | 0 | .translation = offset, |
188 | 0 | }); |
189 | 0 | } |
190 | 0 | } |
191 | | |
192 | | } |