/src/serenity/Userland/Libraries/LibGfx/Path.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/Enumerate.h> |
8 | | #include <AK/Math.h> |
9 | | #include <AK/StringBuilder.h> |
10 | | #include <AK/TypeCasts.h> |
11 | | #include <LibGfx/BoundingBox.h> |
12 | | #include <LibGfx/Font/ScaledFont.h> |
13 | | #include <LibGfx/Painter.h> |
14 | | #include <LibGfx/Path.h> |
15 | | #include <LibGfx/TextLayout.h> |
16 | | #include <LibGfx/Vector2.h> |
17 | | |
18 | | namespace Gfx { |
19 | | |
20 | | void Path::approximate_elliptical_arc_with_cubic_beziers(FloatPoint center, FloatSize radii, float x_axis_rotation, float theta, float theta_delta) |
21 | 1.77M | { |
22 | 1.77M | float sin_x_rotation; |
23 | 1.77M | float cos_x_rotation; |
24 | 1.77M | AK::sincos(x_axis_rotation, sin_x_rotation, cos_x_rotation); |
25 | 28.2M | auto arc_point_and_derivative = [&](float t, FloatPoint& point, FloatPoint& derivative) { |
26 | 28.2M | float sin_angle; |
27 | 28.2M | float cos_angle; |
28 | 28.2M | AK::sincos(t, sin_angle, cos_angle); |
29 | 28.2M | point = FloatPoint { |
30 | 28.2M | center.x() |
31 | 28.2M | + radii.width() * cos_x_rotation * cos_angle |
32 | 28.2M | - radii.height() * sin_x_rotation * sin_angle, |
33 | 28.2M | center.y() |
34 | 28.2M | + radii.width() * sin_x_rotation * cos_angle |
35 | 28.2M | + radii.height() * cos_x_rotation * sin_angle, |
36 | 28.2M | }; |
37 | 28.2M | derivative = FloatPoint { |
38 | 28.2M | -radii.width() * cos_x_rotation * sin_angle |
39 | 28.2M | - radii.height() * sin_x_rotation * cos_angle, |
40 | 28.2M | -radii.width() * sin_x_rotation * sin_angle |
41 | 28.2M | + radii.height() * cos_x_rotation * cos_angle, |
42 | 28.2M | }; |
43 | 28.2M | }; |
44 | 14.1M | auto approximate_arc_between = [&](float start_angle, float end_angle) { |
45 | 14.1M | auto t = AK::tan((end_angle - start_angle) / 2); |
46 | 14.1M | auto alpha = AK::sin(end_angle - start_angle) * ((AK::sqrt(4 + 3 * t * t) - 1) / 3); |
47 | 14.1M | FloatPoint p1, d1; |
48 | 14.1M | FloatPoint p2, d2; |
49 | 14.1M | arc_point_and_derivative(start_angle, p1, d1); |
50 | 14.1M | arc_point_and_derivative(end_angle, p2, d2); |
51 | 14.1M | auto q1 = p1 + d1.scaled(alpha, alpha); |
52 | 14.1M | auto q2 = p2 - d2.scaled(alpha, alpha); |
53 | 14.1M | cubic_bezier_curve_to(q1, q2, p2); |
54 | 14.1M | }; |
55 | | // FIXME: Come up with a more mathematically sound step size (using some error calculation). |
56 | 1.77M | auto step = theta_delta; |
57 | 1.77M | int step_count = 1; |
58 | 7.07M | while (fabs(step) > AK::Pi<float> / 4) { |
59 | 5.30M | step /= 2; |
60 | 5.30M | step_count *= 2; |
61 | 5.30M | } |
62 | 1.77M | float prev = theta; |
63 | 1.77M | float t = prev + step; |
64 | 15.9M | for (int i = 0; i < step_count; i++, prev = t, t += step) |
65 | 14.1M | approximate_arc_between(prev, t); |
66 | 1.77M | } |
67 | | |
68 | | void Path::elliptical_arc_to(FloatPoint point, FloatSize radii, float x_axis_rotation, bool large_arc, bool sweep) |
69 | 1.78M | { |
70 | 1.78M | auto next_point = point; |
71 | | |
72 | 1.78M | double rx = radii.width(); |
73 | 1.78M | double ry = radii.height(); |
74 | | |
75 | 1.78M | double x_axis_rotation_s; |
76 | 1.78M | double x_axis_rotation_c; |
77 | 1.78M | AK::sincos(static_cast<double>(x_axis_rotation), x_axis_rotation_s, x_axis_rotation_c); |
78 | 1.78M | FloatPoint last_point = this->last_point(); |
79 | | |
80 | | // Step 1 of out-of-range radii correction |
81 | 1.78M | if (rx == 0.0 || ry == 0.0) { |
82 | 2.20k | append_segment<PathSegment::LineTo>(next_point); |
83 | 2.20k | return; |
84 | 2.20k | } |
85 | | |
86 | | // Step 2 of out-of-range radii correction |
87 | 1.77M | if (rx < 0) |
88 | 5.46k | rx *= -1.0; |
89 | 1.77M | if (ry < 0) |
90 | 5.58k | ry *= -1.0; |
91 | | |
92 | | // POSSIBLY HACK: Handle the case where both points are the same. |
93 | 1.77M | auto same_endpoints = next_point == last_point; |
94 | 1.77M | if (same_endpoints) { |
95 | 107k | if (!large_arc) { |
96 | | // Nothing is going to be drawn anyway. |
97 | 2.88k | return; |
98 | 2.88k | } |
99 | | |
100 | | // Move the endpoint by a small amount to avoid division by zero. |
101 | 104k | next_point.translate_by(0.01f, 0.01f); |
102 | 104k | } |
103 | | |
104 | | // Find (cx, cy), theta_1, theta_delta |
105 | | // Step 1: Compute (x1', y1') |
106 | 1.77M | auto x_avg = static_cast<double>(last_point.x() - next_point.x()) / 2.0; |
107 | 1.77M | auto y_avg = static_cast<double>(last_point.y() - next_point.y()) / 2.0; |
108 | 1.77M | auto x1p = x_axis_rotation_c * x_avg + x_axis_rotation_s * y_avg; |
109 | 1.77M | auto y1p = -x_axis_rotation_s * x_avg + x_axis_rotation_c * y_avg; |
110 | | |
111 | | // Step 2: Compute (cx', cy') |
112 | 1.77M | double x1p_sq = x1p * x1p; |
113 | 1.77M | double y1p_sq = y1p * y1p; |
114 | 1.77M | double rx_sq = rx * rx; |
115 | 1.77M | double ry_sq = ry * ry; |
116 | | |
117 | | // Step 3 of out-of-range radii correction |
118 | 1.77M | double lambda = x1p_sq / rx_sq + y1p_sq / ry_sq; |
119 | 1.77M | double multiplier; |
120 | | |
121 | 1.77M | if (lambda > 1.0) { |
122 | 11.2k | auto lambda_sqrt = AK::sqrt(lambda); |
123 | 11.2k | rx *= lambda_sqrt; |
124 | 11.2k | ry *= lambda_sqrt; |
125 | 11.2k | multiplier = 0.0; |
126 | 1.76M | } else { |
127 | 1.76M | double numerator = rx_sq * ry_sq - rx_sq * y1p_sq - ry_sq * x1p_sq; |
128 | 1.76M | double denominator = rx_sq * y1p_sq + ry_sq * x1p_sq; |
129 | 1.76M | multiplier = AK::sqrt(AK::max(0., numerator) / denominator); |
130 | 1.76M | } |
131 | | |
132 | 1.77M | if (large_arc == sweep) |
133 | 1.77M | multiplier *= -1.0; |
134 | | |
135 | 1.77M | double cxp = multiplier * rx * y1p / ry; |
136 | 1.77M | double cyp = multiplier * -ry * x1p / rx; |
137 | | |
138 | | // Step 3: Compute (cx, cy) from (cx', cy') |
139 | 1.77M | x_avg = (last_point.x() + next_point.x()) / 2.0f; |
140 | 1.77M | y_avg = (last_point.y() + next_point.y()) / 2.0f; |
141 | 1.77M | double cx = x_axis_rotation_c * cxp - x_axis_rotation_s * cyp + x_avg; |
142 | 1.77M | double cy = x_axis_rotation_s * cxp + x_axis_rotation_c * cyp + y_avg; |
143 | | |
144 | 1.77M | double theta_1 = AK::atan2((y1p - cyp) / ry, (x1p - cxp) / rx); |
145 | 1.77M | double theta_2 = AK::atan2((-y1p - cyp) / ry, (-x1p - cxp) / rx); |
146 | | |
147 | 1.77M | auto theta_delta = theta_2 - theta_1; |
148 | | |
149 | 1.77M | if (!sweep && theta_delta > 0.0) { |
150 | 1.51k | theta_delta -= 2 * AK::Pi<double>; |
151 | 1.77M | } else if (sweep && theta_delta < 0) { |
152 | 1.73M | theta_delta += 2 * AK::Pi<double>; |
153 | 1.73M | } |
154 | | |
155 | 1.77M | approximate_elliptical_arc_with_cubic_beziers( |
156 | 1.77M | { cx, cy }, |
157 | 1.77M | { rx, ry }, |
158 | 1.77M | x_axis_rotation, |
159 | 1.77M | theta_1, |
160 | 1.77M | theta_delta); |
161 | 1.77M | } |
162 | | |
163 | | void Path::quad(FloatQuad const& quad) |
164 | 0 | { |
165 | 0 | move_to(quad.p1()); |
166 | 0 | line_to(quad.p2()); |
167 | 0 | line_to(quad.p3()); |
168 | 0 | line_to(quad.p4()); |
169 | 0 | close(); |
170 | 0 | } |
171 | | |
172 | | void Path::rounded_rect(FloatRect const& rect, CornerRadius top_left, CornerRadius top_right, CornerRadius bottom_right, CornerRadius bottom_left) |
173 | 0 | { |
174 | 0 | auto x = rect.x(); |
175 | 0 | auto y = rect.y(); |
176 | 0 | auto width = rect.width(); |
177 | 0 | auto height = rect.height(); |
178 | |
|
179 | 0 | if (top_left) |
180 | 0 | move_to({ x + top_left.horizontal_radius, y }); |
181 | 0 | else |
182 | 0 | move_to({ x, y }); |
183 | |
|
184 | 0 | if (top_right) { |
185 | 0 | horizontal_line_to(x + width - top_right.horizontal_radius); |
186 | 0 | elliptical_arc_to({ x + width, y + top_right.horizontal_radius }, { top_right.horizontal_radius, top_right.vertical_radius }, 0, false, true); |
187 | 0 | } else { |
188 | 0 | horizontal_line_to(x + width); |
189 | 0 | } |
190 | |
|
191 | 0 | if (bottom_right) { |
192 | 0 | vertical_line_to(y + height - bottom_right.vertical_radius); |
193 | 0 | elliptical_arc_to({ x + width - bottom_right.horizontal_radius, y + height }, { bottom_right.horizontal_radius, bottom_right.vertical_radius }, 0, false, true); |
194 | 0 | } else { |
195 | 0 | vertical_line_to(y + height); |
196 | 0 | } |
197 | |
|
198 | 0 | if (bottom_left) { |
199 | 0 | horizontal_line_to(x + bottom_left.horizontal_radius); |
200 | 0 | elliptical_arc_to({ x, y + height - bottom_left.vertical_radius }, { bottom_left.horizontal_radius, bottom_left.vertical_radius }, 0, false, true); |
201 | 0 | } else { |
202 | 0 | horizontal_line_to(x); |
203 | 0 | } |
204 | |
|
205 | 0 | if (top_left) { |
206 | 0 | vertical_line_to(y + top_left.vertical_radius); |
207 | 0 | elliptical_arc_to({ x + top_left.horizontal_radius, y }, { top_left.horizontal_radius, top_left.vertical_radius }, 0, false, true); |
208 | 0 | } else { |
209 | 0 | vertical_line_to(y); |
210 | 0 | } |
211 | 0 | } |
212 | | |
213 | | void Path::text(Utf8View text, Font const& font) |
214 | 0 | { |
215 | 0 | if (!is<ScaledFont>(font)) { |
216 | | // FIXME: This API only accepts Gfx::Font for ease of use. |
217 | 0 | dbgln("Cannot path-ify bitmap fonts!"); |
218 | 0 | return; |
219 | 0 | } |
220 | | |
221 | 0 | auto& scaled_font = static_cast<ScaledFont const&>(font); |
222 | 0 | for_each_glyph_position( |
223 | 0 | last_point(), text, scaled_font, [&](DrawGlyphOrEmoji glyph_or_emoji) { |
224 | 0 | if (glyph_or_emoji.has<DrawGlyph>()) { |
225 | 0 | auto& glyph = glyph_or_emoji.get<DrawGlyph>(); |
226 | 0 | move_to(glyph.position); |
227 | 0 | auto glyph_id = scaled_font.glyph_id_for_code_point(glyph.code_point); |
228 | 0 | scaled_font.append_glyph_path_to(*this, glyph_id); |
229 | 0 | } |
230 | 0 | }, |
231 | 0 | IncludeLeftBearing::Yes); |
232 | 0 | } |
233 | | |
234 | | Path Path::place_text_along(Utf8View text, Font const& font) const |
235 | 0 | { |
236 | 0 | if (!is<ScaledFont>(font)) { |
237 | | // FIXME: This API only accepts Gfx::Font for ease of use. |
238 | 0 | dbgln("Cannot path-ify bitmap fonts!"); |
239 | 0 | return {}; |
240 | 0 | } |
241 | | |
242 | 0 | auto lines = split_lines(); |
243 | 0 | auto next_point_for_offset = [&, line_index = 0U, distance_along_path = 0.0f, last_line_length = 0.0f](float offset) mutable -> Optional<FloatPoint> { |
244 | 0 | while (line_index < lines.size() && offset > distance_along_path) { |
245 | 0 | last_line_length = lines[line_index++].length(); |
246 | 0 | distance_along_path += last_line_length; |
247 | 0 | } |
248 | 0 | if (offset > distance_along_path) |
249 | 0 | return {}; |
250 | 0 | if (last_line_length > 1) { |
251 | | // If the last line segment was fairly long, compute the point in the line. |
252 | 0 | float p = (last_line_length + offset - distance_along_path) / last_line_length; |
253 | 0 | auto current_line = lines[line_index - 1]; |
254 | 0 | return current_line.a() + (current_line.b() - current_line.a()).scaled(p); |
255 | 0 | } |
256 | 0 | if (line_index >= lines.size()) |
257 | 0 | return {}; |
258 | 0 | return lines[line_index].a(); |
259 | 0 | }; |
260 | |
|
261 | 0 | auto& scaled_font = static_cast<Gfx::ScaledFont const&>(font); |
262 | 0 | Gfx::Path result_path; |
263 | 0 | Gfx::for_each_glyph_position( |
264 | 0 | {}, text, font, [&](Gfx::DrawGlyphOrEmoji glyph_or_emoji) { |
265 | 0 | auto* glyph = glyph_or_emoji.get_pointer<Gfx::DrawGlyph>(); |
266 | 0 | if (!glyph) |
267 | 0 | return; |
268 | 0 | auto offset = glyph->position.x(); |
269 | 0 | auto width = font.glyph_width(glyph->code_point); |
270 | 0 | auto start = next_point_for_offset(offset); |
271 | 0 | if (!start.has_value()) |
272 | 0 | return; |
273 | 0 | auto end = next_point_for_offset(offset + width); |
274 | 0 | if (!end.has_value()) |
275 | 0 | return; |
276 | | // Find the angle between the start and end points on the path. |
277 | 0 | auto delta = *end - *start; |
278 | 0 | auto angle = AK::atan2(delta.y(), delta.x()); |
279 | 0 | Gfx::Path glyph_path; |
280 | | // Rotate the glyph then move it to start point. |
281 | 0 | auto glyph_id = scaled_font.glyph_id_for_code_point(glyph->code_point); |
282 | 0 | scaled_font.append_glyph_path_to(glyph_path, glyph_id); |
283 | 0 | auto transform = Gfx::AffineTransform {} |
284 | 0 | .translate(*start) |
285 | 0 | .multiply(Gfx::AffineTransform {}.rotate_radians(angle)) |
286 | 0 | .multiply(Gfx::AffineTransform {}.translate({ 0, -scaled_font.pixel_metrics().ascent })); |
287 | 0 | glyph_path = glyph_path.copy_transformed(transform); |
288 | 0 | result_path.append_path(glyph_path); |
289 | 0 | }, |
290 | 0 | Gfx::IncludeLeftBearing::Yes); |
291 | 0 | return result_path; |
292 | 0 | } |
293 | | |
294 | | void Path::close() |
295 | 3.61M | { |
296 | | // If there's no `moveto` starting this subpath assume the start is (0, 0). |
297 | 3.61M | FloatPoint first_point_in_subpath = { 0, 0 }; |
298 | 1.14G | for (auto it = end(); it-- != begin();) { |
299 | 1.14G | auto segment = *it; |
300 | 1.14G | if (segment.command() == PathSegment::MoveTo) { |
301 | 3.61M | first_point_in_subpath = segment.point(); |
302 | 3.61M | break; |
303 | 3.61M | } |
304 | 1.14G | } |
305 | 3.61M | if (first_point_in_subpath != last_point()) |
306 | 3.32M | line_to(first_point_in_subpath); |
307 | 3.61M | append_segment<PathSegment::ClosePath>(); |
308 | 3.61M | } |
309 | | |
310 | | void Path::close_all_subpaths() |
311 | 625k | { |
312 | | // This is only called before filling, not before stroking, so this doesn't have to insert ClosePath segments. |
313 | 625k | auto it = begin(); |
314 | | // Note: Get the end outside the loop as closing subpaths will move the end. |
315 | 625k | auto end = this->end(); |
316 | 1.30M | while (it < end) { |
317 | | // If there's no `moveto` starting this subpath assume the start is (0, 0). |
318 | 683k | FloatPoint first_point_in_subpath = { 0, 0 }; |
319 | 683k | auto segment = *it; |
320 | 683k | if (segment.command() == PathSegment::MoveTo) { |
321 | 683k | first_point_in_subpath = segment.point(); |
322 | 683k | ++it; |
323 | 683k | } |
324 | | // Find the end of the current subpath. |
325 | 683k | FloatPoint cursor = first_point_in_subpath; |
326 | 5.74M | for (; it < end; ++it) { |
327 | 5.12M | auto segment = *it; |
328 | 5.12M | if (segment.command() == PathSegment::ClosePath) |
329 | 622k | continue; |
330 | 4.50M | if (segment.command() == PathSegment::MoveTo) |
331 | 58.1k | break; |
332 | 4.44M | cursor = segment.point(); |
333 | 4.44M | } |
334 | | // Close the subpath. |
335 | 683k | if (first_point_in_subpath != cursor) { |
336 | 36.3k | move_to(cursor); |
337 | 36.3k | line_to(first_point_in_subpath); |
338 | 36.3k | } |
339 | 683k | } |
340 | 625k | } |
341 | | |
342 | | ByteString Path::to_byte_string() const |
343 | 0 | { |
344 | | // Dumps this path as an SVG compatible string. |
345 | 0 | StringBuilder builder; |
346 | 0 | if (is_empty() || m_commands.first() != PathSegment::MoveTo) |
347 | 0 | builder.append("M 0,0"sv); |
348 | 0 | for (auto segment : *this) { |
349 | 0 | if (!builder.is_empty()) |
350 | 0 | builder.append(' '); |
351 | 0 | switch (segment.command()) { |
352 | 0 | case PathSegment::MoveTo: |
353 | 0 | builder.append('M'); |
354 | 0 | break; |
355 | 0 | case PathSegment::LineTo: |
356 | 0 | builder.append('L'); |
357 | 0 | break; |
358 | 0 | case PathSegment::QuadraticBezierCurveTo: |
359 | 0 | builder.append('Q'); |
360 | 0 | break; |
361 | 0 | case PathSegment::CubicBezierCurveTo: |
362 | 0 | builder.append('C'); |
363 | 0 | break; |
364 | 0 | case PathSegment::ClosePath: |
365 | 0 | builder.append('Z'); |
366 | 0 | break; |
367 | 0 | } |
368 | 0 | for (auto point : segment.points()) |
369 | 0 | builder.appendff(" {},{}", point.x(), point.y()); |
370 | 0 | } |
371 | 0 | return builder.to_byte_string(); |
372 | 0 | } |
373 | | |
374 | | Optional<FloatRect> Path::as_rect() const |
375 | 0 | { |
376 | 0 | if (m_commands.size() != 6 || m_points.size() != 5) |
377 | 0 | return {}; |
378 | 0 | if (m_commands[0] != PathSegment::MoveTo |
379 | 0 | || m_commands[1] != PathSegment::LineTo |
380 | 0 | || m_commands[2] != PathSegment::LineTo |
381 | 0 | || m_commands[3] != PathSegment::LineTo |
382 | 0 | || m_commands[4] != PathSegment::LineTo |
383 | 0 | || m_commands[5] != PathSegment::ClosePath) |
384 | 0 | return {}; |
385 | 0 | VERIFY(m_points[0] == m_points[4]); |
386 | 0 | if (m_points[0].y() != m_points[1].y() |
387 | 0 | || m_points[1].x() != m_points[2].x() |
388 | 0 | || m_points[2].y() != m_points[3].y() |
389 | 0 | || m_points[3].x() != m_points[0].x()) |
390 | 0 | return {}; |
391 | 0 | return FloatRect::from_two_points(m_points[0], m_points[2]); |
392 | 0 | } |
393 | | |
394 | | void Path::segmentize_path() |
395 | 725k | { |
396 | 725k | Vector<FloatLine> segments; |
397 | 725k | FloatBoundingBox bounding_box; |
398 | 725k | Vector<size_t> subpath_end_indices; |
399 | | |
400 | 141M | auto add_line = [&](auto const& p0, auto const& p1) { |
401 | 141M | segments.append({ p0, p1 }); |
402 | 141M | bounding_box.add_point(p1); |
403 | 141M | }; |
404 | | |
405 | 725k | FloatPoint cursor { 0, 0 }; |
406 | 54.5M | for (auto segment : *this) { |
407 | 54.5M | switch (segment.command()) { |
408 | 891k | case PathSegment::MoveTo: |
409 | 891k | bounding_box.add_point(segment.point()); |
410 | 891k | break; |
411 | 50.7M | case PathSegment::LineTo: { |
412 | 50.7M | add_line(cursor, segment.point()); |
413 | 50.7M | break; |
414 | 0 | } |
415 | 8.13k | case PathSegment::QuadraticBezierCurveTo: { |
416 | 34.2M | Painter::for_each_line_segment_on_bezier_curve(segment.through(), cursor, segment.point(), [&](FloatPoint p0, FloatPoint p1) { |
417 | 34.2M | add_line(p0, p1); |
418 | 34.2M | }); |
419 | 8.13k | break; |
420 | 0 | } |
421 | 2.13M | case PathSegment::CubicBezierCurveTo: { |
422 | 56.5M | Painter::for_each_line_segment_on_cubic_bezier_curve(segment.through_0(), segment.through_1(), cursor, segment.point(), [&](FloatPoint p0, FloatPoint p1) { |
423 | 56.5M | add_line(p0, p1); |
424 | 56.5M | }); |
425 | 2.13M | break; |
426 | 0 | } |
427 | 774k | case PathSegment::ClosePath: { |
428 | 774k | if (subpath_end_indices.is_empty() || subpath_end_indices.last() != segments.size() - 1) |
429 | 731k | subpath_end_indices.append(segments.size() - 1); |
430 | 774k | break; |
431 | 0 | } |
432 | 54.5M | } |
433 | 54.5M | if (segment.command() != PathSegment::ClosePath) |
434 | 53.7M | cursor = segment.point(); |
435 | 54.5M | } |
436 | | |
437 | 725k | m_split_lines = SplitLines { move(segments), bounding_box, move(subpath_end_indices) }; |
438 | 725k | } |
439 | | |
440 | | Path Path::copy_transformed(Gfx::AffineTransform const& transform) const |
441 | 664k | { |
442 | 664k | Path result; |
443 | 664k | result.m_commands = m_commands; |
444 | 664k | result.m_points.ensure_capacity(m_points.size()); |
445 | 664k | for (auto point : m_points) |
446 | 9.68M | result.m_points.unchecked_append(transform.map(point)); |
447 | 664k | return result; |
448 | 664k | } |
449 | | |
450 | | void Path::transform(AffineTransform const& transform) |
451 | 0 | { |
452 | 0 | for (auto& point : m_points) |
453 | 0 | point = transform.map(point); |
454 | 0 | invalidate_split_lines(); |
455 | 0 | } |
456 | | |
457 | | void Path::append_path(Path const& path, AppendRelativeToLastPoint relative_to_last_point) |
458 | 0 | { |
459 | 0 | auto previous_last_point = last_point(); |
460 | 0 | auto new_points_start = m_points.size(); |
461 | 0 | m_commands.extend(path.m_commands); |
462 | 0 | m_points.extend(path.m_points); |
463 | 0 | if (relative_to_last_point == AppendRelativeToLastPoint::Yes) { |
464 | 0 | for (size_t i = new_points_start; i < m_points.size(); i++) |
465 | 0 | m_points[i] += previous_last_point; |
466 | 0 | } |
467 | 0 | invalidate_split_lines(); |
468 | 0 | } |
469 | | |
470 | | template<typename T> |
471 | | struct RoundTrip { |
472 | | RoundTrip(ReadonlySpan<T> span) |
473 | 32.2k | : m_span(span) |
474 | 32.2k | { |
475 | 32.2k | } |
476 | | |
477 | | size_t size() const |
478 | 132M | { |
479 | 132M | return m_span.size() * 2 - 1; |
480 | 132M | } |
481 | | |
482 | | T const& operator[](size_t index) const |
483 | 132M | { |
484 | | // Follow the path: |
485 | 132M | if (index < m_span.size()) |
486 | 66.2M | return m_span[index]; |
487 | | // Then in reverse: |
488 | 66.1M | if (index < size()) |
489 | 66.1M | return m_span[size() - index - 1]; |
490 | | // Then wrap around again: |
491 | 18.2k | return m_span[index - size() + 1]; |
492 | 66.1M | } |
493 | | |
494 | | private: |
495 | | ReadonlySpan<T> m_span; |
496 | | }; |
497 | | |
498 | | static Vector<FloatPoint, 128> make_pen(float thickness) |
499 | 49.1k | { |
500 | 49.1k | constexpr auto flatness = 0.15f; |
501 | 49.1k | auto pen_vertex_count = 4; |
502 | 49.1k | if (thickness > flatness) { |
503 | 45.3k | pen_vertex_count = max( |
504 | 45.3k | static_cast<int>(ceilf(AK::Pi<float> |
505 | 45.3k | / acosf(1 - (2 * flatness) / thickness))), |
506 | 45.3k | pen_vertex_count); |
507 | 45.3k | } |
508 | | |
509 | 49.1k | if (pen_vertex_count % 2 == 1) |
510 | 15.3k | pen_vertex_count += 1; |
511 | | |
512 | 49.1k | Vector<FloatPoint, 128> pen_vertices; |
513 | 49.1k | pen_vertices.ensure_capacity(pen_vertex_count); |
514 | | |
515 | | // Generate vertices for the pen (going counterclockwise). The pen does not necessarily need |
516 | | // to be a circle (or an approximation of one), but other shapes are untested. |
517 | 49.1k | float theta = 0; |
518 | 49.1k | float theta_delta = (AK::Pi<float> * 2) / pen_vertex_count; |
519 | 877k | for (int i = 0; i < pen_vertex_count; i++) { |
520 | 828k | float sin_theta; |
521 | 828k | float cos_theta; |
522 | 828k | AK::sincos(theta, sin_theta, cos_theta); |
523 | 828k | pen_vertices.unchecked_append({ cos_theta * thickness / 2, sin_theta * thickness / 2 }); |
524 | 828k | theta -= theta_delta; |
525 | 828k | } |
526 | | |
527 | 49.1k | return pen_vertices; |
528 | 49.1k | } |
529 | | |
530 | | static void apply_dash_pattern(Vector<Vector<FloatPoint>>& segments, Vector<bool>& segment_is_closed, Vector<float> dash_pattern, float dash_offset) |
531 | 0 | { |
532 | 0 | VERIFY(!dash_pattern.is_empty()); |
533 | | |
534 | | // Has to be ensured by callers. (They all double the list, but <canvas> needs to do that in a way that |
535 | | // is visible to JS accessors, so don't do it here.) |
536 | 0 | VERIFY(dash_pattern.size() % 2 == 0); |
537 | | |
538 | | // This implementation is vaguely based on the <canvas> spec. One difference is that the <canvas> spec |
539 | | // modifies the path in place, while this implementation returns a new path. The spec is written in terms |
540 | | // of [start, end] intervals that are removed from the input path, while we have to instead add the |
541 | | // complement of those intervals to the output path. This is done by keeping track of the previous `end` |
542 | | // value and then filling in the gap between that and the current `start` value on every interval, and |
543 | | // at the end of each subpath. |
544 | | |
545 | 0 | Vector<Vector<FloatPoint>> new_segments; |
546 | | |
547 | | // https://html.spec.whatwg.org/multipage/canvas.html#line-styles:dash-list-5 |
548 | | // 7. Let `pattern width` be the concatenation of all the entries of style's dash list, in coordinate space units. |
549 | | // (NOTE: The spec means sum, not concatenation.) |
550 | 0 | float pattern_width = 0; |
551 | 0 | for (auto& entry : dash_pattern) { |
552 | 0 | VERIFY(entry >= 0); |
553 | 0 | pattern_width += entry; |
554 | 0 | } |
555 | | |
556 | | // 8. For each subpath `subpath` in `path`, run the following substeps. These substeps mutate the subpaths in `path` in vivo. |
557 | 0 | for (auto const& [subpath_index, subpath] : enumerate(segments)) { |
558 | 0 | float end, last_end = 0; |
559 | | |
560 | | // 1. Let `subpath width` be the length of all the lines of `subpath`, in coordinate space units. |
561 | 0 | float subpath_width = 0; |
562 | 0 | for (size_t i = 0; i < subpath.size() - 1; i++) |
563 | 0 | subpath_width += subpath[i].distance_from(subpath[i + 1]); |
564 | | |
565 | | // 2. Let `offset` be the value of style's lineDashOffset, in coordinate space units. |
566 | 0 | float offset = dash_offset; |
567 | | |
568 | | // 3. While `offset` is greater than `pattern width`, decrement it by pattern width. |
569 | | // While `offset` is less than zero, increment it by `pattern width`. |
570 | | // FIXME: Rewrite this using fmodf() in the future, once this has good test coverage. |
571 | 0 | while (offset > pattern_width) |
572 | 0 | offset -= pattern_width; |
573 | 0 | while (offset < 0) |
574 | 0 | offset += pattern_width; |
575 | | |
576 | | // 4. Define `L` to be a linear coordinate line defined along all lines in subpath, such that the start of the first line |
577 | | // in the subpath is defined as coordinate 0, and the end of the last line in the subpath is defined as coordinate `subpath width`. |
578 | 0 | float L = 0; |
579 | 0 | size_t current_vertex_index = 0; |
580 | |
|
581 | 0 | auto next_L = [&]() -> float { |
582 | 0 | return L + subpath[current_vertex_index].distance_from(subpath[current_vertex_index + 1]); |
583 | 0 | }; |
584 | |
|
585 | 0 | auto append_distinct = [](Vector<FloatPoint>& path, FloatPoint p) { |
586 | 0 | if (path.is_empty() || path.last() != p) |
587 | 0 | path.append(p); |
588 | 0 | }; |
589 | |
|
590 | 0 | auto skip_until = [&](float target_L) { |
591 | 0 | while (next_L() < target_L) { |
592 | 0 | L = next_L(); |
593 | 0 | current_vertex_index++; |
594 | 0 | } |
595 | 0 | }; |
596 | |
|
597 | 0 | auto append_until = [&](Vector<FloatPoint>& new_subpath, float target_L) { |
598 | 0 | while (next_L() < target_L) { |
599 | 0 | L = next_L(); |
600 | 0 | current_vertex_index++; |
601 | 0 | append_distinct(new_subpath, subpath[current_vertex_index]); |
602 | 0 | } |
603 | 0 | }; |
604 | |
|
605 | 0 | auto append_lerp = [&](Vector<FloatPoint>& new_subpath, float target_L) { |
606 | 0 | VERIFY(target_L >= L); |
607 | 0 | VERIFY(target_L <= next_L()); |
608 | 0 | append_distinct(new_subpath, mix(subpath[current_vertex_index], subpath[current_vertex_index + 1], (target_L - L) / (next_L() - L))); |
609 | 0 | }; |
610 | | |
611 | | // 5. Let `position` be zero minus offset. |
612 | 0 | float position = -offset; |
613 | | |
614 | | // 6. Let `index` be 0. |
615 | 0 | size_t index = 0; |
616 | | |
617 | | // 7. Let `current state` be off (the other states being on and zero-on). |
618 | | // (NOTE: The mentioned "zero-on" state in the spec appears unused.) |
619 | 0 | enum class State { |
620 | 0 | Off, |
621 | 0 | On, |
622 | 0 | }; |
623 | 0 | State current_state = State::Off; |
624 | |
|
625 | 0 | dash_on: |
626 | | // 8. Dash on: Let `segment length` be the value of style's dash list's `index`th entry. |
627 | 0 | float segment_length = dash_pattern[index]; |
628 | | |
629 | | // 9. Increment `position` by `segment length`. |
630 | 0 | position += segment_length; |
631 | | |
632 | | // 10. If `position` is greater than `subpath width`, then end these substeps for this subpath and start them again for the next subpath; |
633 | | // if there are no more subpaths, then jump to the step labeled `convert` instead. |
634 | 0 | if (position > subpath_width) { |
635 | 0 | if (last_end < subpath_width) { |
636 | | // Fill from last_end to subpath_width. |
637 | 0 | Vector<FloatPoint> new_subpath; |
638 | |
|
639 | 0 | skip_until(last_end); |
640 | 0 | append_lerp(new_subpath, last_end); |
641 | 0 | for (++current_vertex_index; current_vertex_index < subpath.size(); ++current_vertex_index) |
642 | 0 | append_distinct(new_subpath, subpath[current_vertex_index]); |
643 | |
|
644 | 0 | new_segments.append(move(new_subpath)); |
645 | 0 | } |
646 | 0 | continue; |
647 | 0 | } |
648 | | |
649 | | // 11. If `segment length` is nonzero, then let current state be on. |
650 | 0 | if (segment_length != 0) |
651 | 0 | current_state = State::On; |
652 | | |
653 | | // 12. Increment `index` by one. |
654 | 0 | index++; |
655 | | |
656 | | // 13. Dash off: Let segment length be the value of style's dash list's `index`th entry. |
657 | | // (NOTE: The label "Dash off:" in the spec appears unused.) |
658 | 0 | segment_length = dash_pattern[index]; |
659 | | |
660 | | // 14. Let `start` be the offset `position` on L. |
661 | 0 | float start = position; |
662 | | |
663 | | // 15. Increment `position` by `segment length`. |
664 | 0 | position += segment_length; |
665 | | |
666 | | // 16. If `position` is less than zero, then jump to the step labeled `post-cut`. |
667 | 0 | if (position < 0) |
668 | 0 | goto post_cut; |
669 | | |
670 | | // 17. If `start` is less than zero, then let `start` be zero. |
671 | 0 | if (start < 0) |
672 | 0 | start = 0; |
673 | | |
674 | | // 18. If `position` is greater than `subpath width`, then let `end` be the offset `subpath width` on `L`. Otherwise, let `end` be the offset `position` on `L`. |
675 | 0 | end = position > subpath_width ? subpath_width : position; |
676 | | |
677 | | // 19. Jump to the first appropriate step: |
678 | | // If segment length is zero and current state is off |
679 | | // Do nothing, just continue to the next step. |
680 | | // If current state is off |
681 | | // Cut the line on which `end` finds itself short at `end` and place a point there, cutting in two the subpath that it was in; |
682 | | // remove all line segments, joins, points, and subpaths that are between `start` and `end`; and finally place a single point at |
683 | | // `start` with no lines connecting to it. |
684 | | // The point has a directionality for the purposes of drawing line caps (see below). The directionality is the direction that |
685 | | // the original line had at that point (i.e. when `L` was defined above). |
686 | | // Otherwise |
687 | | // Cut the line on which `start` finds itself into two at `start` and place a point there, cutting in two the subpath that it was in, |
688 | | // and similarly cut the line on which `end` finds itself short at end and place a point there, cutting in two the subpath that it was in, |
689 | | // and then remove all line segments, joins, points, and subpaths that are between `start` and `end`. |
690 | 0 | if (segment_length == 0 && current_state == State::Off) { |
691 | | // Do nothing. |
692 | 0 | } else if (current_state == State::Off) { |
693 | 0 | Vector<FloatPoint> new_subpath; |
694 | |
|
695 | 0 | skip_until(start); |
696 | 0 | append_lerp(new_subpath, start); |
697 | | |
698 | | // FIXME: Store directionality. |
699 | 0 | new_segments.append(move(new_subpath)); |
700 | 0 | } else { |
701 | 0 | Vector<FloatPoint> new_subpath; |
702 | |
|
703 | 0 | skip_until(last_end); |
704 | 0 | append_lerp(new_subpath, last_end); |
705 | 0 | append_until(new_subpath, start); |
706 | 0 | append_lerp(new_subpath, start); |
707 | |
|
708 | 0 | new_segments.append(move(new_subpath)); |
709 | 0 | last_end = end; |
710 | 0 | } |
711 | | |
712 | | // 20. If start and end are the same point, then this results in just the line being cut in two and two points being inserted there, |
713 | | // with nothing being removed, unless a join also happens to be at that point, in which case the join must be removed. |
714 | | // FIXME: Not clear if we have to do anything here, given our inverted interval implementation. |
715 | |
|
716 | 0 | post_cut: |
717 | | // 21. Post-cut: If position is greater than subpath width, then jump to the step labeled convert. |
718 | 0 | if (position > subpath_width) |
719 | 0 | break; |
720 | | |
721 | | // 22. If segment length is greater than zero, then let positioned-at-on-dash be false. |
722 | | // (NOTE: The spec doesn't mention positioned-at-on-dash anywhere else.) |
723 | | |
724 | | // 23. Increment index by one. If it is equal to the number of entries in style's dash list, then let index be 0. |
725 | 0 | index++; |
726 | 0 | if (index == dash_pattern.size()) |
727 | 0 | index = 0; |
728 | | |
729 | | // 24. Return to the step labeled `dash on`. |
730 | 0 | goto dash_on; |
731 | 0 | } |
732 | | |
733 | 0 | segments = move(new_segments); |
734 | | |
735 | | // This function is only called if there are dashes, and dashes are never closed. |
736 | 0 | segment_is_closed.resize(segments.size()); |
737 | 0 | for (auto& is_closed : segment_is_closed) |
738 | 0 | is_closed = false; |
739 | 0 | } |
740 | | |
741 | | Path Path::stroke_to_fill(StrokeStyle const& style) const |
742 | 50.2k | { |
743 | | // Note: This convolves a polygon with the path using the algorithm described |
744 | | // in https://keithp.com/~keithp/talks/cairo2003.pdf (3.1 Stroking Splines via Convolution) |
745 | | // Cap style handling is done by replacing the convolution with an explicit shape |
746 | | // at the path's ends, but we still maintain a position on the pen and pretend we're convolving. |
747 | | |
748 | 50.2k | auto thickness = style.thickness; |
749 | 50.2k | auto cap_style = style.cap_style; |
750 | 50.2k | auto join_style = style.join_style; |
751 | | |
752 | 50.2k | VERIFY(thickness > 0); |
753 | | |
754 | 50.2k | auto lines = split_lines(); |
755 | 50.2k | if (lines.is_empty()) |
756 | 1.09k | return Path {}; |
757 | | |
758 | 49.1k | auto subpath_end_indices = split_lines_subbpath_end_indices(); |
759 | 49.1k | size_t current_subpath_end_indices_cursor = 0; |
760 | | |
761 | | // Paths can be disconnected, which a pain to deal with, so split it up. |
762 | | // Also filter out duplicate points here (but keep one-point paths around |
763 | | // since we draw round and square caps for them). |
764 | 49.1k | Vector<Vector<FloatPoint>> segments; |
765 | 49.1k | Vector<bool> segment_is_closed; |
766 | 49.1k | segments.append({ lines.first().a() }); |
767 | 22.3M | for (auto const& [line_index, line] : enumerate(lines)) { |
768 | 22.3M | bool previous_line_closed_segment = false; |
769 | 22.3M | if (subpath_end_indices.size() > current_subpath_end_indices_cursor) |
770 | 14.2M | previous_line_closed_segment = subpath_end_indices[current_subpath_end_indices_cursor] == line_index - 1; |
771 | | |
772 | 22.3M | if (line.a() == segments.last().last() && !previous_line_closed_segment) { |
773 | 22.2M | if (line.a() != line.b()) |
774 | 21.1M | segments.last().append(line.b()); |
775 | 22.2M | } else { |
776 | 20.9k | segment_is_closed.append(previous_line_closed_segment); |
777 | 20.9k | if (previous_line_closed_segment) |
778 | 3.90k | current_subpath_end_indices_cursor++; |
779 | 20.9k | segments.append({ line.a() }); |
780 | 20.9k | if (line.a() != line.b()) |
781 | 19.0k | segments.last().append(line.b()); |
782 | 20.9k | } |
783 | 22.3M | } |
784 | 49.1k | if (segment_is_closed.size() < segments.size()) { |
785 | 49.1k | bool previous_line_closed_segment = false; |
786 | 49.1k | if (subpath_end_indices.size() > current_subpath_end_indices_cursor) |
787 | 45.5k | previous_line_closed_segment = subpath_end_indices[current_subpath_end_indices_cursor] == lines.size() - 1; |
788 | 49.1k | segment_is_closed.append(previous_line_closed_segment); |
789 | 49.1k | if (previous_line_closed_segment) |
790 | 45.5k | current_subpath_end_indices_cursor++; |
791 | 49.1k | VERIFY(segment_is_closed.size() == segments.size()); |
792 | 49.1k | VERIFY(current_subpath_end_indices_cursor == subpath_end_indices.size()); |
793 | 49.1k | } |
794 | | |
795 | 49.1k | if (!style.dash_pattern.is_empty()) |
796 | 0 | apply_dash_pattern(segments, segment_is_closed, style.dash_pattern, style.dash_offset); |
797 | | |
798 | 49.1k | Vector<FloatPoint, 128> pen_vertices = make_pen(thickness); |
799 | | |
800 | 6.68M | static constexpr auto mod = [](int a, int b) { |
801 | 6.68M | VERIFY(b > 0); |
802 | 6.68M | VERIFY(a + b >= 0); |
803 | 6.68M | return (a + b) % b; |
804 | 6.68M | }; |
805 | 1.65M | auto wrapping_index = [](auto& vertices, auto index) { |
806 | 1.65M | return vertices[mod(index, vertices.size())]; |
807 | 1.65M | }; Path.cpp:auto Gfx::Path::stroke_to_fill(Gfx::Path::StrokeStyle const&) const::$_0::operator()<AK::Vector<Gfx::Point<float>, 128ul>, int>(AK::Vector<Gfx::Point<float>, 128ul>&, int) const Line | Count | Source | 805 | 1.65M | auto wrapping_index = [](auto& vertices, auto index) { | 806 | 1.65M | return vertices[mod(index, vertices.size())]; | 807 | 1.65M | }; |
Unexecuted instantiation: Path.cpp:auto Gfx::Path::stroke_to_fill(Gfx::Path::StrokeStyle const&) const::$_0::operator()<AK::Vector<Gfx::Path::stroke_to_fill(Gfx::Path::StrokeStyle const&) const::ActiveRange, 128ul>, int>(AK::Vector<Gfx::Path::stroke_to_fill(Gfx::Path::StrokeStyle const&) const::ActiveRange, 128ul>&, int) const |
808 | | |
809 | 44.1M | auto angle_between = [](auto p1, auto p2) { |
810 | 44.1M | auto delta = p2 - p1; |
811 | 44.1M | return atan2f(delta.y(), delta.x()); |
812 | 44.1M | }; |
813 | | |
814 | 49.1k | struct ActiveRange { |
815 | 49.1k | float start; |
816 | 49.1k | float end; |
817 | | |
818 | 49.1k | bool in_range(float angle) const |
819 | 48.0M | { |
820 | | // Note: Since active ranges go counterclockwise start > end unless we wrap around at 180 degrees |
821 | 48.0M | return ((angle <= start && angle >= end) |
822 | 9.54M | || (start < end && angle <= start) |
823 | 8.87M | || (start < end && angle >= end)); |
824 | 48.0M | } |
825 | 49.1k | }; |
826 | | |
827 | 49.1k | Vector<ActiveRange, 128> active_ranges; |
828 | 49.1k | active_ranges.ensure_capacity(pen_vertices.size()); |
829 | 877k | for (int i = 0; i < (int)pen_vertices.size(); i++) { |
830 | 828k | active_ranges.unchecked_append({ angle_between(wrapping_index(pen_vertices, i - 1), pen_vertices[i]), |
831 | 828k | angle_between(pen_vertices[i], wrapping_index(pen_vertices, i + 1)) }); |
832 | 828k | } |
833 | | |
834 | 5.03M | auto clockwise = [](float current_angle, float target_angle) { |
835 | 5.03M | if (target_angle < 0) |
836 | 2.50M | target_angle += AK::Pi<float> * 2; |
837 | 5.03M | if (current_angle < 0) |
838 | 1.71M | current_angle += AK::Pi<float> * 2; |
839 | 5.03M | if (target_angle < current_angle) |
840 | 1.97M | target_angle += AK::Pi<float> * 2; |
841 | | |
842 | 5.03M | auto angle = target_angle - current_angle; |
843 | | |
844 | | // If the end of the range is antiparallel to where we want to go, |
845 | | // we have to keep moving clockwise: In that case, the _next_ range |
846 | | // is what we want. |
847 | 5.03M | if (fabs(angle - AK::Pi<float>) < 0.0001f) |
848 | 5.00k | return true; |
849 | | |
850 | 5.02M | return angle <= AK::Pi<float>; |
851 | 5.03M | }; |
852 | | |
853 | 49.1k | Path convolution; |
854 | 70.1k | for (auto const& [segment_index, segment] : enumerate(segments)) { |
855 | 70.1k | if (segment.size() < 2) { |
856 | | // Draw round and square caps for single-point segments. |
857 | | // FIXME: THis is is a bit ad-hoc. It matches what most PDF engines do, |
858 | | // and matches what Chrome and Firefox (but not WebKit) do for canvas paths. |
859 | 37.8k | if (cap_style == CapStyle::Round) { |
860 | 37.8k | convolution.move_to(segment[0] + pen_vertices[0]); |
861 | 424k | for (int i = 1; i < (int)pen_vertices.size(); i++) |
862 | 386k | convolution.line_to(segment[0] + pen_vertices[i]); |
863 | 37.8k | convolution.close(); |
864 | 37.8k | } else if (cap_style == CapStyle::Square) { |
865 | 0 | convolution.rect({ segment[0].translated(-thickness / 2, -thickness / 2), { thickness, thickness } }); |
866 | 0 | } |
867 | 37.8k | continue; |
868 | 37.8k | } |
869 | | |
870 | 32.2k | RoundTrip<FloatPoint> shape { segment }; |
871 | | |
872 | 32.2k | bool first = true; |
873 | 47.4M | auto add_vertex = [&](auto v) { |
874 | 47.4M | if (first) { |
875 | 46.2k | convolution.move_to(v); |
876 | 46.2k | first = false; |
877 | 47.4M | } else { |
878 | 47.4M | convolution.line_to(v); |
879 | 47.4M | } |
880 | 47.4M | }; |
881 | | |
882 | 32.2k | auto shape_idx = 0u; |
883 | | |
884 | 46.2k | auto slope = [&] { |
885 | 46.2k | return angle_between(shape[shape_idx], shape[shape_idx + 1]); |
886 | 46.2k | }; |
887 | | |
888 | 32.2k | auto start_slope = slope(); |
889 | | // Note: At least one range must be active. |
890 | 411k | int active = *active_ranges.find_first_index_if([&](auto& range) { |
891 | 411k | return range.in_range(start_slope); |
892 | 411k | }); |
893 | | |
894 | 32.2k | shape_idx = 1; |
895 | | |
896 | 42.4M | auto add_round_join = [&](unsigned next_index) { |
897 | 42.4M | add_vertex(shape[shape_idx] + pen_vertices[active]); |
898 | 42.4M | auto slope_now = angle_between(shape[shape_idx], shape[next_index]); |
899 | 42.4M | auto range = active_ranges[active]; |
900 | 47.4M | while (!range.in_range(slope_now)) { |
901 | 5.03M | active = mod(active + (clockwise(slope_now, range.end) ? 1 : -1), pen_vertices.size()); |
902 | 5.03M | add_vertex(shape[shape_idx] + pen_vertices[active]); |
903 | 5.03M | range = active_ranges[active]; |
904 | 5.03M | } |
905 | 42.4M | }; |
906 | | |
907 | 32.2k | auto add_bevel_join = [&](unsigned next_index) { |
908 | 0 | add_vertex(shape[shape_idx] + pen_vertices[active]); |
909 | 0 | auto slope_now = angle_between(shape[shape_idx], shape[next_index]); |
910 | 0 | auto range = active_ranges[active]; |
911 | 0 | auto last_active = active; |
912 | 0 | while (!range.in_range(slope_now)) { |
913 | 0 | last_active = active; |
914 | 0 | active = mod(active + (clockwise(slope_now, range.end) ? 1 : -1), pen_vertices.size()); |
915 | 0 | range = active_ranges[active]; |
916 | 0 | } |
917 | 0 | if (last_active != active) |
918 | 0 | add_vertex(shape[shape_idx] + pen_vertices[active]); |
919 | 0 | }; |
920 | | |
921 | 32.2k | auto add_miter_join = [&](unsigned next_index) { |
922 | 0 | auto cross_product = [](FloatPoint const& p1, FloatPoint const& p2) { |
923 | 0 | return p1.x() * p2.y() - p1.y() * p2.x(); |
924 | 0 | }; |
925 | |
|
926 | 0 | auto segment1 = shape[shape_idx] - shape[shape_idx - 1]; |
927 | 0 | auto normal1 = FloatVector2(-segment1.y(), segment1.x()).normalized(); |
928 | 0 | auto offset1 = FloatPoint(normal1.x(), normal1.y()) * (thickness / 2); |
929 | 0 | auto p1 = shape[shape_idx - 1] + offset1; |
930 | |
|
931 | 0 | auto segment2 = shape[next_index] - shape[shape_idx]; |
932 | 0 | auto normal2 = FloatVector2(-segment2.y(), segment2.x()).normalized(); |
933 | 0 | auto offset2 = FloatPoint(normal2.x(), normal2.y()) * (thickness / 2); |
934 | 0 | auto p2 = shape[shape_idx] + offset2; |
935 | |
|
936 | 0 | auto denominator = cross_product(segment1, segment2); |
937 | 0 | if (denominator == 0) |
938 | 0 | return add_bevel_join(next_index); |
939 | | |
940 | 0 | auto intersection = p1 + segment1 * cross_product(p2 - p1, segment2) / denominator; |
941 | 0 | if (intersection.distance_from(shape[shape_idx]) / (thickness / 2) > style.miter_limit) |
942 | 0 | return add_bevel_join(next_index); |
943 | | |
944 | 0 | add_vertex(intersection); |
945 | 0 | auto slope_now = angle_between(shape[shape_idx], shape[next_index]); |
946 | 0 | auto range = active_ranges[active]; |
947 | 0 | while (!range.in_range(slope_now)) { |
948 | 0 | active = mod(active + (clockwise(slope_now, range.end) ? 1 : -1), pen_vertices.size()); |
949 | 0 | range = active_ranges[active]; |
950 | 0 | } |
951 | 0 | }; |
952 | | |
953 | 42.3M | auto add_linejoin = [&](unsigned next_index) { |
954 | 42.3M | switch (join_style) { |
955 | 0 | case JoinStyle::Miter: |
956 | 0 | add_miter_join(next_index); |
957 | 0 | break; |
958 | 42.3M | case JoinStyle::Round: |
959 | 42.3M | add_round_join(next_index); |
960 | 42.3M | break; |
961 | 0 | case JoinStyle::Bevel: |
962 | 0 | add_bevel_join(next_index); |
963 | 0 | break; |
964 | 42.3M | } |
965 | 42.3M | }; |
966 | | |
967 | 64.5k | auto trace_path_until_index = [&](size_t index) { |
968 | 42.4M | while (shape_idx < index) { |
969 | 42.3M | add_linejoin(shape_idx + 1); |
970 | 42.3M | shape_idx++; |
971 | 42.3M | } |
972 | 64.5k | }; |
973 | | |
974 | 36.5k | auto add_linecap = [&]() { |
975 | 36.5k | if (cap_style == CapStyle::Butt || cap_style == CapStyle::Square) { |
976 | 0 | auto segment = shape[shape_idx] - shape[shape_idx - 1]; |
977 | 0 | auto segment_vector = FloatVector2(segment.x(), segment.y()).normalized(); |
978 | 0 | auto normal = FloatVector2(-segment_vector.y(), segment_vector.x()); |
979 | 0 | auto offset = FloatPoint(normal.x() * (thickness / 2), normal.y() * (thickness / 2)); |
980 | 0 | auto p1 = shape[shape_idx] + offset; |
981 | 0 | auto p2 = shape[shape_idx] - offset; |
982 | 0 | if (cap_style == CapStyle::Square) { |
983 | 0 | auto square_cap_offset = segment_vector * (thickness / 2); |
984 | 0 | p1.translate_by(square_cap_offset.x(), square_cap_offset.y()); |
985 | 0 | p2.translate_by(square_cap_offset.x(), square_cap_offset.y()); |
986 | 0 | } |
987 | |
|
988 | 0 | add_vertex(p1); |
989 | 0 | auto slope_now = slope(); |
990 | 0 | active = mod(active + pen_vertices.size() / 2, pen_vertices.size()); |
991 | 0 | if (!active_ranges[active].in_range(slope_now)) { |
992 | 0 | if (wrapping_index(active_ranges, active + 1).in_range(slope_now)) |
993 | 0 | active = mod(active + 1, pen_vertices.size()); |
994 | 0 | else if (wrapping_index(active_ranges, active - 1).in_range(slope_now)) |
995 | 0 | active = mod(active - 1, pen_vertices.size()); |
996 | 0 | else |
997 | 0 | VERIFY_NOT_REACHED(); |
998 | 0 | } |
999 | 0 | add_vertex(p2); |
1000 | 0 | shape_idx++; |
1001 | 36.5k | } else { |
1002 | 36.5k | VERIFY(cap_style == CapStyle::Round); |
1003 | 36.5k | add_round_join(shape_idx + 1); |
1004 | 36.5k | } |
1005 | 36.5k | }; |
1006 | | |
1007 | 32.2k | bool current_segment_is_closed = segment_is_closed[segment_index]; |
1008 | | |
1009 | | // Outer stroke. |
1010 | 32.2k | trace_path_until_index(segment.size() - 1); |
1011 | 32.2k | VERIFY(shape_idx == segment.size() - 1); |
1012 | | |
1013 | | // Close outer stroke for closed paths, or draw cap 1 for open paths. |
1014 | 32.2k | if (current_segment_is_closed) { |
1015 | 13.9k | add_linejoin(1); |
1016 | | |
1017 | | // Start an independent path for the inner stroke. |
1018 | 13.9k | convolution.close(); |
1019 | 13.9k | first = true; |
1020 | | |
1021 | 13.9k | auto start_slope = slope(); |
1022 | 177k | active = *active_ranges.find_first_index_if([&](auto& range) { |
1023 | 177k | return range.in_range(start_slope); |
1024 | 177k | }); |
1025 | | |
1026 | 13.9k | ++shape_idx; |
1027 | 13.9k | VERIFY(shape_idx == segment.size()); |
1028 | 18.2k | } else { |
1029 | 18.2k | add_linecap(); |
1030 | 18.2k | } |
1031 | | |
1032 | | // Inner stroke. |
1033 | 32.2k | trace_path_until_index(2 * (segment.size() - 1)); |
1034 | 32.2k | VERIFY(shape_idx == 2 * (segment.size() - 1)); |
1035 | | |
1036 | | // Close inner stroke for closed paths, or draw cap 2 for open paths. |
1037 | 32.2k | if (current_segment_is_closed) { |
1038 | 13.9k | add_linejoin(segment.size()); |
1039 | 18.2k | } else { |
1040 | 18.2k | add_linecap(); |
1041 | 18.2k | } |
1042 | | |
1043 | 32.2k | convolution.close(); |
1044 | 32.2k | } |
1045 | | |
1046 | 49.1k | return convolution; |
1047 | 49.1k | } |
1048 | | |
1049 | | } |