diff --git a/engine/src/flutter/impeller/display_list/aiks_dl_path_unittests.cc b/engine/src/flutter/impeller/display_list/aiks_dl_path_unittests.cc index 1ee8ddebab..a450b907ea 100644 --- a/engine/src/flutter/impeller/display_list/aiks_dl_path_unittests.cc +++ b/engine/src/flutter/impeller/display_list/aiks_dl_path_unittests.cc @@ -153,6 +153,160 @@ TEST_P(AiksTest, CanRenderQuadraticStrokeWithInstantTurn) { ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); } +TEST_P(AiksTest, CanRenderFilledConicPaths) { + DisplayListBuilder builder; + builder.Scale(GetContentScale().x, GetContentScale().y); + + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setDrawStyle(DlDrawStyle::kFill); + + DlPaint reference_paint; + reference_paint.setColor(DlColor::kGreen()); + reference_paint.setDrawStyle(DlDrawStyle::kFill); + + DlPathBuilder path_builder; + DlPathBuilder reference_builder; + + // weight of 1.0 is just a quadratic bezier + path_builder.MoveTo(DlPoint(100, 100)); + path_builder.ConicCurveTo(DlPoint(150, 150), DlPoint(200, 100), 1.0f); + reference_builder.MoveTo(DlPoint(300, 100)); + reference_builder.QuadraticCurveTo(DlPoint(350, 150), DlPoint(400, 100)); + + // weight of sqrt(2)/2 is a circular section + path_builder.MoveTo(DlPoint(100, 200)); + path_builder.ConicCurveTo(DlPoint(150, 250), DlPoint(200, 200), kSqrt2Over2); + reference_builder.MoveTo(DlPoint(300, 200)); + auto magic = DlPathBuilder::kArcApproximationMagic; + reference_builder.CubicCurveTo(DlPoint(300, 200) + DlPoint(50, 50) * magic, + DlPoint(400, 200) + DlPoint(-50, 50) * magic, + DlPoint(400, 200)); + + // weight of .01 is nearly a straight line + path_builder.MoveTo(DlPoint(100, 300)); + path_builder.ConicCurveTo(DlPoint(150, 350), DlPoint(200, 300), 0.01f); + reference_builder.MoveTo(DlPoint(300, 300)); + reference_builder.LineTo(DlPoint(350, 300.5)); + reference_builder.LineTo(DlPoint(400, 300)); + + // weight of 100.0 is nearly a triangle + path_builder.MoveTo(DlPoint(100, 400)); + path_builder.ConicCurveTo(DlPoint(150, 450), DlPoint(200, 400), 100.0f); + reference_builder.MoveTo(DlPoint(300, 400)); + reference_builder.LineTo(DlPoint(350, 450)); + reference_builder.LineTo(DlPoint(400, 400)); + + builder.DrawPath(DlPath(path_builder), paint); + builder.DrawPath(DlPath(reference_builder), reference_paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanRenderStrokedConicPaths) { + DisplayListBuilder builder; + builder.Scale(GetContentScale().x, GetContentScale().y); + + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setStrokeWidth(10); + paint.setDrawStyle(DlDrawStyle::kStroke); + paint.setStrokeCap(DlStrokeCap::kRound); + paint.setStrokeJoin(DlStrokeJoin::kRound); + + DlPaint reference_paint; + reference_paint.setColor(DlColor::kGreen()); + reference_paint.setStrokeWidth(10); + reference_paint.setDrawStyle(DlDrawStyle::kStroke); + reference_paint.setStrokeCap(DlStrokeCap::kRound); + reference_paint.setStrokeJoin(DlStrokeJoin::kRound); + + DlPathBuilder path_builder; + DlPathBuilder reference_builder; + + // weight of 1.0 is just a quadratic bezier + path_builder.MoveTo(DlPoint(100, 100)); + path_builder.ConicCurveTo(DlPoint(150, 150), DlPoint(200, 100), 1.0f); + reference_builder.MoveTo(DlPoint(300, 100)); + reference_builder.QuadraticCurveTo(DlPoint(350, 150), DlPoint(400, 100)); + + // weight of sqrt(2)/2 is a circular section + path_builder.MoveTo(DlPoint(100, 200)); + path_builder.ConicCurveTo(DlPoint(150, 250), DlPoint(200, 200), kSqrt2Over2); + reference_builder.MoveTo(DlPoint(300, 200)); + auto magic = DlPathBuilder::kArcApproximationMagic; + reference_builder.CubicCurveTo(DlPoint(300, 200) + DlPoint(50, 50) * magic, + DlPoint(400, 200) + DlPoint(-50, 50) * magic, + DlPoint(400, 200)); + + // weight of .0 is a straight line + path_builder.MoveTo(DlPoint(100, 300)); + path_builder.ConicCurveTo(DlPoint(150, 350), DlPoint(200, 300), 0.0f); + reference_builder.MoveTo(DlPoint(300, 300)); + reference_builder.LineTo(DlPoint(400, 300)); + + // weight of 100.0 is nearly a triangle + path_builder.MoveTo(DlPoint(100, 400)); + path_builder.ConicCurveTo(DlPoint(150, 450), DlPoint(200, 400), 100.0f); + reference_builder.MoveTo(DlPoint(300, 400)); + reference_builder.LineTo(DlPoint(350, 450)); + reference_builder.LineTo(DlPoint(400, 400)); + + builder.DrawPath(DlPath(path_builder), paint); + builder.DrawPath(DlPath(reference_builder), reference_paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanRenderTightConicPath) { + DisplayListBuilder builder; + builder.Scale(GetContentScale().x, GetContentScale().y); + + DlPaint paint; + paint.setColor(DlColor::kRed()); + paint.setDrawStyle(DlDrawStyle::kFill); + + DlPaint reference_paint; + reference_paint.setColor(DlColor::kGreen()); + reference_paint.setDrawStyle(DlDrawStyle::kFill); + + DlPathBuilder path_builder; + + path_builder.MoveTo(DlPoint(100, 100)); + path_builder.ConicCurveTo(DlPoint(150, 450), DlPoint(200, 100), 5.0f); + + DlPathBuilder reference_builder; + ConicPathComponent component(DlPoint(300, 100), // + DlPoint(350, 450), // + DlPoint(400, 100), // + 5.0f); + reference_builder.MoveTo(component.p1); + constexpr int N = 100; + for (int i = 1; i < N; i++) { + reference_builder.LineTo(component.Solve(static_cast(i) / N)); + } + reference_builder.LineTo(component.p2); + + DlPaint line_paint; + line_paint.setColor(DlColor::kYellow()); + line_paint.setDrawStyle(DlDrawStyle::kStroke); + line_paint.setStrokeWidth(1.0f); + + // Draw some lines to provide a spacial reference for the curvature of + // the tips of the direct rendering and the manually tessellated versions. + builder.DrawLine(DlPoint(145, 100), DlPoint(145, 450), line_paint); + builder.DrawLine(DlPoint(155, 100), DlPoint(155, 450), line_paint); + builder.DrawLine(DlPoint(345, 100), DlPoint(345, 450), line_paint); + builder.DrawLine(DlPoint(355, 100), DlPoint(355, 450), line_paint); + builder.DrawLine(DlPoint(100, 392.5f), DlPoint(400, 392.5f), line_paint); + + // Draw the two paths (direct and manually tessellated) on top of the lines. + builder.DrawPath(DlPath(path_builder), paint); + builder.DrawPath(DlPath(reference_builder), reference_paint); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + TEST_P(AiksTest, CanRenderDifferencePaths) { DisplayListBuilder builder; diff --git a/engine/src/flutter/impeller/geometry/path_component.cc b/engine/src/flutter/impeller/geometry/path_component.cc index f61e89a704..6d7d302c72 100644 --- a/engine/src/flutter/impeller/geometry/path_component.cc +++ b/engine/src/flutter/impeller/geometry/path_component.cc @@ -190,9 +190,9 @@ static inline Scalar ConicSolve(Scalar t, Scalar p2, Scalar w) { auto u = (1 - t); - auto coefficient_p0 = t * t; + auto coefficient_p0 = u * u; auto coefficient_p1 = 2 * t * u * w; - auto coefficient_p2 = u * u; + auto coefficient_p2 = t * t; return ((p0 * coefficient_p0 + p1 * coefficient_p1 + p2 * coefficient_p2) / (coefficient_p0 + coefficient_p1 + coefficient_p2)); @@ -331,27 +331,34 @@ Point ConicPathComponent::Solve(Scalar time) const { }; } +void ConicPathComponent::ToLinearPathComponents(Scalar scale_factor, + const PointProc& proc) const { + Scalar line_count = std::ceilf(ComputeConicSubdivisions(scale_factor, *this)); + for (size_t i = 1; i < line_count; i += 1) { + proc(Solve(i / line_count)); + } + proc(p2); +} + void ConicPathComponent::AppendPolylinePoints( Scalar scale_factor, std::vector& points) const { - for (auto quad : ToQuadraticPathComponents()) { - quad.AppendPolylinePoints(scale_factor, points); - } + ToLinearPathComponents(scale_factor, [&points](const Point& point) { + points.emplace_back(point); + }); } void ConicPathComponent::ToLinearPathComponents(Scalar scale, VertexWriter& writer) const { - for (auto quad : ToQuadraticPathComponents()) { - quad.ToLinearPathComponents(scale, writer); + Scalar line_count = std::ceilf(ComputeConicSubdivisions(scale, *this)); + for (size_t i = 1; i < line_count; i += 1) { + writer.Write(Solve(i / line_count)); } + writer.Write(p2); } size_t ConicPathComponent::CountLinearPathComponents(Scalar scale) const { - size_t count = 0; - for (auto quad : ToQuadraticPathComponents()) { - count += quad.CountLinearPathComponents(scale); - } - return count; + return std::ceilf(ComputeConicSubdivisions(scale, *this)) + 2; } std::vector ConicPathComponent::Extrema() const { diff --git a/engine/src/flutter/impeller/geometry/path_component.h b/engine/src/flutter/impeller/geometry/path_component.h index f2aeea4699..fb2be614aa 100644 --- a/engine/src/flutter/impeller/geometry/path_component.h +++ b/engine/src/flutter/impeller/geometry/path_component.h @@ -252,6 +252,10 @@ struct ConicPathComponent { void AppendPolylinePoints(Scalar scale_factor, std::vector& points) const; + using PointProc = std::function; + + void ToLinearPathComponents(Scalar scale_factor, const PointProc& proc) const; + void ToLinearPathComponents(Scalar scale, VertexWriter& writer) const; size_t CountLinearPathComponents(Scalar scale) const; diff --git a/engine/src/flutter/impeller/geometry/wangs_formula.cc b/engine/src/flutter/impeller/geometry/wangs_formula.cc index 302785b619..f5b775c22a 100644 --- a/engine/src/flutter/impeller/geometry/wangs_formula.cc +++ b/engine/src/flutter/impeller/geometry/wangs_formula.cc @@ -39,6 +39,49 @@ Scalar ComputeQuadradicSubdivisions(Scalar scale_factor, return std::sqrt(k * length(p0 - p1 * 2 + p2)); } +// Returns Wang's formula specialized for a conic curve. +// +// This is not actually due to Wang, but is an analogue from: +// (Theorem 3, corollary 1): +// J. Zheng, T. Sederberg. "Estimating Tessellation Parameter Intervals for +// Rational Curves and Surfaces." ACM Transactions on Graphics 19(1). 2000. +Scalar ComputeConicSubdivisions(Scalar scale_factor, + Point p0, + Point p1, + Point p2, + Scalar w) { + // Compute center of bounding box in projected space + const Point C = 0.5f * (p0.Min(p1).Min(p2) + p0.Max(p1).Max(p2)); + + // Translate by -C. This improves translation-invariance of the formula, + // see Sec. 3.3 of cited paper + p0 -= C; + p1 -= C; + p2 -= C; + + // Compute max length + const Scalar max_len = + std::sqrt(std::max(p0.Dot(p0), std::max(p1.Dot(p1), p2.Dot(p2)))); + + // Compute forward differences + const Point dp = -2 * w * p1 + p0 + p2; + const Scalar dw = std::abs(-2 * w + 2); + + // Compute numerator and denominator for parametric step size of + // linearization. Here, the epsilon referenced from the cited paper + // is 1/precision. + Scalar k = scale_factor * kPrecision; + const Scalar rp_minus_1 = std::max(0.0f, max_len * k - 1); + const Scalar numer = std::sqrt(dp.Dot(dp)) * k + rp_minus_1 * dw; + const Scalar denom = 4 * std::min(w, 1.0f); + + // Number of segments = sqrt(numer / denom). + // This assumes parametric interval of curve being linearized is + // [t0,t1] = [0, 1]. + // If not, the number of segments is (tmax - tmin) / sqrt(denom / numer). + return std::sqrt(numer / denom); +} + Scalar ComputeQuadradicSubdivisions(Scalar scale_factor, const QuadraticPathComponent& quad) { return ComputeQuadradicSubdivisions(scale_factor, quad.p1, quad.cp, quad.p2); @@ -50,4 +93,10 @@ Scalar ComputeCubicSubdivisions(float scale_factor, cub.p2); } +Scalar ComputeConicSubdivisions(float scale_factor, + const ConicPathComponent& conic) { + return ComputeConicSubdivisions(scale_factor, conic.p1, conic.cp, conic.p2, + conic.weight.x); +} + } // namespace impeller diff --git a/engine/src/flutter/impeller/geometry/wangs_formula.h b/engine/src/flutter/impeller/geometry/wangs_formula.h index df26365db3..907512e680 100644 --- a/engine/src/flutter/impeller/geometry/wangs_formula.h +++ b/engine/src/flutter/impeller/geometry/wangs_formula.h @@ -45,6 +45,17 @@ Scalar ComputeQuadradicSubdivisions(Scalar scale_factor, Point p1, Point p2); +/// Returns the minimum number of evenly spaced (in the parametric sense) line +/// segments that the conic must be chopped into in order to guarantee all +/// lines stay within a distance of "1/intolerance" pixels from the true curve. +/// +/// The scale_factor should be the max basis XY of the current transform. +Scalar ComputeConicSubdivisions(Scalar scale_factor, + Point p0, + Point p1, + Point p2, + Scalar w); + /// Returns the minimum number of evenly spaced (in the parametric sense) line /// segments that the quadratic must be chopped into in order to guarantee all /// lines stay within a distance of "1/intolerance" pixels from the true curve. @@ -60,6 +71,14 @@ Scalar ComputeQuadradicSubdivisions(Scalar scale_factor, /// The scale_factor should be the max basis XY of the current transform. Scalar ComputeCubicSubdivisions(float scale_factor, const CubicPathComponent& cub); + +/// Returns the minimum number of evenly spaced (in the parametric sense) line +/// segments that the conic must be chopped into in order to guarantee all lines +/// stay within a distance of "1/intolerance" pixels from the true curve. +/// +/// The scale_factor should be the max basis XY of the current transform. +Scalar ComputeConicSubdivisions(float scale_factor, + const ConicPathComponent& conic); } // namespace impeller #endif // FLUTTER_IMPELLER_GEOMETRY_WANGS_FORMULA_H_