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