/src/serenity/Userland/Libraries/LibWeb/SVG/SVGPathElement.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2020, Matthew Olsson <mattco@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/Debug.h> |
8 | | #include <AK/Optional.h> |
9 | | #include <LibGfx/Path.h> |
10 | | #include <LibWeb/Bindings/SVGPathElementPrototype.h> |
11 | | #include <LibWeb/DOM/Document.h> |
12 | | #include <LibWeb/DOM/Event.h> |
13 | | #include <LibWeb/Layout/SVGGeometryBox.h> |
14 | | #include <LibWeb/SVG/SVGPathElement.h> |
15 | | |
16 | | namespace Web::SVG { |
17 | | |
18 | | JS_DEFINE_ALLOCATOR(SVGPathElement); |
19 | | |
20 | | [[maybe_unused]] static void print_instruction(PathInstruction const& instruction) |
21 | 0 | { |
22 | 0 | VERIFY(PATH_DEBUG); |
23 | 0 |
|
24 | 0 | auto& data = instruction.data; |
25 | 0 |
|
26 | 0 | switch (instruction.type) { |
27 | 0 | case PathInstructionType::Move: |
28 | 0 | dbgln("Move (absolute={})", instruction.absolute); |
29 | 0 | for (size_t i = 0; i < data.size(); i += 2) |
30 | 0 | dbgln(" x={}, y={}", data[i], data[i + 1]); |
31 | 0 | break; |
32 | 0 | case PathInstructionType::ClosePath: |
33 | 0 | dbgln("ClosePath (absolute={})", instruction.absolute); |
34 | 0 | break; |
35 | 0 | case PathInstructionType::Line: |
36 | 0 | dbgln("Line (absolute={})", instruction.absolute); |
37 | 0 | for (size_t i = 0; i < data.size(); i += 2) |
38 | 0 | dbgln(" x={}, y={}", data[i], data[i + 1]); |
39 | 0 | break; |
40 | 0 | case PathInstructionType::HorizontalLine: |
41 | 0 | dbgln("HorizontalLine (absolute={})", instruction.absolute); |
42 | 0 | for (size_t i = 0; i < data.size(); ++i) |
43 | 0 | dbgln(" x={}", data[i]); |
44 | 0 | break; |
45 | 0 | case PathInstructionType::VerticalLine: |
46 | 0 | dbgln("VerticalLine (absolute={})", instruction.absolute); |
47 | 0 | for (size_t i = 0; i < data.size(); ++i) |
48 | 0 | dbgln(" y={}", data[i]); |
49 | 0 | break; |
50 | 0 | case PathInstructionType::Curve: |
51 | 0 | dbgln("Curve (absolute={})", instruction.absolute); |
52 | 0 | for (size_t i = 0; i < data.size(); i += 6) |
53 | 0 | dbgln(" (x1={}, y1={}, x2={}, y2={}), (x={}, y={})", data[i], data[i + 1], data[i + 2], data[i + 3], data[i + 4], data[i + 5]); |
54 | 0 | break; |
55 | 0 | case PathInstructionType::SmoothCurve: |
56 | 0 | dbgln("SmoothCurve (absolute={})", instruction.absolute); |
57 | 0 | for (size_t i = 0; i < data.size(); i += 4) |
58 | 0 | dbgln(" (x2={}, y2={}), (x={}, y={})", data[i], data[i + 1], data[i + 2], data[i + 3]); |
59 | 0 | break; |
60 | 0 | case PathInstructionType::QuadraticBezierCurve: |
61 | 0 | dbgln("QuadraticBezierCurve (absolute={})", instruction.absolute); |
62 | 0 | for (size_t i = 0; i < data.size(); i += 4) |
63 | 0 | dbgln(" (x1={}, y1={}), (x={}, y={})", data[i], data[i + 1], data[i + 2], data[i + 3]); |
64 | 0 | break; |
65 | 0 | case PathInstructionType::SmoothQuadraticBezierCurve: |
66 | 0 | dbgln("SmoothQuadraticBezierCurve (absolute={})", instruction.absolute); |
67 | 0 | for (size_t i = 0; i < data.size(); i += 2) |
68 | 0 | dbgln(" x={}, y={}", data[i], data[i + 1]); |
69 | 0 | break; |
70 | 0 | case PathInstructionType::EllipticalArc: |
71 | 0 | dbgln("EllipticalArc (absolute={})", instruction.absolute); |
72 | 0 | for (size_t i = 0; i < data.size(); i += 7) |
73 | 0 | dbgln(" (rx={}, ry={}) x-axis-rotation={}, large-arc-flag={}, sweep-flag={}, (x={}, y={})", |
74 | 0 | data[i], |
75 | 0 | data[i + 1], |
76 | 0 | data[i + 2], |
77 | 0 | data[i + 3], |
78 | 0 | data[i + 4], |
79 | 0 | data[i + 5], |
80 | 0 | data[i + 6]); |
81 | 0 | break; |
82 | 0 | case PathInstructionType::Invalid: |
83 | 0 | dbgln("Invalid"); |
84 | 0 | break; |
85 | 0 | } |
86 | 0 | } |
87 | | |
88 | | SVGPathElement::SVGPathElement(DOM::Document& document, DOM::QualifiedName qualified_name) |
89 | 0 | : SVGGeometryElement(document, move(qualified_name)) |
90 | 0 | { |
91 | 0 | } |
92 | | |
93 | | void SVGPathElement::initialize(JS::Realm& realm) |
94 | 0 | { |
95 | 0 | Base::initialize(realm); |
96 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGPathElement); |
97 | 0 | } |
98 | | |
99 | | void SVGPathElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value) |
100 | 0 | { |
101 | 0 | SVGGeometryElement::attribute_changed(name, old_value, value); |
102 | |
|
103 | 0 | if (name == "d") |
104 | 0 | m_instructions = AttributeParser::parse_path_data(value.value_or(String {})); |
105 | 0 | } |
106 | | |
107 | | Gfx::Path path_from_path_instructions(ReadonlySpan<PathInstruction> instructions) |
108 | 0 | { |
109 | 0 | Gfx::Path path; |
110 | 0 | Optional<Gfx::FloatPoint> previous_control_point; |
111 | 0 | PathInstructionType last_instruction = PathInstructionType::Invalid; |
112 | |
|
113 | 0 | for (auto& instruction : instructions) { |
114 | | // If the first path element uses relative coordinates, we treat them as absolute by making them relative to (0, 0). |
115 | 0 | auto last_point = path.last_point(); |
116 | |
|
117 | 0 | auto& absolute = instruction.absolute; |
118 | 0 | auto& data = instruction.data; |
119 | |
|
120 | | if constexpr (PATH_DEBUG) { |
121 | | print_instruction(instruction); |
122 | | } |
123 | |
|
124 | 0 | bool clear_last_control_point = true; |
125 | |
|
126 | 0 | switch (instruction.type) { |
127 | 0 | case PathInstructionType::Move: { |
128 | 0 | Gfx::FloatPoint point = { data[0], data[1] }; |
129 | 0 | if (absolute) { |
130 | 0 | path.move_to(point); |
131 | 0 | } else { |
132 | 0 | path.move_to(point + last_point); |
133 | 0 | } |
134 | 0 | break; |
135 | 0 | } |
136 | 0 | case PathInstructionType::ClosePath: |
137 | 0 | path.close(); |
138 | 0 | break; |
139 | 0 | case PathInstructionType::Line: { |
140 | 0 | Gfx::FloatPoint point = { data[0], data[1] }; |
141 | 0 | if (absolute) { |
142 | 0 | path.line_to(point); |
143 | 0 | } else { |
144 | 0 | path.line_to(point + last_point); |
145 | 0 | } |
146 | 0 | break; |
147 | 0 | } |
148 | 0 | case PathInstructionType::HorizontalLine: { |
149 | 0 | if (absolute) |
150 | 0 | path.line_to(Gfx::FloatPoint { data[0], last_point.y() }); |
151 | 0 | else |
152 | 0 | path.line_to(Gfx::FloatPoint { data[0] + last_point.x(), last_point.y() }); |
153 | 0 | break; |
154 | 0 | } |
155 | 0 | case PathInstructionType::VerticalLine: { |
156 | 0 | if (absolute) |
157 | 0 | path.line_to(Gfx::FloatPoint { last_point.x(), data[0] }); |
158 | 0 | else |
159 | 0 | path.line_to(Gfx::FloatPoint { last_point.x(), data[0] + last_point.y() }); |
160 | 0 | break; |
161 | 0 | } |
162 | 0 | case PathInstructionType::EllipticalArc: { |
163 | 0 | double rx = data[0]; |
164 | 0 | double ry = data[1]; |
165 | 0 | double x_axis_rotation = AK::to_radians(static_cast<double>(data[2])); |
166 | 0 | double large_arc_flag = data[3]; |
167 | 0 | double sweep_flag = data[4]; |
168 | |
|
169 | 0 | Gfx::FloatPoint next_point; |
170 | |
|
171 | 0 | if (absolute) |
172 | 0 | next_point = { data[5], data[6] }; |
173 | 0 | else |
174 | 0 | next_point = { data[5] + last_point.x(), data[6] + last_point.y() }; |
175 | |
|
176 | 0 | path.elliptical_arc_to(next_point, { rx, ry }, x_axis_rotation, large_arc_flag != 0, sweep_flag != 0); |
177 | 0 | break; |
178 | 0 | } |
179 | 0 | case PathInstructionType::QuadraticBezierCurve: { |
180 | 0 | clear_last_control_point = false; |
181 | |
|
182 | 0 | Gfx::FloatPoint through = { data[0], data[1] }; |
183 | 0 | Gfx::FloatPoint point = { data[2], data[3] }; |
184 | |
|
185 | 0 | if (absolute) { |
186 | 0 | path.quadratic_bezier_curve_to(through, point); |
187 | 0 | previous_control_point = through; |
188 | 0 | } else { |
189 | 0 | auto control_point = through + last_point; |
190 | 0 | path.quadratic_bezier_curve_to(control_point, point + last_point); |
191 | 0 | previous_control_point = control_point; |
192 | 0 | } |
193 | 0 | break; |
194 | 0 | } |
195 | 0 | case PathInstructionType::SmoothQuadraticBezierCurve: { |
196 | 0 | clear_last_control_point = false; |
197 | |
|
198 | 0 | if (!previous_control_point.has_value() |
199 | 0 | || ((last_instruction != PathInstructionType::QuadraticBezierCurve) && (last_instruction != PathInstructionType::SmoothQuadraticBezierCurve))) { |
200 | 0 | previous_control_point = last_point; |
201 | 0 | } |
202 | |
|
203 | 0 | auto dx_end_control = last_point.dx_relative_to(previous_control_point.value()); |
204 | 0 | auto dy_end_control = last_point.dy_relative_to(previous_control_point.value()); |
205 | 0 | auto control_point = Gfx::FloatPoint { last_point.x() + dx_end_control, last_point.y() + dy_end_control }; |
206 | |
|
207 | 0 | Gfx::FloatPoint end_point = { data[0], data[1] }; |
208 | |
|
209 | 0 | if (absolute) { |
210 | 0 | path.quadratic_bezier_curve_to(control_point, end_point); |
211 | 0 | } else { |
212 | 0 | path.quadratic_bezier_curve_to(control_point, end_point + last_point); |
213 | 0 | } |
214 | |
|
215 | 0 | previous_control_point = control_point; |
216 | 0 | break; |
217 | 0 | } |
218 | | |
219 | 0 | case PathInstructionType::Curve: { |
220 | 0 | clear_last_control_point = false; |
221 | |
|
222 | 0 | Gfx::FloatPoint c1 = { data[0], data[1] }; |
223 | 0 | Gfx::FloatPoint c2 = { data[2], data[3] }; |
224 | 0 | Gfx::FloatPoint p2 = { data[4], data[5] }; |
225 | 0 | if (!absolute) { |
226 | 0 | p2 += last_point; |
227 | 0 | c1 += last_point; |
228 | 0 | c2 += last_point; |
229 | 0 | } |
230 | 0 | path.cubic_bezier_curve_to(c1, c2, p2); |
231 | |
|
232 | 0 | previous_control_point = c2; |
233 | 0 | break; |
234 | 0 | } |
235 | | |
236 | 0 | case PathInstructionType::SmoothCurve: { |
237 | 0 | clear_last_control_point = false; |
238 | |
|
239 | 0 | if (!previous_control_point.has_value() |
240 | 0 | || ((last_instruction != PathInstructionType::Curve) && (last_instruction != PathInstructionType::SmoothCurve))) { |
241 | 0 | previous_control_point = last_point; |
242 | 0 | } |
243 | | |
244 | | // 9.5.2. Reflected control points https://svgwg.org/svg2-draft/paths.html#ReflectedControlPoints |
245 | | // If the current point is (curx, cury) and the final control point of the previous path segment is (oldx2, oldy2), |
246 | | // then the reflected point (i.e., (newx1, newy1), the first control point of the current path segment) is: |
247 | | // (newx1, newy1) = (curx - (oldx2 - curx), cury - (oldy2 - cury)) |
248 | 0 | auto reflected_previous_control_x = last_point.x() - previous_control_point.value().dx_relative_to(last_point); |
249 | 0 | auto reflected_previous_control_y = last_point.y() - previous_control_point.value().dy_relative_to(last_point); |
250 | 0 | Gfx::FloatPoint c1 = Gfx::FloatPoint { reflected_previous_control_x, reflected_previous_control_y }; |
251 | 0 | Gfx::FloatPoint c2 = { data[0], data[1] }; |
252 | 0 | Gfx::FloatPoint p2 = { data[2], data[3] }; |
253 | 0 | if (!absolute) { |
254 | 0 | p2 += last_point; |
255 | 0 | c2 += last_point; |
256 | 0 | } |
257 | 0 | path.cubic_bezier_curve_to(c1, c2, p2); |
258 | |
|
259 | 0 | previous_control_point = c2; |
260 | 0 | break; |
261 | 0 | } |
262 | 0 | case PathInstructionType::Invalid: |
263 | 0 | VERIFY_NOT_REACHED(); |
264 | 0 | } |
265 | | |
266 | 0 | if (clear_last_control_point) { |
267 | 0 | previous_control_point = Gfx::FloatPoint {}; |
268 | 0 | } |
269 | 0 | last_instruction = instruction.type; |
270 | 0 | } |
271 | | |
272 | 0 | return path; |
273 | 0 | } |
274 | | |
275 | | Gfx::Path SVGPathElement::get_path(CSSPixelSize) |
276 | 0 | { |
277 | 0 | return path_from_path_instructions(m_instructions); |
278 | 0 | } |
279 | | |
280 | | } |