Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 1230eec

Browse files
authored
[Impeller] Round out extreme angles between curve polyline segments. (#53210)
Resolves flutter/flutter#137956. Detect when the angle between two curve segments is > 10 degrees and insert smoothing geometry if so. See [this comment](flutter/flutter#137956 (comment)) for the full rationale behind this change. ```dart Paint paint = Paint() ..color = Colors.white ..strokeWidth = 50 ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round ..style = PaintingStyle.stroke; Path path = Path(); path.moveTo(0, 0); path.quadraticBezierTo(100, 100, 0, 0); canvas.drawPath(path, paint); ``` Before: <img width="166" alt="image" src="https://github.com/flutter/engine/assets/919017/bbca80fb-a928-42aa-9e85-ad8e345558de"> After: <img width="145" alt="image" src="https://github.com/flutter/engine/assets/919017/aab6ffc8-d03e-4c2f-af83-c125d7acc250">
1 parent 5a0a499 commit 1230eec

File tree

5 files changed

+150
-21
lines changed

5 files changed

+150
-21
lines changed

impeller/aiks/aiks_path_unittests.cc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,25 @@ TEST_P(AiksTest, CanRenderStrokePathWithCubicLine) {
9191
ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));
9292
}
9393

94+
TEST_P(AiksTest, CanRenderQuadraticStrokeWithInstantTurn) {
95+
Canvas canvas;
96+
97+
Paint paint;
98+
paint.color = Color::Red();
99+
paint.style = Paint::Style::kStroke;
100+
paint.stroke_cap = Cap::kRound;
101+
paint.stroke_width = 50;
102+
103+
// Should draw a diagonal pill shape. If flat on either end, the stroke is
104+
// rendering wrong.
105+
PathBuilder builder;
106+
builder.MoveTo({250, 250});
107+
builder.QuadraticCurveTo({100, 100}, {250, 250});
108+
109+
canvas.DrawPath(builder.TakePath(), paint);
110+
ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));
111+
}
112+
94113
TEST_P(AiksTest, CanRenderDifferencePaths) {
95114
Canvas canvas;
96115

impeller/entity/geometry/stroke_path_geometry.cc

Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "impeller/core/buffer_view.h"
88
#include "impeller/core/formats.h"
99
#include "impeller/entity/geometry/geometry.h"
10+
#include "impeller/geometry/constants.h"
1011
#include "impeller/geometry/path_builder.h"
1112
#include "impeller/geometry/path_component.h"
1213

@@ -90,7 +91,7 @@ class StrokeGenerator {
9091
previous_offset = offset;
9192
offset = ComputeOffset(contour_start_point_i, contour_start_point_i,
9293
contour_end_point_i, contour);
93-
const Point contour_first_offset = offset;
94+
const Point contour_first_offset = offset.GetVector();
9495

9596
if (contour_i > 0) {
9697
// This branch only executes when we've just finished drawing a contour
@@ -155,19 +156,52 @@ class StrokeGenerator {
155156
cap_proc(vtx_builder, polyline.GetPoint(contour_end_point_i - 1),
156157
cap_offset, scale, /*reverse=*/false);
157158
} else {
158-
join_proc(vtx_builder, polyline.GetPoint(contour_start_point_i), offset,
159-
contour_first_offset, scaled_miter_limit, scale);
159+
join_proc(vtx_builder, polyline.GetPoint(contour_start_point_i),
160+
offset.GetVector(), contour_first_offset, scaled_miter_limit,
161+
scale);
160162
}
161163
}
162164
}
163165

166+
/// @brief Represents a ray in 2D space.
167+
///
168+
/// This is a simple convenience struct for handling polyline offset
169+
/// values when generating stroke geometry. For performance reasons,
170+
/// it's sometimes adventageous to track the direction and magnitude
171+
/// for offsets separately.
172+
struct Ray {
173+
/// The normalized direction of the ray.
174+
Vector2 direction;
175+
176+
/// The magnitude of the ray.
177+
Scalar magnitude = 0.0;
178+
179+
/// Returns the vector representation of the ray.
180+
Vector2 GetVector() const { return direction * magnitude; }
181+
182+
/// Returns the scalar alignment of the two rays.
183+
///
184+
/// Domain: [-1, 1]
185+
/// A value of 1 indicates the rays are parallel and pointing in the same
186+
/// direction. A value of -1 indicates the rays are parallel and pointing in
187+
/// opposite directions. A value of 0 indicates the rays are perpendicular.
188+
Scalar GetAlignment(const Ray& other) const {
189+
return direction.Dot(other.direction);
190+
}
191+
192+
/// Returns the scalar angle between the two rays.
193+
Radians AngleTo(const Ray& other) const {
194+
return direction.AngleTo(other.direction);
195+
}
196+
};
197+
164198
/// Computes offset by calculating the direction from point_i - 1 to point_i
165199
/// if point_i is within `contour_start_point_i` and `contour_end_point_i`;
166200
/// Otherwise, it uses direction from contour.
167-
Point ComputeOffset(const size_t point_i,
168-
const size_t contour_start_point_i,
169-
const size_t contour_end_point_i,
170-
const Path::PolylineContour& contour) const {
201+
Ray ComputeOffset(const size_t point_i,
202+
const size_t contour_start_point_i,
203+
const size_t contour_end_point_i,
204+
const Path::PolylineContour& contour) const {
171205
Point direction;
172206
if (point_i >= contour_end_point_i) {
173207
direction = contour.end_direction;
@@ -177,7 +211,8 @@ class StrokeGenerator {
177211
direction = (polyline.GetPoint(point_i) - polyline.GetPoint(point_i - 1))
178212
.Normalize();
179213
}
180-
return Vector2{-direction.y, direction.x} * stroke_width * 0.5f;
214+
return {.direction = Vector2{-direction.y, direction.x},
215+
.magnitude = stroke_width * 0.5f};
181216
}
182217

183218
void AddVerticesForLinearComponent(VertexWriter& vtx_builder,
@@ -192,25 +227,29 @@ class StrokeGenerator {
192227
for (size_t point_i = component_start_index; point_i < component_end_index;
193228
point_i++) {
194229
bool is_end_of_component = point_i == component_end_index - 1;
195-
vtx.position = polyline.GetPoint(point_i) + offset;
230+
231+
Point offset_vector = offset.GetVector();
232+
233+
vtx.position = polyline.GetPoint(point_i) + offset_vector;
196234
vtx_builder.AppendVertex(vtx.position);
197-
vtx.position = polyline.GetPoint(point_i) - offset;
235+
vtx.position = polyline.GetPoint(point_i) - offset_vector;
198236
vtx_builder.AppendVertex(vtx.position);
199237

200238
// For line components, two additional points need to be appended
201239
// prior to appending a join connecting the next component.
202-
vtx.position = polyline.GetPoint(point_i + 1) + offset;
240+
vtx.position = polyline.GetPoint(point_i + 1) + offset_vector;
203241
vtx_builder.AppendVertex(vtx.position);
204-
vtx.position = polyline.GetPoint(point_i + 1) - offset;
242+
vtx.position = polyline.GetPoint(point_i + 1) - offset_vector;
205243
vtx_builder.AppendVertex(vtx.position);
206244

207245
previous_offset = offset;
208246
offset = ComputeOffset(point_i + 2, contour_start_point_i,
209247
contour_end_point_i, contour);
210248
if (!is_last_component && is_end_of_component) {
211249
// Generate join from the current line to the next line.
212-
join_proc(vtx_builder, polyline.GetPoint(point_i + 1), previous_offset,
213-
offset, scaled_miter_limit, scale);
250+
join_proc(vtx_builder, polyline.GetPoint(point_i + 1),
251+
previous_offset.GetVector(), offset.GetVector(),
252+
scaled_miter_limit, scale);
214253
}
215254
}
216255
}
@@ -228,14 +267,44 @@ class StrokeGenerator {
228267
point_i++) {
229268
bool is_end_of_component = point_i == component_end_index - 1;
230269

231-
vtx.position = polyline.GetPoint(point_i) + offset;
270+
vtx.position = polyline.GetPoint(point_i) + offset.GetVector();
232271
vtx_builder.AppendVertex(vtx.position);
233-
vtx.position = polyline.GetPoint(point_i) - offset;
272+
vtx.position = polyline.GetPoint(point_i) - offset.GetVector();
234273
vtx_builder.AppendVertex(vtx.position);
235274

236275
previous_offset = offset;
237276
offset = ComputeOffset(point_i + 2, contour_start_point_i,
238277
contour_end_point_i, contour);
278+
279+
// If the angle to the next segment is too sharp, round out the join.
280+
if (!is_end_of_component) {
281+
constexpr Scalar kAngleThreshold = 10 * kPi / 180;
282+
// `std::cosf` is not constexpr-able, unfortunately, so we have to bake
283+
// the alignment constant.
284+
constexpr Scalar kAlignmentThreshold =
285+
0.984807753012208; // std::cosf(kThresholdAngle) -- 10 degrees
286+
287+
// Use a cheap dot product to determine whether the angle is too sharp.
288+
if (previous_offset.GetAlignment(offset) < kAlignmentThreshold) {
289+
Scalar angle_total = previous_offset.AngleTo(offset).radians;
290+
Scalar angle = kAngleThreshold;
291+
292+
// Bridge the large angle with additional geometry at
293+
// `kAngleThreshold` interval.
294+
while (angle < std::abs(angle_total)) {
295+
Scalar signed_angle = angle_total < 0 ? -angle : angle;
296+
Point offset =
297+
previous_offset.GetVector().Rotate(Radians(signed_angle));
298+
vtx.position = polyline.GetPoint(point_i) + offset;
299+
vtx_builder.AppendVertex(vtx.position);
300+
vtx.position = polyline.GetPoint(point_i) - offset;
301+
vtx_builder.AppendVertex(vtx.position);
302+
303+
angle += kAngleThreshold;
304+
}
305+
}
306+
}
307+
239308
// For curve components, the polyline is detailed enough such that
240309
// it can avoid worrying about joins altogether.
241310
if (is_end_of_component) {
@@ -245,16 +314,18 @@ class StrokeGenerator {
245314
// `ComputeOffset` returns the contour's end direction when attempting
246315
// to grab offsets past `contour_end_point_i`, so just use `offset` when
247316
// we're on the last component.
248-
Point last_component_offset =
249-
is_last_component ? offset : previous_offset;
317+
Point last_component_offset = is_last_component
318+
? offset.GetVector()
319+
: previous_offset.GetVector();
250320
vtx.position = polyline.GetPoint(point_i + 1) + last_component_offset;
251321
vtx_builder.AppendVertex(vtx.position);
252322
vtx.position = polyline.GetPoint(point_i + 1) - last_component_offset;
253323
vtx_builder.AppendVertex(vtx.position);
254324
// Generate join from the current line to the next line.
255325
if (!is_last_component) {
256326
join_proc(vtx_builder, polyline.GetPoint(point_i + 1),
257-
previous_offset, offset, scaled_miter_limit, scale);
327+
previous_offset.GetVector(), offset.GetVector(),
328+
scaled_miter_limit, scale);
258329
}
259330
}
260331
}
@@ -267,8 +338,8 @@ class StrokeGenerator {
267338
const CapProc<VertexWriter>& cap_proc;
268339
const Scalar scale;
269340

270-
Point previous_offset;
271-
Point offset;
341+
Ray previous_offset;
342+
Ray offset;
272343
SolidFillVertexShader::PerVertexData vtx;
273344
};
274345

impeller/geometry/geometry_unittests.cc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,36 @@ TEST(GeometryTest, PointAbs) {
956956
ASSERT_POINT_NEAR(a_abs, expected);
957957
}
958958

959+
TEST(GeometryTest, PointRotate) {
960+
{
961+
Point a(1, 0);
962+
auto rotated = a.Rotate(Radians{kPiOver2});
963+
auto expected = Point(0, 1);
964+
ASSERT_POINT_NEAR(rotated, expected);
965+
}
966+
967+
{
968+
Point a(1, 0);
969+
auto rotated = a.Rotate(Radians{-kPiOver2});
970+
auto expected = Point(0, -1);
971+
ASSERT_POINT_NEAR(rotated, expected);
972+
}
973+
974+
{
975+
Point a(1, 0);
976+
auto rotated = a.Rotate(Radians{kPi});
977+
auto expected = Point(-1, 0);
978+
ASSERT_POINT_NEAR(rotated, expected);
979+
}
980+
981+
{
982+
Point a(1, 0);
983+
auto rotated = a.Rotate(Radians{kPi * 1.5});
984+
auto expected = Point(0, -1);
985+
ASSERT_POINT_NEAR(rotated, expected);
986+
}
987+
}
988+
959989
TEST(GeometryTest, PointAngleTo) {
960990
// Negative result in the CCW (with up = -Y) direction.
961991
{

impeller/geometry/point.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,12 @@ struct TPoint {
223223
return *this - axis * this->Dot(axis) * 2;
224224
}
225225

226+
constexpr TPoint Rotate(const Radians& angle) const {
227+
const auto cos_a = std::cosf(angle.radians);
228+
const auto sin_a = std::sinf(angle.radians);
229+
return {x * cos_a - y * sin_a, x * sin_a + y * cos_a};
230+
}
231+
226232
constexpr Radians AngleTo(const TPoint& p) const {
227233
return Radians{std::atan2(this->Cross(p), this->Dot(p))};
228234
}

testing/impeller_golden_tests_output.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,9 @@ impeller_Play_AiksTest_CanRenderNestedClips_Vulkan.png
382382
impeller_Play_AiksTest_CanRenderOverlappingMultiContourPath_Metal.png
383383
impeller_Play_AiksTest_CanRenderOverlappingMultiContourPath_OpenGLES.png
384384
impeller_Play_AiksTest_CanRenderOverlappingMultiContourPath_Vulkan.png
385+
impeller_Play_AiksTest_CanRenderQuadraticStrokeWithInstantTurn_Metal.png
386+
impeller_Play_AiksTest_CanRenderQuadraticStrokeWithInstantTurn_OpenGLES.png
387+
impeller_Play_AiksTest_CanRenderQuadraticStrokeWithInstantTurn_Vulkan.png
385388
impeller_Play_AiksTest_CanRenderRadialGradientManyColors_Metal.png
386389
impeller_Play_AiksTest_CanRenderRadialGradientManyColors_OpenGLES.png
387390
impeller_Play_AiksTest_CanRenderRadialGradientManyColors_Vulkan.png

0 commit comments

Comments
 (0)