From 3c58e2bbcaf264aac2eab27ba125fba4da869853 Mon Sep 17 00:00:00 2001 From: Aaron Clarke Date: Wed, 13 Dec 2023 16:39:43 -0800 Subject: [PATCH 1/4] [Impeller] grouped aiks unittests by type --- impeller/aiks/aiks_unittests.cc | 2928 ++++++++++++++++--------------- 1 file changed, 1475 insertions(+), 1453 deletions(-) diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc index c0f88a1d3bd41..d4a98acd63569 100644 --- a/impeller/aiks/aiks_unittests.cc +++ b/impeller/aiks/aiks_unittests.cc @@ -58,303 +58,213 @@ using AiksTest = AiksPlayground; #endif INSTANTIATE_PLAYGROUND_SUITE(AiksTest); -TEST_P(AiksTest, RotateColorFilteredPath) { +/// These tests check the visual results of drawing gradients. +namespace gradients { +namespace { +void CanRenderLinearGradient(AiksTest* aiks_test, Entity::TileMode tile_mode) { Canvas canvas; - canvas.Concat(Matrix::MakeTranslation({300, 300})); - canvas.Concat(Matrix::MakeRotationZ(Radians(kPiOver2))); - auto arrow_stem = - PathBuilder{}.MoveTo({120, 190}).LineTo({120, 50}).TakePath(); - auto arrow_head = PathBuilder{} - .MoveTo({50, 120}) - .LineTo({120, 190}) - .LineTo({190, 120}) - .TakePath(); - auto paint = Paint{ - .stroke_width = 15.0, - .stroke_cap = Cap::kRound, - .stroke_join = Join::kRound, - .style = Paint::Style::kStroke, - .color_filter = - ColorFilter::MakeBlend(BlendMode::kSourceIn, Color::AliceBlue()), - }; - - canvas.DrawPath(std::move(arrow_stem), paint); - canvas.DrawPath(std::move(arrow_head), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} + canvas.Scale(aiks_test->GetContentScale()); + Paint paint; + canvas.Translate({100.0f, 0, 0}); -TEST_P(AiksTest, CanvasCTMCanBeUpdated) { - Canvas canvas; - Matrix identity; - ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), identity); - canvas.Translate(Size{100, 100}); - ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), - Matrix::MakeTranslation({100.0, 100.0, 0.0})); -} + std::vector colors = {Color{0.9568, 0.2627, 0.2118, 1.0}, + Color{0.1294, 0.5882, 0.9529, 0.0}}; + std::vector stops = {0.0, 1.0}; -TEST_P(AiksTest, CanvasCanPushPopCTM) { - Canvas canvas; - ASSERT_EQ(canvas.GetSaveCount(), 1u); - ASSERT_EQ(canvas.Restore(), false); + paint.color_source = ColorSource::MakeLinearGradient( + {0, 0}, {200, 200}, std::move(colors), std::move(stops), tile_mode, {}); - canvas.Translate(Size{100, 100}); - canvas.Save(); - ASSERT_EQ(canvas.GetSaveCount(), 2u); - ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), - Matrix::MakeTranslation({100.0, 100.0, 0.0})); - ASSERT_TRUE(canvas.Restore()); - ASSERT_EQ(canvas.GetSaveCount(), 1u); - ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), - Matrix::MakeTranslation({100.0, 100.0, 0.0})); + paint.color = Color(1.0, 1.0, 1.0, 1.0); + canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); + ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +} // namespace -TEST_P(AiksTest, CanRenderColoredRect) { - Canvas canvas; - Paint paint; - paint.color = Color::Blue(); - canvas.DrawPath(PathBuilder{} - .AddRect(Rect::MakeXYWH(100.0, 100.0, 100.0, 100.0)) - .TakePath(), - paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +TEST_P(AiksTest, CanRenderLinearGradientClamp) { + CanRenderLinearGradient(this, Entity::TileMode::kClamp); } - -TEST_P(AiksTest, CanRenderImage) { - Canvas canvas; - Paint paint; - auto image = std::make_shared(CreateTextureForFixture("kalimba.jpg")); - paint.color = Color::Red(); - canvas.DrawImage(image, Point::MakeXY(100.0, 100.0), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +TEST_P(AiksTest, CanRenderLinearGradientRepeat) { + CanRenderLinearGradient(this, Entity::TileMode::kRepeat); +} +TEST_P(AiksTest, CanRenderLinearGradientMirror) { + CanRenderLinearGradient(this, Entity::TileMode::kMirror); +} +TEST_P(AiksTest, CanRenderLinearGradientDecal) { + CanRenderLinearGradient(this, Entity::TileMode::kDecal); } -TEST_P(AiksTest, CanRenderInvertedImageWithColorFilter) { +TEST_P(AiksTest, CanRenderLinearGradientDecalWithColorFilter) { Canvas canvas; + canvas.Scale(GetContentScale()); Paint paint; - auto image = std::make_shared(CreateTextureForFixture("kalimba.jpg")); - paint.color = Color::Red(); - paint.color_filter = - ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); - paint.invert_colors = true; + canvas.Translate({100.0f, 0, 0}); - canvas.DrawImage(image, Point::MakeXY(100.0, 100.0), paint); + std::vector colors = {Color{0.9568, 0.2627, 0.2118, 1.0}, + Color{0.1294, 0.5882, 0.9529, 0.0}}; + std::vector stops = {0.0, 1.0}; + + paint.color_source = ColorSource::MakeLinearGradient( + {0, 0}, {200, 200}, std::move(colors), std::move(stops), + Entity::TileMode::kDecal, {}); + // Overlay the gradient with 25% green. This should appear as the entire + // rectangle being drawn with 25% green, including the border area outside the + // decal gradient. + paint.color_filter = ColorFilter::MakeBlend(BlendMode::kSourceOver, + Color::Green().WithAlpha(0.25)); + + paint.color = Color(1.0, 1.0, 1.0, 1.0); + canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderColorFilterWithInvertColors) { +static void CanRenderLinearGradientWithDithering(AiksTest* aiks_test, + bool use_dithering) { Canvas canvas; Paint paint; - paint.color = Color::Red(); - paint.color_filter = - ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); - paint.invert_colors = true; + canvas.Translate({100.0, 100.0, 0}); - canvas.DrawRect(Rect::MakeLTRB(0, 0, 100, 100), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); + // 0xffcccccc --> 0xff333333, taken from + // https://github.com/flutter/flutter/issues/118073#issue-1521699748 + std::vector colors = {Color{0.8, 0.8, 0.8, 1.0}, + Color{0.2, 0.2, 0.2, 1.0}}; + std::vector stops = {0.0, 1.0}; + + paint.color_source = ColorSource::MakeLinearGradient( + {0, 0}, {800, 500}, std::move(colors), std::move(stops), + Entity::TileMode::kClamp, {}); + paint.dither = use_dithering; + canvas.DrawRect(Rect::MakeXYWH(0, 0, 800, 500), paint); + ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderColorFilterWithInvertColorsDrawPaint) { +TEST_P(AiksTest, CanRenderLinearGradientWithDitheringDisabled) { + CanRenderLinearGradientWithDithering(this, false); +} + +TEST_P(AiksTest, CanRenderLinearGradientWithDitheringEnabled) { + CanRenderLinearGradientWithDithering(this, true); +} // namespace + +static void CanRenderRadialGradientWithDithering(AiksTest* aiks_test, + bool use_dithering) { Canvas canvas; Paint paint; - paint.color = Color::Red(); - paint.color_filter = - ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); - paint.invert_colors = true; + canvas.Translate({100.0, 100.0, 0}); - canvas.DrawPaint(paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); + // #FFF -> #000 + std::vector colors = {Color{1.0, 1.0, 1.0, 1.0}, + Color{0.0, 0.0, 0.0, 1.0}}; + std::vector stops = {0.0, 1.0}; + + paint.color_source = ColorSource::MakeRadialGradient( + {600, 600}, 600, std::move(colors), std::move(stops), + Entity::TileMode::kClamp, {}); + paint.dither = use_dithering; + canvas.DrawRect(Rect::MakeXYWH(0, 0, 1200, 1200), paint); + ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -namespace { -bool GenerateMipmap(const std::shared_ptr& context, - std::shared_ptr texture, - std::string label, - bool async_submit) { - auto buffer = context->CreateCommandBuffer(); - if (!buffer) { - return false; - } - auto pass = buffer->CreateBlitPass(); - if (!pass) { - return false; - } - pass->GenerateMipmap(std::move(texture), std::move(label)); - if (async_submit) { - return buffer->EncodeAndSubmit(pass, context->GetResourceAllocator()); - } +TEST_P(AiksTest, CanRenderRadialGradientWithDitheringDisabled) { + CanRenderRadialGradientWithDithering(this, false); +} - pass->EncodeCommands(context->GetResourceAllocator()); - return buffer->SubmitCommands(); +TEST_P(AiksTest, CanRenderRadialGradientWithDitheringEnabled) { + CanRenderRadialGradientWithDithering(this, true); } -void CanRenderTiledTexture(AiksTest* aiks_test, - Entity::TileMode tile_mode, - bool async_submit = false, - Matrix local_matrix = {}) { - auto context = aiks_test->GetContext(); - ASSERT_TRUE(context); - auto texture = aiks_test->CreateTextureForFixture("table_mountain_nx.png", - /*enable_mipmapping=*/true); - GenerateMipmap(context, texture, "table_mountain_nx", async_submit); +static void CanRenderSweepGradientWithDithering(AiksTest* aiks_test, + bool use_dithering) { Canvas canvas; canvas.Scale(aiks_test->GetContentScale()); - canvas.Translate({100.0f, 100.0f, 0}); Paint paint; - paint.color_source = - ColorSource::MakeImage(texture, tile_mode, tile_mode, {}, local_matrix); - paint.color = Color(1, 1, 1, 1); - canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); - - // Should not change the image. - constexpr auto stroke_width = 64; - paint.style = Paint::Style::kStroke; - paint.stroke_width = stroke_width; - if (tile_mode == Entity::TileMode::kDecal) { - canvas.DrawRect(Rect::MakeXYWH(stroke_width, stroke_width, 600, 600), - paint); - } else { - canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); - } + canvas.Translate({100.0, 100.0, 0}); - { - // Should not change the image. - PathBuilder path_builder; - path_builder.AddCircle({150, 150}, 150); - path_builder.AddRoundedRect(Rect::MakeLTRB(300, 300, 600, 600), 10); - paint.style = Paint::Style::kFill; - canvas.DrawPath(path_builder.TakePath(), paint); - } + // #FFF -> #000 + std::vector colors = {Color{1.0, 1.0, 1.0, 1.0}, + Color{0.0, 0.0, 0.0, 1.0}}; + std::vector stops = {0.0, 1.0}; - { - // Should not change the image. Tests the Convex short-cut code. - PathBuilder path_builder; - path_builder.AddCircle({150, 450}, 150); - path_builder.SetConvexity(Convexity::kConvex); - paint.style = Paint::Style::kFill; - canvas.DrawPath(path_builder.TakePath(), paint); - } + paint.color_source = ColorSource::MakeSweepGradient( + {100, 100}, Degrees(45), Degrees(135), std::move(colors), + std::move(stops), Entity::TileMode::kMirror, {}); + paint.dither = use_dithering; + canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -} // namespace -TEST_P(AiksTest, CanRenderTiledTextureClamp) { - CanRenderTiledTexture(this, Entity::TileMode::kClamp); +TEST_P(AiksTest, CanRenderSweepGradientWithDitheringDisabled) { + CanRenderSweepGradientWithDithering(this, false); } -TEST_P(AiksTest, CanRenderTiledTextureClampAsync) { - CanRenderTiledTexture(this, Entity::TileMode::kClamp, /*async_submit=*/true); +TEST_P(AiksTest, CanRenderSweepGradientWithDitheringEnabled) { + CanRenderSweepGradientWithDithering(this, true); } -TEST_P(AiksTest, CanRenderTiledTextureRepeat) { - CanRenderTiledTexture(this, Entity::TileMode::kRepeat); -} +static void CanRenderConicalGradientWithDithering(AiksTest* aiks_test, + bool use_dithering) { + Canvas canvas; + canvas.Scale(aiks_test->GetContentScale()); + Paint paint; + canvas.Translate({100.0, 100.0, 0}); -TEST_P(AiksTest, CanRenderTiledTextureMirror) { - CanRenderTiledTexture(this, Entity::TileMode::kMirror); -} + // #FFF -> #000 + std::vector colors = {Color{1.0, 1.0, 1.0, 1.0}, + Color{0.0, 0.0, 0.0, 1.0}}; + std::vector stops = {0.0, 1.0}; -TEST_P(AiksTest, CanRenderTiledTextureDecal) { - CanRenderTiledTexture(this, Entity::TileMode::kDecal); -} + paint.color_source = ColorSource::MakeConicalGradient( + {100, 100}, 100, std::move(colors), std::move(stops), {0, 1}, 0, + Entity::TileMode::kMirror, {}); + paint.dither = use_dithering; -TEST_P(AiksTest, CanRenderTiledTextureClampWithTranslate) { - CanRenderTiledTexture(this, Entity::TileMode::kClamp, /*async_submit=*/false, - Matrix::MakeTranslation({172.f, 172.f, 0.f})); + canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); + ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderImageRect) { - Canvas canvas; - Paint paint; - auto image = std::make_shared(CreateTextureForFixture("kalimba.jpg")); - Size image_half_size = Size(image->GetSize()) * 0.5; - - // Render the bottom right quarter of the source image in a stretched rect. - auto source_rect = Rect::MakeSize(image_half_size); - source_rect = source_rect.Shift(Point(image_half_size)); - - canvas.DrawImageRect(image, source_rect, Rect::MakeXYWH(100, 100, 600, 600), - paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +TEST_P(AiksTest, CanRenderConicalGradientWithDitheringDisabled) { + CanRenderConicalGradientWithDithering(this, false); } -TEST_P(AiksTest, CanRenderStrokes) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.stroke_width = 20.0; - paint.style = Paint::Style::kStroke; - canvas.DrawPath(PathBuilder{}.AddLine({200, 100}, {800, 100}).TakePath(), - paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +TEST_P(AiksTest, CanRenderConicalGradientWithDitheringEnabled) { + CanRenderConicalGradientWithDithering(this, true); } -TEST_P(AiksTest, CanRenderCurvedStrokes) { +namespace { +void CanRenderLinearGradientWithOverlappingStops(AiksTest* aiks_test, + Entity::TileMode tile_mode) { Canvas canvas; Paint paint; - paint.color = Color::Red(); - paint.stroke_width = 25.0; - paint.style = Paint::Style::kStroke; - canvas.DrawPath(PathBuilder{}.AddCircle({500, 500}, 250).TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} + canvas.Translate({100.0, 100.0, 0}); -TEST_P(AiksTest, CanRenderThickCurvedStrokes) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.stroke_width = 100.0; - paint.style = Paint::Style::kStroke; - canvas.DrawPath(PathBuilder{}.AddCircle({100, 100}, 50).TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); + std::vector colors = { + Color{0.9568, 0.2627, 0.2118, 1.0}, Color{0.9568, 0.2627, 0.2118, 1.0}, + Color{0.1294, 0.5882, 0.9529, 1.0}, Color{0.1294, 0.5882, 0.9529, 1.0}}; + std::vector stops = {0.0, 0.5, 0.5, 1.0}; + + paint.color_source = ColorSource::MakeLinearGradient( + {0, 0}, {500, 500}, std::move(colors), std::move(stops), tile_mode, {}); + + paint.color = Color(1.0, 1.0, 1.0, 1.0); + canvas.DrawRect(Rect::MakeXYWH(0, 0, 500, 500), paint); + ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +} // namespace -TEST_P(AiksTest, CanRenderClips) { - Canvas canvas; - Paint paint; - paint.color = Color::Fuchsia(); - canvas.ClipPath( - PathBuilder{}.AddRect(Rect::MakeXYWH(0, 0, 500, 500)).TakePath()); - canvas.DrawPath(PathBuilder{}.AddCircle({500, 500}, 250).TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +// Only clamp is necessary. All tile modes are the same output. +TEST_P(AiksTest, CanRenderLinearGradientWithOverlappingStopsClamp) { + CanRenderLinearGradientWithOverlappingStops(this, Entity::TileMode::kClamp); } -TEST_P(AiksTest, CanRenderSimpleClips) { +namespace { +void CanRenderLinearGradientManyColors(AiksTest* aiks_test, + Entity::TileMode tile_mode) { Canvas canvas; - canvas.Scale(GetContentScale()); + canvas.Scale(aiks_test->GetContentScale()); Paint paint; + canvas.Translate({100, 100, 0}); - paint.color = Color::White(); - canvas.DrawPaint(paint); - - auto draw = [&canvas](const Paint& paint, Scalar x, Scalar y) { - canvas.Save(); - canvas.Translate({x, y}); - { - canvas.Save(); - canvas.ClipRect(Rect::MakeLTRB(50, 50, 150, 150)); - canvas.DrawPaint(paint); - canvas.Restore(); - } - { - canvas.Save(); - canvas.ClipOval(Rect::MakeLTRB(200, 50, 300, 150)); - canvas.DrawPaint(paint); - canvas.Restore(); - } - { - canvas.Save(); - canvas.ClipRRect(Rect::MakeLTRB(50, 200, 150, 300), {20, 20}); - canvas.DrawPaint(paint); - canvas.Restore(); - } - canvas.Restore(); - }; - - paint.color = Color::Blue(); - draw(paint, 0, 0); - - std::vector gradient_colors = { + std::vector colors = { Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0}, Color{0x5b / 255.0, 0.0, 0x60 / 255.0, 1.0}, Color{0x87 / 255.0, 0x01 / 255.0, 0x60 / 255.0, 1.0}, @@ -371,1335 +281,1674 @@ TEST_P(AiksTest, CanRenderSimpleClips) { (1.0 / 6.0) * 5, 1.0, }; - auto texture = CreateTextureForFixture("airplane.jpg", - /*enable_mipmapping=*/true); - paint.color_source = ColorSource::MakeRadialGradient( - {500, 600}, 75, std::move(gradient_colors), std::move(stops), - Entity::TileMode::kMirror, {}); - draw(paint, 0, 300); - - paint.color_source = ColorSource::MakeImage( - texture, Entity::TileMode::kRepeat, Entity::TileMode::kRepeat, {}, - Matrix::MakeTranslation({0, 0})); - draw(paint, 300, 0); + paint.color_source = ColorSource::MakeLinearGradient( + {0, 0}, {200, 200}, std::move(colors), std::move(stops), tile_mode, {}); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); + paint.color = Color(1.0, 1.0, 1.0, 1.0); + canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); + canvas.Restore(); + ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +} // namespace -TEST_P(AiksTest, CanRenderNestedClips) { - Canvas canvas; - Paint paint; - paint.color = Color::Fuchsia(); - canvas.Save(); - canvas.ClipPath(PathBuilder{}.AddCircle({200, 400}, 300).TakePath()); - canvas.Restore(); - canvas.ClipPath(PathBuilder{}.AddCircle({600, 400}, 300).TakePath()); - canvas.ClipPath(PathBuilder{}.AddCircle({400, 600}, 300).TakePath()); - canvas.DrawRect(Rect::MakeXYWH(200, 200, 400, 400), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +TEST_P(AiksTest, CanRenderLinearGradientManyColorsClamp) { + CanRenderLinearGradientManyColors(this, Entity::TileMode::kClamp); +} +TEST_P(AiksTest, CanRenderLinearGradientManyColorsRepeat) { + CanRenderLinearGradientManyColors(this, Entity::TileMode::kRepeat); +} +TEST_P(AiksTest, CanRenderLinearGradientManyColorsMirror) { + CanRenderLinearGradientManyColors(this, Entity::TileMode::kMirror); +} +TEST_P(AiksTest, CanRenderLinearGradientManyColorsDecal) { + CanRenderLinearGradientManyColors(this, Entity::TileMode::kDecal); } -TEST_P(AiksTest, CanRenderDifferenceClips) { - Paint paint; +namespace { +void CanRenderLinearGradientWayManyColors(AiksTest* aiks_test, + Entity::TileMode tile_mode) { Canvas canvas; - canvas.Translate({400, 400}); - - // Limit drawing to face circle with a clip. - canvas.ClipPath(PathBuilder{}.AddCircle(Point(), 200).TakePath()); - canvas.Save(); - - // Cut away eyes/mouth using difference clips. - canvas.ClipPath(PathBuilder{}.AddCircle({-100, -50}, 30).TakePath(), - Entity::ClipOperation::kDifference); - canvas.ClipPath(PathBuilder{}.AddCircle({100, -50}, 30).TakePath(), - Entity::ClipOperation::kDifference); - canvas.ClipPath(PathBuilder{} - .AddQuadraticCurve({-100, 50}, {0, 150}, {100, 50}) - .TakePath(), - Entity::ClipOperation::kDifference); + Paint paint; + canvas.Translate({100.0, 100.0, 0}); + auto color = Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0}; + std::vector colors; + std::vector stops; + auto current_stop = 0.0; + for (int i = 0; i < 2000; i++) { + colors.push_back(color); + stops.push_back(current_stop); + current_stop += 1 / 2000.0; + } + stops[2000 - 1] = 1.0; - // Draw a huge yellow rectangle to prove the clipping works. - paint.color = Color::Yellow(); - canvas.DrawRect(Rect::MakeXYWH(-1000, -1000, 2000, 2000), paint); + paint.color_source = ColorSource::MakeLinearGradient( + {0, 0}, {200, 200}, std::move(colors), std::move(stops), tile_mode, {}); - // Remove the difference clips and draw hair that partially covers the eyes. - canvas.Restore(); - paint.color = Color::Maroon(); - canvas.DrawPath(PathBuilder{} - .MoveTo({200, -200}) - .HorizontalLineTo(-200) - .VerticalLineTo(-40) - .CubicCurveTo({0, -40}, {0, -80}, {200, -80}) - .TakePath(), - paint); + canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); + ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} +} // namespace - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +// Only test clamp on purpose since they all look the same. +TEST_P(AiksTest, CanRenderLinearGradientWayManyColorsClamp) { + CanRenderLinearGradientWayManyColors(this, Entity::TileMode::kClamp); } -TEST_P(AiksTest, CanRenderWithContiguousClipRestores) { - Canvas canvas; +TEST_P(AiksTest, CanRenderLinearGradientManyColorsUnevenStops) { + auto callback = [&](AiksContext& renderer) -> std::optional { + const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"}; + const Entity::TileMode tile_modes[] = { + Entity::TileMode::kClamp, Entity::TileMode::kRepeat, + Entity::TileMode::kMirror, Entity::TileMode::kDecal}; - // Cover the whole canvas with red. - canvas.DrawPaint({.color = Color::Red()}); + static int selected_tile_mode = 0; + ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Combo("Tile mode", &selected_tile_mode, tile_mode_names, + sizeof(tile_mode_names) / sizeof(char*)); + static Matrix matrix = { + 1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 1, 0, // + 0, 0, 0, 1 // + }; + std::string label = "##1"; + for (int i = 0; i < 4; i++) { + ImGui::InputScalarN(label.c_str(), ImGuiDataType_Float, &(matrix.vec[i]), + 4, NULL, NULL, "%.2f", 0); + label[2]++; + } + ImGui::End(); - canvas.Save(); + Canvas canvas; + Paint paint; + canvas.Translate({100.0, 100.0, 0}); + auto tile_mode = tile_modes[selected_tile_mode]; - // Append two clips, the second resulting in empty coverage. - canvas.ClipPath( - PathBuilder{}.AddRect(Rect::MakeXYWH(100, 100, 100, 100)).TakePath()); - canvas.ClipPath( - PathBuilder{}.AddRect(Rect::MakeXYWH(300, 300, 100, 100)).TakePath()); - - // Restore to no clips. - canvas.Restore(); + std::vector colors = { + Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0}, + Color{0x5b / 255.0, 0.0, 0x60 / 255.0, 1.0}, + Color{0x87 / 255.0, 0x01 / 255.0, 0x60 / 255.0, 1.0}, + Color{0xac / 255.0, 0x25 / 255.0, 0x53 / 255.0, 1.0}, + Color{0xe1 / 255.0, 0x6b / 255.0, 0x5c / 255.0, 1.0}, + Color{0xf3 / 255.0, 0x90 / 255.0, 0x60 / 255.0, 1.0}, + Color{0xff / 255.0, 0xb5 / 255.0, 0x6b / 250.0, 1.0}}; + std::vector stops = { + 0.0, 2.0 / 62.0, 4.0 / 62.0, 8.0 / 62.0, 16.0 / 62.0, 32.0 / 62.0, 1.0, + }; - // Replace the whole canvas with green. - canvas.DrawPaint({.color = Color::Green()}); + paint.color_source = ColorSource::MakeLinearGradient( + {0, 0}, {200, 200}, std::move(colors), std::move(stops), tile_mode, {}); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); + canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); + return canvas.EndRecordingAsPicture(); + }; + ASSERT_TRUE(OpenPlaygroundHere(callback)); } -TEST_P(AiksTest, ClipsUseCurrentTransform) { - std::array colors = {Color::White(), Color::Black(), - Color::SkyBlue(), Color::Red(), - Color::Yellow()}; +TEST_P(AiksTest, CanRenderLinearGradientMaskBlur) { Canvas canvas; - Paint paint; - canvas.Translate(Vector3(300, 300)); - for (int i = 0; i < 15; i++) { - canvas.Scale(Vector3(0.8, 0.8)); + Paint paint = { + .color = Color::White(), + .color_source = ColorSource::MakeLinearGradient( + {200, 200}, {400, 400}, + {Color::Red(), Color::White(), Color::Red(), Color::White(), + Color::Red(), Color::White(), Color::Red(), Color::White(), + Color::Red(), Color::White(), Color::Red()}, + {0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}, + Entity::TileMode::kClamp, {}), + .mask_blur_descriptor = + Paint::MaskBlurDescriptor{ + .style = FilterContents::BlurStyle::kNormal, + .sigma = Sigma(20), + }, + }; + + canvas.DrawCircle({300, 300}, 200, paint); + canvas.DrawRect(Rect::MakeLTRB(100, 300, 500, 600), paint); - paint.color = colors[i % colors.size()]; - canvas.ClipPath(PathBuilder{}.AddCircle({0, 0}, 300).TakePath()); - canvas.DrawRect(Rect::MakeXYWH(-300, -300, 600, 600), paint); - } ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanSaveLayerStandalone) { - Canvas canvas; +TEST_P(AiksTest, CanRenderRadialGradient) { + auto callback = [&](AiksContext& renderer) -> std::optional { + const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"}; + const Entity::TileMode tile_modes[] = { + Entity::TileMode::kClamp, Entity::TileMode::kRepeat, + Entity::TileMode::kMirror, Entity::TileMode::kDecal}; - Paint red; - red.color = Color::Red(); + static int selected_tile_mode = 0; + ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Combo("Tile mode", &selected_tile_mode, tile_mode_names, + sizeof(tile_mode_names) / sizeof(char*)); + static Matrix matrix = { + 1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 1, 0, // + 0, 0, 0, 1 // + }; + std::string label = "##1"; + for (int i = 0; i < 4; i++) { + ImGui::InputScalarN(label.c_str(), ImGuiDataType_Float, &(matrix.vec[i]), + 4, NULL, NULL, "%.2f", 0); + label[2]++; + } + ImGui::End(); - Paint alpha; - alpha.color = Color::Red().WithAlpha(0.5); + Canvas canvas; + Paint paint; + canvas.Translate({100.0, 100.0, 0}); + auto tile_mode = tile_modes[selected_tile_mode]; - canvas.SaveLayer(alpha); + std::vector colors = {Color{0.9568, 0.2627, 0.2118, 1.0}, + Color{0.1294, 0.5882, 0.9529, 1.0}}; + std::vector stops = {0.0, 1.0}; - canvas.DrawCircle({125, 125}, 125, red); + paint.color_source = ColorSource::MakeRadialGradient( + {100, 100}, 100, std::move(colors), std::move(stops), tile_mode, {}); - canvas.Restore(); + canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); + return canvas.EndRecordingAsPicture(); + }; + ASSERT_TRUE(OpenPlaygroundHere(callback)); +} - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +TEST_P(AiksTest, CanRenderRadialGradientManyColors) { + auto callback = [&](AiksContext& renderer) -> std::optional { + const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"}; + const Entity::TileMode tile_modes[] = { + Entity::TileMode::kClamp, Entity::TileMode::kRepeat, + Entity::TileMode::kMirror, Entity::TileMode::kDecal}; + + static int selected_tile_mode = 0; + ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Combo("Tile mode", &selected_tile_mode, tile_mode_names, + sizeof(tile_mode_names) / sizeof(char*)); + static Matrix matrix = { + 1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 1, 0, // + 0, 0, 0, 1 // + }; + std::string label = "##1"; + for (int i = 0; i < 4; i++) { + ImGui::InputScalarN(label.c_str(), ImGuiDataType_Float, &(matrix.vec[i]), + 4, NULL, NULL, "%.2f", 0); + label[2]++; + } + ImGui::End(); + + Canvas canvas; + Paint paint; + canvas.Translate({100.0, 100.0, 0}); + auto tile_mode = tile_modes[selected_tile_mode]; + + std::vector colors = { + Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0}, + Color{0x5b / 255.0, 0.0, 0x60 / 255.0, 1.0}, + Color{0x87 / 255.0, 0x01 / 255.0, 0x60 / 255.0, 1.0}, + Color{0xac / 255.0, 0x25 / 255.0, 0x53 / 255.0, 1.0}, + Color{0xe1 / 255.0, 0x6b / 255.0, 0x5c / 255.0, 1.0}, + Color{0xf3 / 255.0, 0x90 / 255.0, 0x60 / 255.0, 1.0}, + Color{0xff / 255.0, 0xb5 / 255.0, 0x6b / 250.0, 1.0}}; + std::vector stops = { + 0.0, + (1.0 / 6.0) * 1, + (1.0 / 6.0) * 2, + (1.0 / 6.0) * 3, + (1.0 / 6.0) * 4, + (1.0 / 6.0) * 5, + 1.0, + }; + + paint.color_source = ColorSource::MakeRadialGradient( + {100, 100}, 100, std::move(colors), std::move(stops), tile_mode, {}); + + canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); + return canvas.EndRecordingAsPicture(); + }; + ASSERT_TRUE(OpenPlaygroundHere(callback)); } namespace { -void CanRenderLinearGradient(AiksTest* aiks_test, Entity::TileMode tile_mode) { +void CanRenderSweepGradient(AiksTest* aiks_test, Entity::TileMode tile_mode) { Canvas canvas; canvas.Scale(aiks_test->GetContentScale()); Paint paint; - canvas.Translate({100.0f, 0, 0}); + canvas.Translate({100, 100, 0}); std::vector colors = {Color{0.9568, 0.2627, 0.2118, 1.0}, - Color{0.1294, 0.5882, 0.9529, 0.0}}; + Color{0.1294, 0.5882, 0.9529, 1.0}}; std::vector stops = {0.0, 1.0}; - paint.color_source = ColorSource::MakeLinearGradient( - {0, 0}, {200, 200}, std::move(colors), std::move(stops), tile_mode, {}); + paint.color_source = ColorSource::MakeSweepGradient( + {100, 100}, Degrees(45), Degrees(135), std::move(colors), + std::move(stops), tile_mode, {}); - paint.color = Color(1.0, 1.0, 1.0, 1.0); canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } } // namespace -TEST_P(AiksTest, CanRenderLinearGradientClamp) { - CanRenderLinearGradient(this, Entity::TileMode::kClamp); +TEST_P(AiksTest, CanRenderSweepGradientClamp) { + CanRenderSweepGradient(this, Entity::TileMode::kClamp); } -TEST_P(AiksTest, CanRenderLinearGradientRepeat) { - CanRenderLinearGradient(this, Entity::TileMode::kRepeat); +TEST_P(AiksTest, CanRenderSweepGradientRepeat) { + CanRenderSweepGradient(this, Entity::TileMode::kRepeat); } -TEST_P(AiksTest, CanRenderLinearGradientMirror) { - CanRenderLinearGradient(this, Entity::TileMode::kMirror); +TEST_P(AiksTest, CanRenderSweepGradientMirror) { + CanRenderSweepGradient(this, Entity::TileMode::kMirror); } -TEST_P(AiksTest, CanRenderLinearGradientDecal) { - CanRenderLinearGradient(this, Entity::TileMode::kDecal); +TEST_P(AiksTest, CanRenderSweepGradientDecal) { + CanRenderSweepGradient(this, Entity::TileMode::kDecal); } -TEST_P(AiksTest, CanRenderLinearGradientDecalWithColorFilter) { +namespace { +void CanRenderSweepGradientManyColors(AiksTest* aiks_test, + Entity::TileMode tile_mode) { Canvas canvas; - canvas.Scale(GetContentScale()); Paint paint; - canvas.Translate({100.0f, 0, 0}); + canvas.Translate({100.0, 100.0, 0}); - std::vector colors = {Color{0.9568, 0.2627, 0.2118, 1.0}, - Color{0.1294, 0.5882, 0.9529, 0.0}}; - std::vector stops = {0.0, 1.0}; + std::vector colors = { + Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0}, + Color{0x5b / 255.0, 0.0, 0x60 / 255.0, 1.0}, + Color{0x87 / 255.0, 0x01 / 255.0, 0x60 / 255.0, 1.0}, + Color{0xac / 255.0, 0x25 / 255.0, 0x53 / 255.0, 1.0}, + Color{0xe1 / 255.0, 0x6b / 255.0, 0x5c / 255.0, 1.0}, + Color{0xf3 / 255.0, 0x90 / 255.0, 0x60 / 255.0, 1.0}, + Color{0xff / 255.0, 0xb5 / 255.0, 0x6b / 250.0, 1.0}}; + std::vector stops = { + 0.0, + (1.0 / 6.0) * 1, + (1.0 / 6.0) * 2, + (1.0 / 6.0) * 3, + (1.0 / 6.0) * 4, + (1.0 / 6.0) * 5, + 1.0, + }; - paint.color_source = ColorSource::MakeLinearGradient( - {0, 0}, {200, 200}, std::move(colors), std::move(stops), - Entity::TileMode::kDecal, {}); - // Overlay the gradient with 25% green. This should appear as the entire - // rectangle being drawn with 25% green, including the border area outside the - // decal gradient. - paint.color_filter = ColorFilter::MakeBlend(BlendMode::kSourceOver, - Color::Green().WithAlpha(0.25)); + paint.color_source = ColorSource::MakeSweepGradient( + {100, 100}, Degrees(45), Degrees(135), std::move(colors), + std::move(stops), tile_mode, {}); - paint.color = Color(1.0, 1.0, 1.0, 1.0); canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -static void CanRenderLinearGradientWithDithering(AiksTest* aiks_test, - bool use_dithering) { - Canvas canvas; - Paint paint; - canvas.Translate({100.0, 100.0, 0}); - - // 0xffcccccc --> 0xff333333, taken from - // https://github.com/flutter/flutter/issues/118073#issue-1521699748 - std::vector colors = {Color{0.8, 0.8, 0.8, 1.0}, - Color{0.2, 0.2, 0.2, 1.0}}; - std::vector stops = {0.0, 1.0}; - - paint.color_source = ColorSource::MakeLinearGradient( - {0, 0}, {800, 500}, std::move(colors), std::move(stops), - Entity::TileMode::kClamp, {}); - paint.dither = use_dithering; - canvas.DrawRect(Rect::MakeXYWH(0, 0, 800, 500), paint); ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } - -TEST_P(AiksTest, CanRenderLinearGradientWithDitheringDisabled) { - CanRenderLinearGradientWithDithering(this, false); -} - -TEST_P(AiksTest, CanRenderLinearGradientWithDitheringEnabled) { - CanRenderLinearGradientWithDithering(this, true); } // namespace -static void CanRenderRadialGradientWithDithering(AiksTest* aiks_test, - bool use_dithering) { - Canvas canvas; +TEST_P(AiksTest, CanRenderSweepGradientManyColorsClamp) { + CanRenderSweepGradientManyColors(this, Entity::TileMode::kClamp); +} +TEST_P(AiksTest, CanRenderSweepGradientManyColorsRepeat) { + CanRenderSweepGradientManyColors(this, Entity::TileMode::kRepeat); +} +TEST_P(AiksTest, CanRenderSweepGradientManyColorsMirror) { + CanRenderSweepGradientManyColors(this, Entity::TileMode::kMirror); +} +TEST_P(AiksTest, CanRenderSweepGradientManyColorsDecal) { + CanRenderSweepGradientManyColors(this, Entity::TileMode::kDecal); +} + +TEST_P(AiksTest, CanRenderConicalGradient) { + Scalar size = 256; + Canvas canvas; Paint paint; - canvas.Translate({100.0, 100.0, 0}); + paint.color = Color::White(); + canvas.DrawRect(Rect::MakeXYWH(0, 0, size * 3, size * 3), paint); + std::vector colors = {Color::MakeRGBA8(0xF4, 0x43, 0x36, 0xFF), + Color::MakeRGBA8(0xFF, 0xEB, 0x3B, 0xFF), + Color::MakeRGBA8(0x4c, 0xAF, 0x50, 0xFF), + Color::MakeRGBA8(0x21, 0x96, 0xF3, 0xFF)}; + std::vector stops = {0.0, 1.f / 3.f, 2.f / 3.f, 1.0}; + std::array, 8> array{ + std::make_tuple(Point{size / 2.f, size / 2.f}, 0.f, + Point{size / 2.f, size / 2.f}, size / 2.f), + std::make_tuple(Point{size / 2.f, size / 2.f}, size / 4.f, + Point{size / 2.f, size / 2.f}, size / 2.f), + std::make_tuple(Point{size / 4.f, size / 4.f}, 0.f, + Point{size / 2.f, size / 2.f}, size / 2.f), + std::make_tuple(Point{size / 4.f, size / 4.f}, size / 2.f, + Point{size / 2.f, size / 2.f}, 0), + std::make_tuple(Point{size / 4.f, size / 4.f}, size / 4.f, + Point{size / 2.f, size / 2.f}, size / 2.f), + std::make_tuple(Point{size / 4.f, size / 4.f}, size / 16.f, + Point{size / 2.f, size / 2.f}, size / 8.f), + std::make_tuple(Point{size / 4.f, size / 4.f}, size / 8.f, + Point{size / 2.f, size / 2.f}, size / 16.f), + std::make_tuple(Point{size / 8.f, size / 8.f}, size / 8.f, + Point{size / 2.f, size / 2.f}, size / 8.f), + }; + for (int i = 0; i < 8; i++) { + canvas.Save(); + canvas.Translate({(i % 3) * size, i / 3 * size, 0}); + paint.color_source = ColorSource::MakeConicalGradient( + std::get<0>(array[i]), std::get<1>(array[i]), colors, stops, + std::get<2>(array[i]), std::get<3>(array[i]), Entity::TileMode::kClamp, + {}); + canvas.DrawRect(Rect::MakeXYWH(0, 0, size, size), paint); + canvas.Restore(); + } + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} - // #FFF -> #000 - std::vector colors = {Color{1.0, 1.0, 1.0, 1.0}, - Color{0.0, 0.0, 0.0, 1.0}}; - std::vector stops = {0.0, 1.0}; +TEST_P(AiksTest, CanRenderGradientDecalWithBackground) { + std::vector colors = {Color::MakeRGBA8(0xF4, 0x43, 0x36, 0xFF), + Color::MakeRGBA8(0xFF, 0xEB, 0x3B, 0xFF), + Color::MakeRGBA8(0x4c, 0xAF, 0x50, 0xFF), + Color::MakeRGBA8(0x21, 0x96, 0xF3, 0xFF)}; + std::vector stops = {0.0, 1.f / 3.f, 2.f / 3.f, 1.0}; - paint.color_source = ColorSource::MakeRadialGradient( - {600, 600}, 600, std::move(colors), std::move(stops), - Entity::TileMode::kClamp, {}); - paint.dither = use_dithering; - canvas.DrawRect(Rect::MakeXYWH(0, 0, 1200, 1200), paint); - ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); + std::array color_sources = { + ColorSource::MakeLinearGradient({0, 0}, {100, 100}, colors, stops, + Entity::TileMode::kDecal, {}), + ColorSource::MakeRadialGradient({100, 100}, 100, colors, stops, + Entity::TileMode::kDecal, {}), + ColorSource::MakeSweepGradient({100, 100}, Degrees(45), Degrees(135), + colors, stops, Entity::TileMode::kDecal, + {}), + }; + + Canvas canvas; + Paint paint; + paint.color = Color::White(); + canvas.DrawRect(Rect::MakeLTRB(0, 0, 605, 205), paint); + for (int i = 0; i < 3; i++) { + canvas.Save(); + canvas.Translate({i * 200.0f, 0, 0}); + paint.color_source = color_sources[i]; + canvas.DrawRect(Rect::MakeLTRB(0, 0, 200, 200), paint); + canvas.Restore(); + } + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderRadialGradientWithDitheringDisabled) { - CanRenderRadialGradientWithDithering(this, false); +TEST_P(AiksTest, GradientStrokesRenderCorrectly) { + // Compare with https://fiddle.skia.org/c/027392122bec8ac2b5d5de00a4b9bbe2 + auto callback = [&](AiksContext& renderer) -> std::optional { + static float scale = 3; + static bool add_circle_clip = true; + const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"}; + const Entity::TileMode tile_modes[] = { + Entity::TileMode::kClamp, Entity::TileMode::kRepeat, + Entity::TileMode::kMirror, Entity::TileMode::kDecal}; + static int selected_tile_mode = 0; + static float alpha = 1; + + ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::SliderFloat("Scale", &scale, 0, 6); + ImGui::Checkbox("Circle clip", &add_circle_clip); + ImGui::SliderFloat("Alpha", &alpha, 0, 1); + ImGui::Combo("Tile mode", &selected_tile_mode, tile_mode_names, + sizeof(tile_mode_names) / sizeof(char*)); + ImGui::End(); + + Canvas canvas; + canvas.Scale(GetContentScale()); + Paint paint; + paint.color = Color::White(); + canvas.DrawPaint(paint); + + paint.style = Paint::Style::kStroke; + paint.color = Color(1.0, 1.0, 1.0, alpha); + paint.stroke_width = 10; + auto tile_mode = tile_modes[selected_tile_mode]; + + std::vector colors = {Color{0.9568, 0.2627, 0.2118, 1.0}, + Color{0.1294, 0.5882, 0.9529, 1.0}}; + std::vector stops = {0.0, 1.0}; + + paint.color_source = ColorSource::MakeLinearGradient( + {0, 0}, {50, 50}, std::move(colors), std::move(stops), tile_mode, {}); + + Path path = PathBuilder{} + .MoveTo({20, 20}) + .QuadraticCurveTo({60, 20}, {60, 60}) + .Close() + .MoveTo({60, 20}) + .QuadraticCurveTo({60, 60}, {20, 60}) + .TakePath(); + + canvas.Scale(Vector2(scale, scale)); + + if (add_circle_clip) { + auto [handle_a, handle_b] = IMPELLER_PLAYGROUND_LINE( + Point(60, 300), Point(600, 300), 20, Color::Red(), Color::Red()); + + auto screen_to_canvas = canvas.GetCurrentTransform().Invert(); + Point point_a = screen_to_canvas * handle_a * GetContentScale(); + Point point_b = screen_to_canvas * handle_b * GetContentScale(); + + Point middle = (point_a + point_b) / 2; + auto radius = point_a.GetDistance(middle); + canvas.ClipPath(PathBuilder{}.AddCircle(middle, radius).TakePath()); + } + + for (auto join : {Join::kBevel, Join::kRound, Join::kMiter}) { + paint.stroke_join = join; + for (auto cap : {Cap::kButt, Cap::kSquare, Cap::kRound}) { + paint.stroke_cap = cap; + canvas.DrawPath(path.Clone(), paint); + canvas.Translate({80, 0}); + } + canvas.Translate({-240, 60}); + } + + return canvas.EndRecordingAsPicture(); + }; + + ASSERT_TRUE(OpenPlaygroundHere(callback)); } -TEST_P(AiksTest, CanRenderRadialGradientWithDitheringEnabled) { - CanRenderRadialGradientWithDithering(this, true); +#define APPLY_COLOR_FILTER_GRADIENT_TEST(name) \ + TEST_P(AiksTest, name##GradientApplyColorFilter) { \ + auto contents = name##GradientContents(); \ + contents.SetColors({Color::CornflowerBlue().WithAlpha(0.75)}); \ + auto result = contents.ApplyColorFilter([](const Color& color) { \ + return color.Blend(Color::LimeGreen().WithAlpha(0.75), \ + BlendMode::kScreen); \ + }); \ + ASSERT_TRUE(result); \ + \ + std::vector expected = {Color(0.433247, 0.879523, 0.825324, 0.75)}; \ + ASSERT_COLORS_NEAR(contents.GetColors(), expected); \ + } + +APPLY_COLOR_FILTER_GRADIENT_TEST(Linear); +APPLY_COLOR_FILTER_GRADIENT_TEST(Radial); +APPLY_COLOR_FILTER_GRADIENT_TEST(Conical); +APPLY_COLOR_FILTER_GRADIENT_TEST(Sweep); + +} // namespace gradients + +/// These tests check the visual results of drawing images. +namespace images { +namespace { + +bool GenerateMipmap(const std::shared_ptr& context, + std::shared_ptr texture, + std::string label, + bool async_submit) { + auto buffer = context->CreateCommandBuffer(); + if (!buffer) { + return false; + } + auto pass = buffer->CreateBlitPass(); + if (!pass) { + return false; + } + pass->GenerateMipmap(std::move(texture), std::move(label)); + if (async_submit) { + return buffer->EncodeAndSubmit(pass, context->GetResourceAllocator()); + } + + pass->EncodeCommands(context->GetResourceAllocator()); + return buffer->SubmitCommands(); } -static void CanRenderSweepGradientWithDithering(AiksTest* aiks_test, - bool use_dithering) { +void CanRenderTiledTexture(AiksTest* aiks_test, + Entity::TileMode tile_mode, + bool async_submit = false, + Matrix local_matrix = {}) { + auto context = aiks_test->GetContext(); + ASSERT_TRUE(context); + auto texture = aiks_test->CreateTextureForFixture("table_mountain_nx.png", + /*enable_mipmapping=*/true); + GenerateMipmap(context, texture, "table_mountain_nx", async_submit); Canvas canvas; canvas.Scale(aiks_test->GetContentScale()); + canvas.Translate({100.0f, 100.0f, 0}); Paint paint; - canvas.Translate({100.0, 100.0, 0}); + paint.color_source = + ColorSource::MakeImage(texture, tile_mode, tile_mode, {}, local_matrix); + paint.color = Color(1, 1, 1, 1); + canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); - // #FFF -> #000 - std::vector colors = {Color{1.0, 1.0, 1.0, 1.0}, - Color{0.0, 0.0, 0.0, 1.0}}; - std::vector stops = {0.0, 1.0}; + // Should not change the image. + constexpr auto stroke_width = 64; + paint.style = Paint::Style::kStroke; + paint.stroke_width = stroke_width; + if (tile_mode == Entity::TileMode::kDecal) { + canvas.DrawRect(Rect::MakeXYWH(stroke_width, stroke_width, 600, 600), + paint); + } else { + canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); + } - paint.color_source = ColorSource::MakeSweepGradient( - {100, 100}, Degrees(45), Degrees(135), std::move(colors), - std::move(stops), Entity::TileMode::kMirror, {}); - paint.dither = use_dithering; + { + // Should not change the image. + PathBuilder path_builder; + path_builder.AddCircle({150, 150}, 150); + path_builder.AddRoundedRect(Rect::MakeLTRB(300, 300, 600, 600), 10); + paint.style = Paint::Style::kFill; + canvas.DrawPath(path_builder.TakePath(), paint); + } + + { + // Should not change the image. Tests the Convex short-cut code. + PathBuilder path_builder; + path_builder.AddCircle({150, 450}, 150); + path_builder.SetConvexity(Convexity::kConvex); + paint.style = Paint::Style::kFill; + canvas.DrawPath(path_builder.TakePath(), paint); + } - canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +} // namespace -TEST_P(AiksTest, CanRenderSweepGradientWithDitheringDisabled) { - CanRenderSweepGradientWithDithering(this, false); +TEST_P(AiksTest, CanRenderImage) { + Canvas canvas; + Paint paint; + auto image = std::make_shared(CreateTextureForFixture("kalimba.jpg")); + paint.color = Color::Red(); + canvas.DrawImage(image, Point::MakeXY(100.0, 100.0), paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderSweepGradientWithDitheringEnabled) { - CanRenderSweepGradientWithDithering(this, true); +TEST_P(AiksTest, CanRenderInvertedImageWithColorFilter) { + Canvas canvas; + Paint paint; + auto image = std::make_shared(CreateTextureForFixture("kalimba.jpg")); + paint.color = Color::Red(); + paint.color_filter = + ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); + paint.invert_colors = true; + + canvas.DrawImage(image, Point::MakeXY(100.0, 100.0), paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -static void CanRenderConicalGradientWithDithering(AiksTest* aiks_test, - bool use_dithering) { +TEST_P(AiksTest, CanRenderTiledTextureClamp) { + CanRenderTiledTexture(this, Entity::TileMode::kClamp); +} + +TEST_P(AiksTest, CanRenderTiledTextureClampAsync) { + CanRenderTiledTexture(this, Entity::TileMode::kClamp, /*async_submit=*/true); +} + +TEST_P(AiksTest, CanRenderTiledTextureRepeat) { + CanRenderTiledTexture(this, Entity::TileMode::kRepeat); +} + +TEST_P(AiksTest, CanRenderTiledTextureMirror) { + CanRenderTiledTexture(this, Entity::TileMode::kMirror); +} + +TEST_P(AiksTest, CanRenderTiledTextureDecal) { + CanRenderTiledTexture(this, Entity::TileMode::kDecal); +} + +TEST_P(AiksTest, CanRenderTiledTextureClampWithTranslate) { + CanRenderTiledTexture(this, Entity::TileMode::kClamp, /*async_submit=*/false, + Matrix::MakeTranslation({172.f, 172.f, 0.f})); +} + +TEST_P(AiksTest, CanRenderImageRect) { Canvas canvas; - canvas.Scale(aiks_test->GetContentScale()); Paint paint; - canvas.Translate({100.0, 100.0, 0}); + auto image = std::make_shared(CreateTextureForFixture("kalimba.jpg")); + Size image_half_size = Size(image->GetSize()) * 0.5; - // #FFF -> #000 - std::vector colors = {Color{1.0, 1.0, 1.0, 1.0}, - Color{0.0, 0.0, 0.0, 1.0}}; - std::vector stops = {0.0, 1.0}; + // Render the bottom right quarter of the source image in a stretched rect. + auto source_rect = Rect::MakeSize(image_half_size); + source_rect = source_rect.Shift(Point(image_half_size)); - paint.color_source = ColorSource::MakeConicalGradient( - {100, 100}, 100, std::move(colors), std::move(stops), {0, 1}, 0, - Entity::TileMode::kMirror, {}); - paint.dither = use_dithering; + canvas.DrawImageRect(image, source_rect, Rect::MakeXYWH(100, 100, 600, 600), + paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} +} // namespace images + +/// These tests check the visual results of drawing pictures. +namespace pictures { +TEST_P(AiksTest, CanPictureConvertToImage) { + Canvas recorder_canvas; + Paint paint; + paint.color = Color{0.9568, 0.2627, 0.2118, 1.0}; + recorder_canvas.DrawRect(Rect::MakeXYWH(100.0, 100.0, 600, 600), paint); + paint.color = Color{0.1294, 0.5882, 0.9529, 1.0}; + recorder_canvas.DrawRect(Rect::MakeXYWH(200.0, 200.0, 600, 600), paint); + + Canvas canvas; + AiksContext renderer(GetContext(), nullptr); + paint.color = Color::BlackTransparent(); + canvas.DrawPaint(paint); + Picture picture = recorder_canvas.EndRecordingAsPicture(); + auto image = picture.ToImage(renderer, ISize{1000, 1000}); + if (image) { + canvas.DrawImage(image, Point(), Paint()); + paint.color = Color{0.1, 0.1, 0.1, 0.2}; + canvas.DrawRect(Rect::MakeSize(ISize{1000, 1000}), paint); + } - canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); - ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderConicalGradientWithDitheringDisabled) { - CanRenderConicalGradientWithDithering(this, false); -} -TEST_P(AiksTest, CanRenderConicalGradientWithDitheringEnabled) { - CanRenderConicalGradientWithDithering(this, true); -} +TEST_P(AiksTest, CanCanvasDrawPicture) { + Canvas subcanvas; + subcanvas.DrawRect(Rect::MakeLTRB(-100, -50, 100, 50), + {.color = Color::CornflowerBlue()}); + auto picture = subcanvas.EndRecordingAsPicture(); -namespace { -void CanRenderLinearGradientWithOverlappingStops(AiksTest* aiks_test, - Entity::TileMode tile_mode) { Canvas canvas; - Paint paint; - canvas.Translate({100.0, 100.0, 0}); + canvas.Translate({200, 200}); + canvas.Rotate(Radians(kPi / 4)); + canvas.DrawPicture(picture); - std::vector colors = { - Color{0.9568, 0.2627, 0.2118, 1.0}, Color{0.9568, 0.2627, 0.2118, 1.0}, - Color{0.1294, 0.5882, 0.9529, 1.0}, Color{0.1294, 0.5882, 0.9529, 1.0}}; - std::vector stops = {0.0, 0.5, 0.5, 1.0}; + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} - paint.color_source = ColorSource::MakeLinearGradient( - {0, 0}, {500, 500}, std::move(colors), std::move(stops), tile_mode, {}); +TEST_P(AiksTest, CanCanvasDrawPictureWithAdvancedBlend) { + Canvas subcanvas; + subcanvas.DrawRect( + Rect::MakeLTRB(-100, -50, 100, 50), + {.color = Color::CornflowerBlue(), .blend_mode = BlendMode::kColorDodge}); + auto picture = subcanvas.EndRecordingAsPicture(); - paint.color = Color(1.0, 1.0, 1.0, 1.0); - canvas.DrawRect(Rect::MakeXYWH(0, 0, 500, 500), paint); - ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} -} // namespace + Canvas canvas; + canvas.DrawPaint({.color = Color::Black()}); + canvas.DrawCircle(Point::MakeXY(150, 150), 25, {.color = Color::Red()}); + canvas.DrawPicture(picture); -// Only clamp is necessary. All tile modes are the same output. -TEST_P(AiksTest, CanRenderLinearGradientWithOverlappingStopsClamp) { - CanRenderLinearGradientWithOverlappingStops(this, Entity::TileMode::kClamp); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -namespace { -void CanRenderLinearGradientManyColors(AiksTest* aiks_test, - Entity::TileMode tile_mode) { - Canvas canvas; - canvas.Scale(aiks_test->GetContentScale()); +TEST_P(AiksTest, CanCanvasDrawPictureWithBackdropFilter) { + Canvas subcanvas; + subcanvas.SaveLayer({}, {}, + ImageFilter::MakeBlur(Sigma(20.0), Sigma(20.0), + FilterContents::BlurStyle::kNormal, + Entity::TileMode::kDecal)); + auto image = std::make_shared(CreateTextureForFixture("kalimba.jpg")); Paint paint; - canvas.Translate({100, 100, 0}); + paint.color = Color::Red().WithAlpha(0.5); + subcanvas.DrawImage(image, Point::MakeXY(100.0, 100.0), paint); - std::vector colors = { - Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0}, - Color{0x5b / 255.0, 0.0, 0x60 / 255.0, 1.0}, - Color{0x87 / 255.0, 0x01 / 255.0, 0x60 / 255.0, 1.0}, - Color{0xac / 255.0, 0x25 / 255.0, 0x53 / 255.0, 1.0}, - Color{0xe1 / 255.0, 0x6b / 255.0, 0x5c / 255.0, 1.0}, - Color{0xf3 / 255.0, 0x90 / 255.0, 0x60 / 255.0, 1.0}, - Color{0xff / 255.0, 0xb5 / 255.0, 0x6b / 250.0, 1.0}}; - std::vector stops = { - 0.0, - (1.0 / 6.0) * 1, - (1.0 / 6.0) * 2, - (1.0 / 6.0) * 3, - (1.0 / 6.0) * 4, - (1.0 / 6.0) * 5, - 1.0, - }; + auto picture = subcanvas.EndRecordingAsPicture(); - paint.color_source = ColorSource::MakeLinearGradient( - {0, 0}, {200, 200}, std::move(colors), std::move(stops), tile_mode, {}); + Canvas canvas; + canvas.DrawPaint({.color = Color::Black()}); + canvas.DrawCircle(Point::MakeXY(150, 150), 25, {.color = Color::Red()}); + canvas.DrawPicture(picture); - paint.color = Color(1.0, 1.0, 1.0, 1.0); - canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); - canvas.Restore(); - ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -} // namespace -TEST_P(AiksTest, CanRenderLinearGradientManyColorsClamp) { - CanRenderLinearGradientManyColors(this, Entity::TileMode::kClamp); -} -TEST_P(AiksTest, CanRenderLinearGradientManyColorsRepeat) { - CanRenderLinearGradientManyColors(this, Entity::TileMode::kRepeat); -} -TEST_P(AiksTest, CanRenderLinearGradientManyColorsMirror) { - CanRenderLinearGradientManyColors(this, Entity::TileMode::kMirror); -} -TEST_P(AiksTest, CanRenderLinearGradientManyColorsDecal) { - CanRenderLinearGradientManyColors(this, Entity::TileMode::kDecal); -} +TEST_P(AiksTest, DrawPictureClipped) { + Canvas subcanvas; + subcanvas.ClipRRect(Rect::MakeLTRB(100, 100, 400, 400), {15, 15}); + subcanvas.DrawPaint({.color = Color::Red()}); + auto picture = subcanvas.EndRecordingAsPicture(); -namespace { -void CanRenderLinearGradientWayManyColors(AiksTest* aiks_test, - Entity::TileMode tile_mode) { Canvas canvas; - Paint paint; - canvas.Translate({100.0, 100.0, 0}); - auto color = Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0}; - std::vector colors; - std::vector stops; - auto current_stop = 0.0; - for (int i = 0; i < 2000; i++) { - colors.push_back(color); - stops.push_back(current_stop); - current_stop += 1 / 2000.0; - } - stops[2000 - 1] = 1.0; + canvas.DrawPaint({.color = Color::CornflowerBlue()}); - paint.color_source = ColorSource::MakeLinearGradient( - {0, 0}, {200, 200}, std::move(colors), std::move(stops), tile_mode, {}); + // Draw a red RRect via DrawPicture. + canvas.DrawPicture(picture); - canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); - ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} -} // namespace + // Draw over the picture with a larger green rectangle, completely covering it + // up. + canvas.ClipRRect(Rect::MakeLTRB(100, 100, 400, 400).Expand(20), {15, 15}); + canvas.DrawPaint({.color = Color::Green()}); -// Only test clamp on purpose since they all look the same. -TEST_P(AiksTest, CanRenderLinearGradientWayManyColorsClamp) { - CanRenderLinearGradientWayManyColors(this, Entity::TileMode::kClamp); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} } -TEST_P(AiksTest, CanRenderLinearGradientManyColorsUnevenStops) { - auto callback = [&](AiksContext& renderer) -> std::optional { - const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"}; - const Entity::TileMode tile_modes[] = { - Entity::TileMode::kClamp, Entity::TileMode::kRepeat, - Entity::TileMode::kMirror, Entity::TileMode::kDecal}; +/// These tests check the visual results of drawing text. +namespace text { +namespace { +struct TextRenderOptions { + Scalar font_size = 50; + Color color = Color::Yellow(); + Point position = Vector2(100, 200); + std::optional mask_blur_descriptor; +}; - static int selected_tile_mode = 0; - ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); - ImGui::Combo("Tile mode", &selected_tile_mode, tile_mode_names, - sizeof(tile_mode_names) / sizeof(char*)); - static Matrix matrix = { - 1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 1, 0, // - 0, 0, 0, 1 // - }; - std::string label = "##1"; - for (int i = 0; i < 4; i++) { - ImGui::InputScalarN(label.c_str(), ImGuiDataType_Float, &(matrix.vec[i]), - 4, NULL, NULL, "%.2f", 0); - label[2]++; - } - ImGui::End(); +bool RenderTextInCanvasSkia(const std::shared_ptr& context, + Canvas& canvas, + const std::string& text, + const std::string_view& font_fixture, + TextRenderOptions options = {}) { + // Draw the baseline. + canvas.DrawRect( + Rect::MakeXYWH(options.position.x - 50, options.position.y, 900, 10), + Paint{.color = Color::Aqua().WithAlpha(0.25)}); - Canvas canvas; - Paint paint; - canvas.Translate({100.0, 100.0, 0}); - auto tile_mode = tile_modes[selected_tile_mode]; + // Mark the point at which the text is drawn. + canvas.DrawCircle(options.position, 5.0, + Paint{.color = Color::Red().WithAlpha(0.25)}); - std::vector colors = { - Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0}, - Color{0x5b / 255.0, 0.0, 0x60 / 255.0, 1.0}, - Color{0x87 / 255.0, 0x01 / 255.0, 0x60 / 255.0, 1.0}, - Color{0xac / 255.0, 0x25 / 255.0, 0x53 / 255.0, 1.0}, - Color{0xe1 / 255.0, 0x6b / 255.0, 0x5c / 255.0, 1.0}, - Color{0xf3 / 255.0, 0x90 / 255.0, 0x60 / 255.0, 1.0}, - Color{0xff / 255.0, 0xb5 / 255.0, 0x6b / 250.0, 1.0}}; - std::vector stops = { - 0.0, 2.0 / 62.0, 4.0 / 62.0, 8.0 / 62.0, 16.0 / 62.0, 32.0 / 62.0, 1.0, - }; + // Construct the text blob. + auto c_font_fixture = std::string(font_fixture); + auto mapping = flutter::testing::OpenFixtureAsSkData(c_font_fixture.c_str()); + if (!mapping) { + return false; + } + sk_sp font_mgr = txt::GetDefaultFontManager(); + SkFont sk_font(font_mgr->makeFromData(mapping), options.font_size); + auto blob = SkTextBlob::MakeFromString(text.c_str(), sk_font); + if (!blob) { + return false; + } - paint.color_source = ColorSource::MakeLinearGradient( - {0, 0}, {200, 200}, std::move(colors), std::move(stops), tile_mode, {}); + // Create the Impeller text frame and draw it at the designated baseline. + auto frame = MakeTextFrameFromTextBlobSkia(blob); - canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); - return canvas.EndRecordingAsPicture(); - }; - ASSERT_TRUE(OpenPlaygroundHere(callback)); + Paint text_paint; + text_paint.color = options.color; + text_paint.mask_blur_descriptor = options.mask_blur_descriptor; + canvas.DrawTextFrame(frame, options.position, text_paint); + return true; } -TEST_P(AiksTest, CanRenderLinearGradientMaskBlur) { - Canvas canvas; - - Paint paint = { - .color = Color::White(), - .color_source = ColorSource::MakeLinearGradient( - {200, 200}, {400, 400}, - {Color::Red(), Color::White(), Color::Red(), Color::White(), - Color::Red(), Color::White(), Color::Red(), Color::White(), - Color::Red(), Color::White(), Color::Red()}, - {0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}, - Entity::TileMode::kClamp, {}), - .mask_blur_descriptor = - Paint::MaskBlurDescriptor{ - .style = FilterContents::BlurStyle::kNormal, - .sigma = Sigma(20), - }, - }; +bool RenderTextInCanvasSTB(const std::shared_ptr& context, + Canvas& canvas, + const std::string& text, + const std::string& font_fixture, + TextRenderOptions options = {}) { + // Draw the baseline. + canvas.DrawRect( + Rect::MakeXYWH(options.position.x - 50, options.position.y, 900, 10), + Paint{.color = Color::Aqua().WithAlpha(0.25)}); - canvas.DrawCircle({300, 300}, 200, paint); - canvas.DrawRect(Rect::MakeLTRB(100, 300, 500, 600), paint); + // Mark the point at which the text is drawn. + canvas.DrawCircle(options.position, 5.0, + Paint{.color = Color::Red().WithAlpha(0.25)}); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); + // Construct the text blob. + auto mapping = flutter::testing::OpenFixtureAsMapping(font_fixture.c_str()); + if (!mapping) { + return false; + } + auto typeface_stb = std::make_shared(std::move(mapping)); + + auto frame = MakeTextFrameSTB( + typeface_stb, Font::Metrics{.point_size = options.font_size}, text); + + Paint text_paint; + text_paint.color = options.color; + canvas.DrawTextFrame(frame, options.position, text_paint); + return true; } +} // namespace -TEST_P(AiksTest, CanRenderRadialGradient) { - auto callback = [&](AiksContext& renderer) -> std::optional { - const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"}; - const Entity::TileMode tile_modes[] = { - Entity::TileMode::kClamp, Entity::TileMode::kRepeat, - Entity::TileMode::kMirror, Entity::TileMode::kDecal}; +TEST_P(AiksTest, DrawPictureWithText) { + Canvas subcanvas; + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), subcanvas, + "the quick brown fox jumped over the lazy dog!.?", "Roboto-Regular.ttf")); + subcanvas.Translate({0, 10}); + subcanvas.Scale(Vector2(3, 3)); + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), subcanvas, + "the quick brown fox jumped over the very big lazy dog!.?", + "Roboto-Regular.ttf")); + auto picture = subcanvas.EndRecordingAsPicture(); - static int selected_tile_mode = 0; - ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); - ImGui::Combo("Tile mode", &selected_tile_mode, tile_mode_names, - sizeof(tile_mode_names) / sizeof(char*)); - static Matrix matrix = { - 1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 1, 0, // - 0, 0, 0, 1 // - }; - std::string label = "##1"; - for (int i = 0; i < 4; i++) { - ImGui::InputScalarN(label.c_str(), ImGuiDataType_Float, &(matrix.vec[i]), - 4, NULL, NULL, "%.2f", 0); - label[2]++; - } - ImGui::End(); + Canvas canvas; + canvas.Scale(Vector2(.2, .2)); + canvas.Save(); + canvas.Translate({200, 200}); + canvas.Scale(Vector2(3.5, 3.5)); // The text must not be blurry after this. + canvas.DrawPicture(picture); + canvas.Restore(); - Canvas canvas; - Paint paint; - canvas.Translate({100.0, 100.0, 0}); - auto tile_mode = tile_modes[selected_tile_mode]; + canvas.Scale(Vector2(1.5, 1.5)); + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), canvas, + "the quick brown fox jumped over the smaller lazy dog!.?", + "Roboto-Regular.ttf")); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} - std::vector colors = {Color{0.9568, 0.2627, 0.2118, 1.0}, - Color{0.1294, 0.5882, 0.9529, 1.0}}; - std::vector stops = {0.0, 1.0}; +TEST_P(AiksTest, CanRenderTextFrame) { + Canvas canvas; + canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), canvas, "the quick brown fox jumped over the lazy dog!.?", + "Roboto-Regular.ttf")); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} - paint.color_source = ColorSource::MakeRadialGradient( - {100, 100}, 100, std::move(colors), std::move(stops), tile_mode, {}); +TEST_P(AiksTest, CanRenderTextFrameSTB) { + Canvas canvas; + canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); + ASSERT_TRUE(RenderTextInCanvasSTB( + GetContext(), canvas, "the quick brown fox jumped over the lazy dog!.?", + "Roboto-Regular.ttf")); - canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); - return canvas.EndRecordingAsPicture(); - }; - ASSERT_TRUE(OpenPlaygroundHere(callback)); + SetTypographerContext(TypographerContextSTB::Make()); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderRadialGradientManyColors) { - auto callback = [&](AiksContext& renderer) -> std::optional { - const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"}; - const Entity::TileMode tile_modes[] = { - Entity::TileMode::kClamp, Entity::TileMode::kRepeat, - Entity::TileMode::kMirror, Entity::TileMode::kDecal}; +TEST_P(AiksTest, TextFrameSubpixelAlignment) { + std::array phase_offsets; + for (Scalar& offset : phase_offsets) { + auto rand = std::rand(); // NOLINT + offset = (static_cast(rand) / static_cast(RAND_MAX)) * k2Pi; + } - static int selected_tile_mode = 0; + auto callback = [&](AiksContext& renderer) -> std::optional { + static float font_size = 20; + static float phase_variation = 0.2; + static float speed = 0.5; + static float magnitude = 100; ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); - ImGui::Combo("Tile mode", &selected_tile_mode, tile_mode_names, - sizeof(tile_mode_names) / sizeof(char*)); - static Matrix matrix = { - 1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 1, 0, // - 0, 0, 0, 1 // - }; - std::string label = "##1"; - for (int i = 0; i < 4; i++) { - ImGui::InputScalarN(label.c_str(), ImGuiDataType_Float, &(matrix.vec[i]), - 4, NULL, NULL, "%.2f", 0); - label[2]++; - } + ImGui::SliderFloat("Font size", &font_size, 5, 50); + ImGui::SliderFloat("Phase variation", &phase_variation, 0, 1); + ImGui::SliderFloat("Oscillation speed", &speed, 0, 2); + ImGui::SliderFloat("Oscillation magnitude", &magnitude, 0, 300); ImGui::End(); Canvas canvas; - Paint paint; - canvas.Translate({100.0, 100.0, 0}); - auto tile_mode = tile_modes[selected_tile_mode]; - - std::vector colors = { - Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0}, - Color{0x5b / 255.0, 0.0, 0x60 / 255.0, 1.0}, - Color{0x87 / 255.0, 0x01 / 255.0, 0x60 / 255.0, 1.0}, - Color{0xac / 255.0, 0x25 / 255.0, 0x53 / 255.0, 1.0}, - Color{0xe1 / 255.0, 0x6b / 255.0, 0x5c / 255.0, 1.0}, - Color{0xf3 / 255.0, 0x90 / 255.0, 0x60 / 255.0, 1.0}, - Color{0xff / 255.0, 0xb5 / 255.0, 0x6b / 250.0, 1.0}}; - std::vector stops = { - 0.0, - (1.0 / 6.0) * 1, - (1.0 / 6.0) * 2, - (1.0 / 6.0) * 3, - (1.0 / 6.0) * 4, - (1.0 / 6.0) * 5, - 1.0, - }; - - paint.color_source = ColorSource::MakeRadialGradient( - {100, 100}, 100, std::move(colors), std::move(stops), tile_mode, {}); + canvas.Scale(GetContentScale()); - canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); + for (size_t i = 0; i < phase_offsets.size(); i++) { + auto position = Point( + 200 + magnitude * std::sin((-phase_offsets[i] * phase_variation + + GetSecondsElapsed() * speed)), // + 200 + i * font_size * 1.1 // + ); + if (!RenderTextInCanvasSkia( + GetContext(), canvas, + "the quick brown fox jumped over " + "the lazy dog!.?", + "Roboto-Regular.ttf", + {.font_size = font_size, .position = position})) { + return std::nullopt; + } + } return canvas.EndRecordingAsPicture(); }; + ASSERT_TRUE(OpenPlaygroundHere(callback)); } -namespace { -void CanRenderSweepGradient(AiksTest* aiks_test, Entity::TileMode tile_mode) { +TEST_P(AiksTest, CanRenderItalicizedText) { Canvas canvas; - canvas.Scale(aiks_test->GetContentScale()); - Paint paint; - canvas.Translate({100, 100, 0}); + canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - std::vector colors = {Color{0.9568, 0.2627, 0.2118, 1.0}, - Color{0.1294, 0.5882, 0.9529, 1.0}}; - std::vector stops = {0.0, 1.0}; + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), canvas, "the quick brown fox jumped over the lazy dog!.?", + "HomemadeApple.ttf")); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} - paint.color_source = ColorSource::MakeSweepGradient( - {100, 100}, Degrees(45), Degrees(135), std::move(colors), - std::move(stops), tile_mode, {}); +static constexpr std::string_view kFontFixture = +#if FML_OS_MACOSX + "Apple Color Emoji.ttc"; +#else + "NotoColorEmoji.ttf"; +#endif - canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); - ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} -} // namespace +TEST_P(AiksTest, CanRenderEmojiTextFrame) { + Canvas canvas; + canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); -TEST_P(AiksTest, CanRenderSweepGradientClamp) { - CanRenderSweepGradient(this, Entity::TileMode::kClamp); -} -TEST_P(AiksTest, CanRenderSweepGradientRepeat) { - CanRenderSweepGradient(this, Entity::TileMode::kRepeat); -} -TEST_P(AiksTest, CanRenderSweepGradientMirror) { - CanRenderSweepGradient(this, Entity::TileMode::kMirror); -} -TEST_P(AiksTest, CanRenderSweepGradientDecal) { - CanRenderSweepGradient(this, Entity::TileMode::kDecal); + ASSERT_TRUE(RenderTextInCanvasSkia(GetContext(), canvas, + "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 😊", kFontFixture)); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -namespace { -void CanRenderSweepGradientManyColors(AiksTest* aiks_test, - Entity::TileMode tile_mode) { +TEST_P(AiksTest, CanRenderEmojiTextFrameWithBlur) { Canvas canvas; - Paint paint; - canvas.Translate({100.0, 100.0, 0}); + canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - std::vector colors = { - Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0}, - Color{0x5b / 255.0, 0.0, 0x60 / 255.0, 1.0}, - Color{0x87 / 255.0, 0x01 / 255.0, 0x60 / 255.0, 1.0}, - Color{0xac / 255.0, 0x25 / 255.0, 0x53 / 255.0, 1.0}, - Color{0xe1 / 255.0, 0x6b / 255.0, 0x5c / 255.0, 1.0}, - Color{0xf3 / 255.0, 0x90 / 255.0, 0x60 / 255.0, 1.0}, - Color{0xff / 255.0, 0xb5 / 255.0, 0x6b / 250.0, 1.0}}; - std::vector stops = { - 0.0, - (1.0 / 6.0) * 1, - (1.0 / 6.0) * 2, - (1.0 / 6.0) * 3, - (1.0 / 6.0) * 4, - (1.0 / 6.0) * 5, - 1.0, - }; + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), canvas, "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 😊", kFontFixture, + TextRenderOptions{.color = Color::Blue(), + .mask_blur_descriptor = Paint::MaskBlurDescriptor{ + .style = FilterContents::BlurStyle::kNormal, + .sigma = Sigma(4)}})); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} - paint.color_source = ColorSource::MakeSweepGradient( - {100, 100}, Degrees(45), Degrees(135), std::move(colors), - std::move(stops), tile_mode, {}); +TEST_P(AiksTest, CanRenderEmojiTextFrameWithAlpha) { + Canvas canvas; + canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - canvas.DrawRect(Rect::MakeXYWH(0, 0, 600, 600), paint); - ASSERT_TRUE(aiks_test->OpenPlaygroundHere(canvas.EndRecordingAsPicture())); + ASSERT_TRUE(RenderTextInCanvasSkia(GetContext(), canvas, + "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 😊", kFontFixture, + {.color = Color::Black().WithAlpha(0.5)})); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -} // namespace -TEST_P(AiksTest, CanRenderSweepGradientManyColorsClamp) { - CanRenderSweepGradientManyColors(this, Entity::TileMode::kClamp); -} -TEST_P(AiksTest, CanRenderSweepGradientManyColorsRepeat) { - CanRenderSweepGradientManyColors(this, Entity::TileMode::kRepeat); -} -TEST_P(AiksTest, CanRenderSweepGradientManyColorsMirror) { - CanRenderSweepGradientManyColors(this, Entity::TileMode::kMirror); -} -TEST_P(AiksTest, CanRenderSweepGradientManyColorsDecal) { - CanRenderSweepGradientManyColors(this, Entity::TileMode::kDecal); +TEST_P(AiksTest, CanRenderTextInSaveLayer) { + Canvas canvas; + canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); + + canvas.Translate({100, 100}); + canvas.Scale(Vector2{0.5, 0.5}); + + // Blend the layer with the parent pass using kClear to expose the coverage. + canvas.SaveLayer({.blend_mode = BlendMode::kClear}); + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), canvas, "the quick brown fox jumped over the lazy dog!.?", + "Roboto-Regular.ttf")); + canvas.Restore(); + + // Render the text again over the cleared coverage rect. + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), canvas, "the quick brown fox jumped over the lazy dog!.?", + "Roboto-Regular.ttf")); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderConicalGradient) { - Scalar size = 256; +TEST_P(AiksTest, CanRenderTextOutsideBoundaries) { Canvas canvas; - Paint paint; - paint.color = Color::White(); - canvas.DrawRect(Rect::MakeXYWH(0, 0, size * 3, size * 3), paint); - std::vector colors = {Color::MakeRGBA8(0xF4, 0x43, 0x36, 0xFF), - Color::MakeRGBA8(0xFF, 0xEB, 0x3B, 0xFF), - Color::MakeRGBA8(0x4c, 0xAF, 0x50, 0xFF), - Color::MakeRGBA8(0x21, 0x96, 0xF3, 0xFF)}; - std::vector stops = {0.0, 1.f / 3.f, 2.f / 3.f, 1.0}; - std::array, 8> array{ - std::make_tuple(Point{size / 2.f, size / 2.f}, 0.f, - Point{size / 2.f, size / 2.f}, size / 2.f), - std::make_tuple(Point{size / 2.f, size / 2.f}, size / 4.f, - Point{size / 2.f, size / 2.f}, size / 2.f), - std::make_tuple(Point{size / 4.f, size / 4.f}, 0.f, - Point{size / 2.f, size / 2.f}, size / 2.f), - std::make_tuple(Point{size / 4.f, size / 4.f}, size / 2.f, - Point{size / 2.f, size / 2.f}, 0), - std::make_tuple(Point{size / 4.f, size / 4.f}, size / 4.f, - Point{size / 2.f, size / 2.f}, size / 2.f), - std::make_tuple(Point{size / 4.f, size / 4.f}, size / 16.f, - Point{size / 2.f, size / 2.f}, size / 8.f), - std::make_tuple(Point{size / 4.f, size / 4.f}, size / 8.f, - Point{size / 2.f, size / 2.f}, size / 16.f), - std::make_tuple(Point{size / 8.f, size / 8.f}, size / 8.f, - Point{size / 2.f, size / 2.f}, size / 8.f), - }; - for (int i = 0; i < 8; i++) { + canvas.Translate({200, 150}); + + // Construct the text blob. + auto mapping = flutter::testing::OpenFixtureAsSkData("wtf.otf"); + ASSERT_NE(mapping, nullptr); + + Scalar font_size = 80; + sk_sp font_mgr = txt::GetDefaultFontManager(); + SkFont sk_font(font_mgr->makeFromData(mapping), font_size); + + Paint text_paint; + text_paint.color = Color::Blue().WithAlpha(0.8); + + struct { + Point position; + const char* text; + } text[] = {{Point(0, 0), "0F0F0F0"}, + {Point(1, 2), "789"}, + {Point(1, 3), "456"}, + {Point(1, 4), "123"}, + {Point(0, 6), "0F0F0F0"}}; + for (auto& t : text) { canvas.Save(); - canvas.Translate({(i % 3) * size, i / 3 * size, 0}); - paint.color_source = ColorSource::MakeConicalGradient( - std::get<0>(array[i]), std::get<1>(array[i]), colors, stops, - std::get<2>(array[i]), std::get<3>(array[i]), Entity::TileMode::kClamp, - {}); - canvas.DrawRect(Rect::MakeXYWH(0, 0, size, size), paint); + canvas.Translate(t.position * Point(font_size * 2, font_size * 1.1)); + { + auto blob = SkTextBlob::MakeFromString(t.text, sk_font); + ASSERT_NE(blob, nullptr); + auto frame = MakeTextFrameFromTextBlobSkia(blob); + canvas.DrawTextFrame(frame, Point(), text_paint); + } canvas.Restore(); } + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderGradientDecalWithBackground) { - std::vector colors = {Color::MakeRGBA8(0xF4, 0x43, 0x36, 0xFF), - Color::MakeRGBA8(0xFF, 0xEB, 0x3B, 0xFF), - Color::MakeRGBA8(0x4c, 0xAF, 0x50, 0xFF), - Color::MakeRGBA8(0x21, 0x96, 0xF3, 0xFF)}; - std::vector stops = {0.0, 1.f / 3.f, 2.f / 3.f, 1.0}; +TEST_P(AiksTest, TextRotated) { + Canvas canvas; + canvas.Scale(GetContentScale()); + canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - std::array color_sources = { - ColorSource::MakeLinearGradient({0, 0}, {100, 100}, colors, stops, - Entity::TileMode::kDecal, {}), - ColorSource::MakeRadialGradient({100, 100}, 100, colors, stops, - Entity::TileMode::kDecal, {}), - ColorSource::MakeSweepGradient({100, 100}, Degrees(45), Degrees(135), - colors, stops, Entity::TileMode::kDecal, - {}), - }; + canvas.Transform(Matrix(0.25, -0.3, 0, -0.002, // + 0, 0.5, 0, 0, // + 0, 0, 0.3, 0, // + 100, 100, 0, 1.3)); + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), canvas, "the quick brown fox jumped over the lazy dog!.?", + "Roboto-Regular.ttf")); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} +TEST_P(AiksTest, DrawScaledTextWithPerspectiveNoSaveLayer) { Canvas canvas; - Paint paint; - paint.color = Color::White(); - canvas.DrawRect(Rect::MakeLTRB(0, 0, 605, 205), paint); - for (int i = 0; i < 3; i++) { - canvas.Save(); - canvas.Translate({i * 200.0f, 0, 0}); - paint.color_source = color_sources[i]; - canvas.DrawRect(Rect::MakeLTRB(0, 0, 200, 200), paint); - canvas.Restore(); - } + // clang-format off + canvas.Transform(Matrix( + 2.000000, 0.000000, 0.000000, 0.000000, + 1.445767, 2.637070, -0.507928, 0.001524, + -2.451887, -0.534662, 0.861399, -0.002584, + 1063.481934, 1025.951416, -48.300270, 1.144901 + )); + // clang-format on + + ASSERT_TRUE(RenderTextInCanvasSkia(GetContext(), canvas, "Hello world", + "Roboto-Regular.ttf")); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderDifferentShapesWithSameColorSource) { +TEST_P(AiksTest, DrawScaledTextWithPerspectiveSaveLayer) { Canvas canvas; - Paint paint; + Paint save_paint; + canvas.SaveLayer(save_paint); + // clang-format off + canvas.Transform(Matrix( + 2.000000, 0.000000, 0.000000, 0.000000, + 1.445767, 2.637070, -0.507928, 0.001524, + -2.451887, -0.534662, 0.861399, -0.002584, + 1063.481934, 1025.951416, -48.300270, 1.144901 + )); + // clang-format on - std::vector colors = {Color{0.9568, 0.2627, 0.2118, 1.0}, - Color{0.1294, 0.5882, 0.9529, 1.0}}; - std::vector stops = { - 0.0, - 1.0, - }; + ASSERT_TRUE(RenderTextInCanvasSkia(GetContext(), canvas, "Hello world", + "Roboto-Regular.ttf")); +} - paint.color_source = ColorSource::MakeLinearGradient( - {0, 0}, {100, 100}, std::move(colors), std::move(stops), - Entity::TileMode::kRepeat, {}); +} // namespace text - canvas.Save(); - canvas.Translate({100, 100, 0}); - canvas.DrawRect(Rect::MakeXYWH(0, 0, 200, 200), paint); - canvas.Restore(); +namespace unsorted { - canvas.Save(); - canvas.Translate({100, 400, 0}); - canvas.DrawCircle({100, 100}, 100, paint); - canvas.Restore(); +TEST_P(AiksTest, RotateColorFilteredPath) { + Canvas canvas; + canvas.Concat(Matrix::MakeTranslation({300, 300})); + canvas.Concat(Matrix::MakeRotationZ(Radians(kPiOver2))); + auto arrow_stem = + PathBuilder{}.MoveTo({120, 190}).LineTo({120, 50}).TakePath(); + auto arrow_head = PathBuilder{} + .MoveTo({50, 120}) + .LineTo({120, 190}) + .LineTo({190, 120}) + .TakePath(); + auto paint = Paint{ + .stroke_width = 15.0, + .stroke_cap = Cap::kRound, + .stroke_join = Join::kRound, + .style = Paint::Style::kStroke, + .color_filter = + ColorFilter::MakeBlend(BlendMode::kSourceIn, Color::AliceBlue()), + }; + + canvas.DrawPath(std::move(arrow_stem), paint); + canvas.DrawPath(std::move(arrow_head), paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanPictureConvertToImage) { - Canvas recorder_canvas; - Paint paint; - paint.color = Color{0.9568, 0.2627, 0.2118, 1.0}; - recorder_canvas.DrawRect(Rect::MakeXYWH(100.0, 100.0, 600, 600), paint); - paint.color = Color{0.1294, 0.5882, 0.9529, 1.0}; - recorder_canvas.DrawRect(Rect::MakeXYWH(200.0, 200.0, 600, 600), paint); +TEST_P(AiksTest, CanvasCTMCanBeUpdated) { + Canvas canvas; + Matrix identity; + ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), identity); + canvas.Translate(Size{100, 100}); + ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), + Matrix::MakeTranslation({100.0, 100.0, 0.0})); +} +TEST_P(AiksTest, CanvasCanPushPopCTM) { Canvas canvas; - AiksContext renderer(GetContext(), nullptr); - paint.color = Color::BlackTransparent(); - canvas.DrawPaint(paint); - Picture picture = recorder_canvas.EndRecordingAsPicture(); - auto image = picture.ToImage(renderer, ISize{1000, 1000}); - if (image) { - canvas.DrawImage(image, Point(), Paint()); - paint.color = Color{0.1, 0.1, 0.1, 0.2}; - canvas.DrawRect(Rect::MakeSize(ISize{1000, 1000}), paint); - } + ASSERT_EQ(canvas.GetSaveCount(), 1u); + ASSERT_EQ(canvas.Restore(), false); + + canvas.Translate(Size{100, 100}); + canvas.Save(); + ASSERT_EQ(canvas.GetSaveCount(), 2u); + ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), + Matrix::MakeTranslation({100.0, 100.0, 0.0})); + ASSERT_TRUE(canvas.Restore()); + ASSERT_EQ(canvas.GetSaveCount(), 1u); + ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), + Matrix::MakeTranslation({100.0, 100.0, 0.0})); +} +TEST_P(AiksTest, CanRenderColoredRect) { + Canvas canvas; + Paint paint; + paint.color = Color::Blue(); + canvas.DrawPath(PathBuilder{} + .AddRect(Rect::MakeXYWH(100.0, 100.0, 100.0, 100.0)) + .TakePath(), + paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, BlendModeShouldCoverWholeScreen) { +TEST_P(AiksTest, CanRenderColorFilterWithInvertColors) { Canvas canvas; Paint paint; - paint.color = Color::Red(); - canvas.DrawPaint(paint); - - paint.blend_mode = BlendMode::kSourceOver; - canvas.SaveLayer(paint); - - paint.color = Color::White(); - canvas.DrawRect(Rect::MakeXYWH(100, 100, 400, 400), paint); - - paint.blend_mode = BlendMode::kSource; - canvas.SaveLayer(paint); - - paint.color = Color::Blue(); - canvas.DrawRect(Rect::MakeXYWH(200, 200, 200, 200), paint); - - canvas.Restore(); - canvas.Restore(); + paint.color_filter = + ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); + paint.invert_colors = true; + canvas.DrawRect(Rect::MakeLTRB(0, 0, 100, 100), paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderGroupOpacity) { +TEST_P(AiksTest, CanRenderColorFilterWithInvertColorsDrawPaint) { Canvas canvas; + Paint paint; + paint.color = Color::Red(); + paint.color_filter = + ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); + paint.invert_colors = true; - Paint red; - red.color = Color::Red(); - Paint green; - green.color = Color::Green().WithAlpha(0.5); - Paint blue; - blue.color = Color::Blue(); - - Paint alpha; - alpha.color = Color::Red().WithAlpha(0.5); - - canvas.SaveLayer(alpha); - - canvas.DrawRect(Rect::MakeXYWH(000, 000, 100, 100), red); - canvas.DrawRect(Rect::MakeXYWH(020, 020, 100, 100), green); - canvas.DrawRect(Rect::MakeXYWH(040, 040, 100, 100), blue); - - canvas.Restore(); - + canvas.DrawPaint(paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CoordinateConversionsAreCorrect) { +TEST_P(AiksTest, CanRenderStrokes) { Canvas canvas; - - // Render a texture directly. - { - Paint paint; - auto image = - std::make_shared(CreateTextureForFixture("kalimba.jpg")); - paint.color = Color::Red(); - - canvas.Save(); - canvas.Translate({100, 200, 0}); - canvas.Scale(Vector2{0.5, 0.5}); - canvas.DrawImage(image, Point::MakeXY(100.0, 100.0), paint); - canvas.Restore(); - } - - // Render an offscreen rendered texture. - { - Paint red; - red.color = Color::Red(); - Paint green; - green.color = Color::Green(); - Paint blue; - blue.color = Color::Blue(); - - Paint alpha; - alpha.color = Color::Red().WithAlpha(0.5); - - canvas.SaveLayer(alpha); - - canvas.DrawRect(Rect::MakeXYWH(000, 000, 100, 100), red); - canvas.DrawRect(Rect::MakeXYWH(020, 020, 100, 100), green); - canvas.DrawRect(Rect::MakeXYWH(040, 040, 100, 100), blue); - - canvas.Restore(); - } - + Paint paint; + paint.color = Color::Red(); + paint.stroke_width = 20.0; + paint.style = Paint::Style::kStroke; + canvas.DrawPath(PathBuilder{}.AddLine({200, 100}, {800, 100}).TakePath(), + paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanPerformFullScreenMSAA) { +TEST_P(AiksTest, CanRenderCurvedStrokes) { Canvas canvas; - - Paint red; - red.color = Color::Red(); - - canvas.DrawCircle({250, 250}, 125, red); - + Paint paint; + paint.color = Color::Red(); + paint.stroke_width = 25.0; + paint.style = Paint::Style::kStroke; + canvas.DrawPath(PathBuilder{}.AddCircle({500, 500}, 250).TakePath(), paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanPerformSkew) { +TEST_P(AiksTest, CanRenderThickCurvedStrokes) { Canvas canvas; - - Paint red; - red.color = Color::Red(); - - canvas.Skew(2, 5); - canvas.DrawRect(Rect::MakeXYWH(0, 0, 100, 100), red); - + Paint paint; + paint.color = Color::Red(); + paint.stroke_width = 100.0; + paint.style = Paint::Style::kStroke; + canvas.DrawPath(PathBuilder{}.AddCircle({100, 100}, 50).TakePath(), paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanPerformSaveLayerWithBounds) { +TEST_P(AiksTest, CanRenderClips) { Canvas canvas; + Paint paint; + paint.color = Color::Fuchsia(); + canvas.ClipPath( + PathBuilder{}.AddRect(Rect::MakeXYWH(0, 0, 500, 500)).TakePath()); + canvas.DrawPath(PathBuilder{}.AddCircle({500, 500}, 250).TakePath(), paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} - Paint red; - red.color = Color::Red(); +TEST_P(AiksTest, CanRenderSimpleClips) { + Canvas canvas; + canvas.Scale(GetContentScale()); + Paint paint; - Paint green; - green.color = Color::Green(); + paint.color = Color::White(); + canvas.DrawPaint(paint); - Paint blue; - blue.color = Color::Blue(); + auto draw = [&canvas](const Paint& paint, Scalar x, Scalar y) { + canvas.Save(); + canvas.Translate({x, y}); + { + canvas.Save(); + canvas.ClipRect(Rect::MakeLTRB(50, 50, 150, 150)); + canvas.DrawPaint(paint); + canvas.Restore(); + } + { + canvas.Save(); + canvas.ClipOval(Rect::MakeLTRB(200, 50, 300, 150)); + canvas.DrawPaint(paint); + canvas.Restore(); + } + { + canvas.Save(); + canvas.ClipRRect(Rect::MakeLTRB(50, 200, 150, 300), {20, 20}); + canvas.DrawPaint(paint); + canvas.Restore(); + } + canvas.Restore(); + }; - Paint save; - save.color = Color::Black(); + paint.color = Color::Blue(); + draw(paint, 0, 0); - canvas.SaveLayer(save, Rect::MakeXYWH(0, 0, 50, 50)); + std::vector gradient_colors = { + Color{0x1f / 255.0, 0.0, 0x5c / 255.0, 1.0}, + Color{0x5b / 255.0, 0.0, 0x60 / 255.0, 1.0}, + Color{0x87 / 255.0, 0x01 / 255.0, 0x60 / 255.0, 1.0}, + Color{0xac / 255.0, 0x25 / 255.0, 0x53 / 255.0, 1.0}, + Color{0xe1 / 255.0, 0x6b / 255.0, 0x5c / 255.0, 1.0}, + Color{0xf3 / 255.0, 0x90 / 255.0, 0x60 / 255.0, 1.0}, + Color{0xff / 255.0, 0xb5 / 255.0, 0x6b / 250.0, 1.0}}; + std::vector stops = { + 0.0, + (1.0 / 6.0) * 1, + (1.0 / 6.0) * 2, + (1.0 / 6.0) * 3, + (1.0 / 6.0) * 4, + (1.0 / 6.0) * 5, + 1.0, + }; + auto texture = CreateTextureForFixture("airplane.jpg", + /*enable_mipmapping=*/true); - canvas.DrawRect(Rect::MakeXYWH(0, 0, 100, 100), red); - canvas.DrawRect(Rect::MakeXYWH(10, 10, 100, 100), green); - canvas.DrawRect(Rect::MakeXYWH(20, 20, 100, 100), blue); + paint.color_source = ColorSource::MakeRadialGradient( + {500, 600}, 75, std::move(gradient_colors), std::move(stops), + Entity::TileMode::kMirror, {}); + draw(paint, 0, 300); - canvas.Restore(); + paint.color_source = ColorSource::MakeImage( + texture, Entity::TileMode::kRepeat, Entity::TileMode::kRepeat, {}, + Matrix::MakeTranslation({0, 0})); + draw(paint, 300, 0); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, - CanPerformSaveLayerWithBoundsAndLargerIntermediateIsNotAllocated) { +TEST_P(AiksTest, CanRenderNestedClips) { Canvas canvas; + Paint paint; + paint.color = Color::Fuchsia(); + canvas.Save(); + canvas.ClipPath(PathBuilder{}.AddCircle({200, 400}, 300).TakePath()); + canvas.Restore(); + canvas.ClipPath(PathBuilder{}.AddCircle({600, 400}, 300).TakePath()); + canvas.ClipPath(PathBuilder{}.AddCircle({400, 600}, 300).TakePath()); + canvas.DrawRect(Rect::MakeXYWH(200, 200, 400, 400), paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} - Paint red; - red.color = Color::Red(); - - Paint green; - green.color = Color::Green(); - - Paint blue; - blue.color = Color::Blue(); +TEST_P(AiksTest, CanRenderDifferenceClips) { + Paint paint; + Canvas canvas; + canvas.Translate({400, 400}); - Paint save; - save.color = Color::Black().WithAlpha(0.5); + // Limit drawing to face circle with a clip. + canvas.ClipPath(PathBuilder{}.AddCircle(Point(), 200).TakePath()); + canvas.Save(); - canvas.SaveLayer(save, Rect::MakeXYWH(0, 0, 100000, 100000)); + // Cut away eyes/mouth using difference clips. + canvas.ClipPath(PathBuilder{}.AddCircle({-100, -50}, 30).TakePath(), + Entity::ClipOperation::kDifference); + canvas.ClipPath(PathBuilder{}.AddCircle({100, -50}, 30).TakePath(), + Entity::ClipOperation::kDifference); + canvas.ClipPath(PathBuilder{} + .AddQuadraticCurve({-100, 50}, {0, 150}, {100, 50}) + .TakePath(), + Entity::ClipOperation::kDifference); - canvas.DrawRect(Rect::MakeXYWH(0, 0, 100, 100), red); - canvas.DrawRect(Rect::MakeXYWH(10, 10, 100, 100), green); - canvas.DrawRect(Rect::MakeXYWH(20, 20, 100, 100), blue); + // Draw a huge yellow rectangle to prove the clipping works. + paint.color = Color::Yellow(); + canvas.DrawRect(Rect::MakeXYWH(-1000, -1000, 2000, 2000), paint); + // Remove the difference clips and draw hair that partially covers the eyes. canvas.Restore(); + paint.color = Color::Maroon(); + canvas.DrawPath(PathBuilder{} + .MoveTo({200, -200}) + .HorizontalLineTo(-200) + .VerticalLineTo(-40) + .CubicCurveTo({0, -40}, {0, -80}, {200, -80}) + .TakePath(), + paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderRoundedRectWithNonUniformRadii) { +TEST_P(AiksTest, CanRenderWithContiguousClipRestores) { Canvas canvas; - Paint paint; - paint.color = Color::Red(); + // Cover the whole canvas with red. + canvas.DrawPaint({.color = Color::Red()}); - PathBuilder::RoundingRadii radii; - radii.top_left = {50, 25}; - radii.top_right = {25, 50}; - radii.bottom_right = {50, 25}; - radii.bottom_left = {25, 50}; + canvas.Save(); - auto path = PathBuilder{} - .AddRoundedRect(Rect::MakeXYWH(100, 100, 500, 500), radii) - .TakePath(); + // Append two clips, the second resulting in empty coverage. + canvas.ClipPath( + PathBuilder{}.AddRect(Rect::MakeXYWH(100, 100, 100, 100)).TakePath()); + canvas.ClipPath( + PathBuilder{}.AddRect(Rect::MakeXYWH(300, 300, 100, 100)).TakePath()); - canvas.DrawPath(std::move(path), paint); + // Restore to no clips. + canvas.Restore(); + + // Replace the whole canvas with green. + canvas.DrawPaint({.color = Color::Green()}); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderStrokePathThatEndsAtSharpTurn) { +TEST_P(AiksTest, ClipsUseCurrentTransform) { + std::array colors = {Color::White(), Color::Black(), + Color::SkyBlue(), Color::Red(), + Color::Yellow()}; Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 200; - Rect rect = Rect::MakeXYWH(100, 100, 200, 200); - PathBuilder builder; - builder.AddArc(rect, Degrees(0), Degrees(90), false); + canvas.Translate(Vector3(300, 300)); + for (int i = 0; i < 15; i++) { + canvas.Scale(Vector3(0.8, 0.8)); - canvas.DrawPath(builder.TakePath(), paint); + paint.color = colors[i % colors.size()]; + canvas.ClipPath(PathBuilder{}.AddCircle({0, 0}, 300).TakePath()); + canvas.DrawRect(Rect::MakeXYWH(-300, -300, 600, 600), paint); + } ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderStrokePathWithCubicLine) { +TEST_P(AiksTest, CanSaveLayerStandalone) { Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 20; + Paint red; + red.color = Color::Red(); - PathBuilder builder; - builder.AddCubicCurve({0, 200}, {50, 400}, {350, 0}, {400, 200}); + Paint alpha; + alpha.color = Color::Red().WithAlpha(0.5); + + canvas.SaveLayer(alpha); + + canvas.DrawCircle({125, 125}, 125, red); + + canvas.Restore(); - canvas.DrawPath(builder.TakePath(), paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderDifferencePaths) { +TEST_P(AiksTest, CanRenderDifferentShapesWithSameColorSource) { Canvas canvas; - Paint paint; - paint.color = Color::Red(); - - PathBuilder builder; - PathBuilder::RoundingRadii radii; - radii.top_left = {50, 25}; - radii.top_right = {25, 50}; - radii.bottom_right = {50, 25}; - radii.bottom_left = {25, 50}; + std::vector colors = {Color{0.9568, 0.2627, 0.2118, 1.0}, + Color{0.1294, 0.5882, 0.9529, 1.0}}; + std::vector stops = { + 0.0, + 1.0, + }; - builder.AddRoundedRect(Rect::MakeXYWH(100, 100, 200, 200), radii); - builder.AddCircle({200, 200}, 50); - auto path = builder.TakePath(FillType::kOdd); + paint.color_source = ColorSource::MakeLinearGradient( + {0, 0}, {100, 100}, std::move(colors), std::move(stops), + Entity::TileMode::kRepeat, {}); - canvas.DrawImage( - std::make_shared(CreateTextureForFixture("boston.jpg")), {10, 10}, - Paint{}); - canvas.DrawPath(std::move(path), paint); + canvas.Save(); + canvas.Translate({100, 100, 0}); + canvas.DrawRect(Rect::MakeXYWH(0, 0, 200, 200), paint); + canvas.Restore(); + canvas.Save(); + canvas.Translate({100, 400, 0}); + canvas.DrawCircle({100, 100}, 100, paint); + canvas.Restore(); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -// Regression test for https://github.com/flutter/flutter/issues/134816. -// -// It should be possible to draw 3 lines, and not have an implicit close path. -TEST_P(AiksTest, CanDrawAnOpenPath) { +TEST_P(AiksTest, BlendModeShouldCoverWholeScreen) { Canvas canvas; - - // Starting at (50, 50), draw lines from: - // 1. (50, height) - // 2. (width, height) - // 3. (width, 50) - PathBuilder builder; - builder.MoveTo({50, 50}); - builder.LineTo({50, 100}); - builder.LineTo({100, 100}); - builder.LineTo({100, 50}); - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 10; - canvas.DrawPath(builder.TakePath(), paint); + paint.color = Color::Red(); + canvas.DrawPaint(paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} + paint.blend_mode = BlendMode::kSourceOver; + canvas.SaveLayer(paint); -TEST_P(AiksTest, CanDrawAnOpenPathThatIsntARect) { - Canvas canvas; + paint.color = Color::White(); + canvas.DrawRect(Rect::MakeXYWH(100, 100, 400, 400), paint); - // Draw a stroked path that is explicitly closed to verify - // It doesn't become a rectangle. - PathBuilder builder; - builder.MoveTo({50, 50}); - builder.LineTo({520, 120}); - builder.LineTo({300, 310}); - builder.LineTo({100, 50}); - builder.Close(); + paint.blend_mode = BlendMode::kSource; + canvas.SaveLayer(paint); - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 10; + paint.color = Color::Blue(); + canvas.DrawRect(Rect::MakeXYWH(200, 200, 200, 200), paint); - canvas.DrawPath(builder.TakePath(), paint); + canvas.Restore(); + canvas.Restore(); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -struct TextRenderOptions { - Scalar font_size = 50; - Color color = Color::Yellow(); - Point position = Vector2(100, 200); - std::optional mask_blur_descriptor; -}; +TEST_P(AiksTest, CanRenderGroupOpacity) { + Canvas canvas; -bool RenderTextInCanvasSkia(const std::shared_ptr& context, - Canvas& canvas, - const std::string& text, - const std::string_view& font_fixture, - TextRenderOptions options = {}) { - // Draw the baseline. - canvas.DrawRect( - Rect::MakeXYWH(options.position.x - 50, options.position.y, 900, 10), - Paint{.color = Color::Aqua().WithAlpha(0.25)}); + Paint red; + red.color = Color::Red(); + Paint green; + green.color = Color::Green().WithAlpha(0.5); + Paint blue; + blue.color = Color::Blue(); - // Mark the point at which the text is drawn. - canvas.DrawCircle(options.position, 5.0, - Paint{.color = Color::Red().WithAlpha(0.25)}); + Paint alpha; + alpha.color = Color::Red().WithAlpha(0.5); - // Construct the text blob. - auto c_font_fixture = std::string(font_fixture); - auto mapping = flutter::testing::OpenFixtureAsSkData(c_font_fixture.c_str()); - if (!mapping) { - return false; - } - sk_sp font_mgr = txt::GetDefaultFontManager(); - SkFont sk_font(font_mgr->makeFromData(mapping), options.font_size); - auto blob = SkTextBlob::MakeFromString(text.c_str(), sk_font); - if (!blob) { - return false; - } + canvas.SaveLayer(alpha); - // Create the Impeller text frame and draw it at the designated baseline. - auto frame = MakeTextFrameFromTextBlobSkia(blob); + canvas.DrawRect(Rect::MakeXYWH(000, 000, 100, 100), red); + canvas.DrawRect(Rect::MakeXYWH(020, 020, 100, 100), green); + canvas.DrawRect(Rect::MakeXYWH(040, 040, 100, 100), blue); - Paint text_paint; - text_paint.color = options.color; - text_paint.mask_blur_descriptor = options.mask_blur_descriptor; - canvas.DrawTextFrame(frame, options.position, text_paint); - return true; + canvas.Restore(); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -bool RenderTextInCanvasSTB(const std::shared_ptr& context, - Canvas& canvas, - const std::string& text, - const std::string& font_fixture, - TextRenderOptions options = {}) { - // Draw the baseline. - canvas.DrawRect( - Rect::MakeXYWH(options.position.x - 50, options.position.y, 900, 10), - Paint{.color = Color::Aqua().WithAlpha(0.25)}); +TEST_P(AiksTest, CoordinateConversionsAreCorrect) { + Canvas canvas; - // Mark the point at which the text is drawn. - canvas.DrawCircle(options.position, 5.0, - Paint{.color = Color::Red().WithAlpha(0.25)}); + // Render a texture directly. + { + Paint paint; + auto image = + std::make_shared(CreateTextureForFixture("kalimba.jpg")); + paint.color = Color::Red(); - // Construct the text blob. - auto mapping = flutter::testing::OpenFixtureAsMapping(font_fixture.c_str()); - if (!mapping) { - return false; + canvas.Save(); + canvas.Translate({100, 200, 0}); + canvas.Scale(Vector2{0.5, 0.5}); + canvas.DrawImage(image, Point::MakeXY(100.0, 100.0), paint); + canvas.Restore(); } - auto typeface_stb = std::make_shared(std::move(mapping)); - auto frame = MakeTextFrameSTB( - typeface_stb, Font::Metrics{.point_size = options.font_size}, text); + // Render an offscreen rendered texture. + { + Paint red; + red.color = Color::Red(); + Paint green; + green.color = Color::Green(); + Paint blue; + blue.color = Color::Blue(); + + Paint alpha; + alpha.color = Color::Red().WithAlpha(0.5); + + canvas.SaveLayer(alpha); + + canvas.DrawRect(Rect::MakeXYWH(000, 000, 100, 100), red); + canvas.DrawRect(Rect::MakeXYWH(020, 020, 100, 100), green); + canvas.DrawRect(Rect::MakeXYWH(040, 040, 100, 100), blue); - Paint text_paint; - text_paint.color = options.color; - canvas.DrawTextFrame(frame, options.position, text_paint); - return true; + canvas.Restore(); + } + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderTextFrame) { +TEST_P(AiksTest, CanPerformFullScreenMSAA) { Canvas canvas; - canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - ASSERT_TRUE(RenderTextInCanvasSkia( - GetContext(), canvas, "the quick brown fox jumped over the lazy dog!.?", - "Roboto-Regular.ttf")); + + Paint red; + red.color = Color::Red(); + + canvas.DrawCircle({250, 250}, 125, red); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderTextFrameSTB) { +TEST_P(AiksTest, CanPerformSkew) { Canvas canvas; - canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - ASSERT_TRUE(RenderTextInCanvasSTB( - GetContext(), canvas, "the quick brown fox jumped over the lazy dog!.?", - "Roboto-Regular.ttf")); - SetTypographerContext(TypographerContextSTB::Make()); + Paint red; + red.color = Color::Red(); + + canvas.Skew(2, 5); + canvas.DrawRect(Rect::MakeXYWH(0, 0, 100, 100), red); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, TextFrameSubpixelAlignment) { - std::array phase_offsets; - for (Scalar& offset : phase_offsets) { - auto rand = std::rand(); // NOLINT - offset = (static_cast(rand) / static_cast(RAND_MAX)) * k2Pi; - } +TEST_P(AiksTest, CanPerformSaveLayerWithBounds) { + Canvas canvas; - auto callback = [&](AiksContext& renderer) -> std::optional { - static float font_size = 20; - static float phase_variation = 0.2; - static float speed = 0.5; - static float magnitude = 100; - ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); - ImGui::SliderFloat("Font size", &font_size, 5, 50); - ImGui::SliderFloat("Phase variation", &phase_variation, 0, 1); - ImGui::SliderFloat("Oscillation speed", &speed, 0, 2); - ImGui::SliderFloat("Oscillation magnitude", &magnitude, 0, 300); - ImGui::End(); + Paint red; + red.color = Color::Red(); - Canvas canvas; - canvas.Scale(GetContentScale()); + Paint green; + green.color = Color::Green(); - for (size_t i = 0; i < phase_offsets.size(); i++) { - auto position = Point( - 200 + magnitude * std::sin((-phase_offsets[i] * phase_variation + - GetSecondsElapsed() * speed)), // - 200 + i * font_size * 1.1 // - ); - if (!RenderTextInCanvasSkia( - GetContext(), canvas, - "the quick brown fox jumped over " - "the lazy dog!.?", - "Roboto-Regular.ttf", - {.font_size = font_size, .position = position})) { - return std::nullopt; - } - } - return canvas.EndRecordingAsPicture(); - }; + Paint blue; + blue.color = Color::Blue(); - ASSERT_TRUE(OpenPlaygroundHere(callback)); + Paint save; + save.color = Color::Black(); + + canvas.SaveLayer(save, Rect::MakeXYWH(0, 0, 50, 50)); + + canvas.DrawRect(Rect::MakeXYWH(0, 0, 100, 100), red); + canvas.DrawRect(Rect::MakeXYWH(10, 10, 100, 100), green); + canvas.DrawRect(Rect::MakeXYWH(20, 20, 100, 100), blue); + + canvas.Restore(); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderItalicizedText) { +TEST_P(AiksTest, + CanPerformSaveLayerWithBoundsAndLargerIntermediateIsNotAllocated) { Canvas canvas; - canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - ASSERT_TRUE(RenderTextInCanvasSkia( - GetContext(), canvas, "the quick brown fox jumped over the lazy dog!.?", - "HomemadeApple.ttf")); + Paint red; + red.color = Color::Red(); + + Paint green; + green.color = Color::Green(); + + Paint blue; + blue.color = Color::Blue(); + + Paint save; + save.color = Color::Black().WithAlpha(0.5); + + canvas.SaveLayer(save, Rect::MakeXYWH(0, 0, 100000, 100000)); + + canvas.DrawRect(Rect::MakeXYWH(0, 0, 100, 100), red); + canvas.DrawRect(Rect::MakeXYWH(10, 10, 100, 100), green); + canvas.DrawRect(Rect::MakeXYWH(20, 20, 100, 100), blue); + + canvas.Restore(); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -static constexpr std::string_view kFontFixture = -#if FML_OS_MACOSX - "Apple Color Emoji.ttc"; -#else - "NotoColorEmoji.ttf"; -#endif - -TEST_P(AiksTest, CanRenderEmojiTextFrame) { +TEST_P(AiksTest, CanRenderRoundedRectWithNonUniformRadii) { Canvas canvas; - canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - ASSERT_TRUE(RenderTextInCanvasSkia( - GetContext(), canvas, "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 😊", kFontFixture)); + Paint paint; + paint.color = Color::Red(); + + PathBuilder::RoundingRadii radii; + radii.top_left = {50, 25}; + radii.top_right = {25, 50}; + radii.bottom_right = {50, 25}; + radii.bottom_left = {25, 50}; + + auto path = PathBuilder{} + .AddRoundedRect(Rect::MakeXYWH(100, 100, 500, 500), radii) + .TakePath(); + + canvas.DrawPath(std::move(path), paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderEmojiTextFrameWithBlur) { +TEST_P(AiksTest, CanRenderStrokePathThatEndsAtSharpTurn) { Canvas canvas; - canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - ASSERT_TRUE(RenderTextInCanvasSkia( - GetContext(), canvas, "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 😊", kFontFixture, - TextRenderOptions{.color = Color::Blue(), - .mask_blur_descriptor = Paint::MaskBlurDescriptor{ - .style = FilterContents::BlurStyle::kNormal, - .sigma = Sigma(4)}})); + Paint paint; + paint.color = Color::Red(); + paint.style = Paint::Style::kStroke; + paint.stroke_width = 200; + + Rect rect = Rect::MakeXYWH(100, 100, 200, 200); + PathBuilder builder; + builder.AddArc(rect, Degrees(0), Degrees(90), false); + + canvas.DrawPath(builder.TakePath(), paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderEmojiTextFrameWithAlpha) { +TEST_P(AiksTest, CanRenderStrokePathWithCubicLine) { Canvas canvas; - canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - ASSERT_TRUE(RenderTextInCanvasSkia( - GetContext(), canvas, "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 😊", kFontFixture, - {.color = Color::Black().WithAlpha(0.5)})); + Paint paint; + paint.color = Color::Red(); + paint.style = Paint::Style::kStroke; + paint.stroke_width = 20; + + PathBuilder builder; + builder.AddCubicCurve({0, 200}, {50, 400}, {350, 0}, {400, 200}); + + canvas.DrawPath(builder.TakePath(), paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderTextInSaveLayer) { +TEST_P(AiksTest, CanRenderDifferencePaths) { Canvas canvas; - canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - canvas.Translate({100, 100}); - canvas.Scale(Vector2{0.5, 0.5}); + Paint paint; + paint.color = Color::Red(); - // Blend the layer with the parent pass using kClear to expose the coverage. - canvas.SaveLayer({.blend_mode = BlendMode::kClear}); - ASSERT_TRUE(RenderTextInCanvasSkia( - GetContext(), canvas, "the quick brown fox jumped over the lazy dog!.?", - "Roboto-Regular.ttf")); - canvas.Restore(); + PathBuilder builder; - // Render the text again over the cleared coverage rect. - ASSERT_TRUE(RenderTextInCanvasSkia( - GetContext(), canvas, "the quick brown fox jumped over the lazy dog!.?", - "Roboto-Regular.ttf")); + PathBuilder::RoundingRadii radii; + radii.top_left = {50, 25}; + radii.top_right = {25, 50}; + radii.bottom_right = {50, 25}; + radii.bottom_left = {25, 50}; + + builder.AddRoundedRect(Rect::MakeXYWH(100, 100, 200, 200), radii); + builder.AddCircle({200, 200}, 50); + auto path = builder.TakePath(FillType::kOdd); + + canvas.DrawImage( + std::make_shared(CreateTextureForFixture("boston.jpg")), {10, 10}, + Paint{}); + canvas.DrawPath(std::move(path), paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderTextOutsideBoundaries) { +// Regression test for https://github.com/flutter/flutter/issues/134816. +// +// It should be possible to draw 3 lines, and not have an implicit close path. +TEST_P(AiksTest, CanDrawAnOpenPath) { Canvas canvas; - canvas.Translate({200, 150}); - - // Construct the text blob. - auto mapping = flutter::testing::OpenFixtureAsSkData("wtf.otf"); - ASSERT_NE(mapping, nullptr); - - Scalar font_size = 80; - sk_sp font_mgr = txt::GetDefaultFontManager(); - SkFont sk_font(font_mgr->makeFromData(mapping), font_size); - Paint text_paint; - text_paint.color = Color::Blue().WithAlpha(0.8); + // Starting at (50, 50), draw lines from: + // 1. (50, height) + // 2. (width, height) + // 3. (width, 50) + PathBuilder builder; + builder.MoveTo({50, 50}); + builder.LineTo({50, 100}); + builder.LineTo({100, 100}); + builder.LineTo({100, 50}); - struct { - Point position; - const char* text; - } text[] = {{Point(0, 0), "0F0F0F0"}, - {Point(1, 2), "789"}, - {Point(1, 3), "456"}, - {Point(1, 4), "123"}, - {Point(0, 6), "0F0F0F0"}}; - for (auto& t : text) { - canvas.Save(); - canvas.Translate(t.position * Point(font_size * 2, font_size * 1.1)); - { - auto blob = SkTextBlob::MakeFromString(t.text, sk_font); - ASSERT_NE(blob, nullptr); - auto frame = MakeTextFrameFromTextBlobSkia(blob); - canvas.DrawTextFrame(frame, Point(), text_paint); - } - canvas.Restore(); - } + Paint paint; + paint.color = Color::Red(); + paint.style = Paint::Style::kStroke; + paint.stroke_width = 10; + + canvas.DrawPath(builder.TakePath(), paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, TextRotated) { +TEST_P(AiksTest, CanDrawAnOpenPathThatIsntARect) { Canvas canvas; - canvas.Scale(GetContentScale()); - canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - canvas.Transform(Matrix(0.25, -0.3, 0, -0.002, // - 0, 0.5, 0, 0, // - 0, 0, 0.3, 0, // - 100, 100, 0, 1.3)); - ASSERT_TRUE(RenderTextInCanvasSkia( - GetContext(), canvas, "the quick brown fox jumped over the lazy dog!.?", - "Roboto-Regular.ttf")); + // Draw a stroked path that is explicitly closed to verify + // It doesn't become a rectangle. + PathBuilder builder; + builder.MoveTo({50, 50}); + builder.LineTo({520, 120}); + builder.LineTo({300, 310}); + builder.LineTo({100, 50}); + builder.Close(); + + Paint paint; + paint.color = Color::Red(); + paint.style = Paint::Style::kStroke; + paint.stroke_width = 10; + + canvas.DrawPath(builder.TakePath(), paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } @@ -2401,82 +2650,6 @@ TEST_P(AiksTest, FilledRoundRectsRenderCorrectly) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, GradientStrokesRenderCorrectly) { - // Compare with https://fiddle.skia.org/c/027392122bec8ac2b5d5de00a4b9bbe2 - auto callback = [&](AiksContext& renderer) -> std::optional { - static float scale = 3; - static bool add_circle_clip = true; - const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"}; - const Entity::TileMode tile_modes[] = { - Entity::TileMode::kClamp, Entity::TileMode::kRepeat, - Entity::TileMode::kMirror, Entity::TileMode::kDecal}; - static int selected_tile_mode = 0; - static float alpha = 1; - - ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); - ImGui::SliderFloat("Scale", &scale, 0, 6); - ImGui::Checkbox("Circle clip", &add_circle_clip); - ImGui::SliderFloat("Alpha", &alpha, 0, 1); - ImGui::Combo("Tile mode", &selected_tile_mode, tile_mode_names, - sizeof(tile_mode_names) / sizeof(char*)); - ImGui::End(); - - Canvas canvas; - canvas.Scale(GetContentScale()); - Paint paint; - paint.color = Color::White(); - canvas.DrawPaint(paint); - - paint.style = Paint::Style::kStroke; - paint.color = Color(1.0, 1.0, 1.0, alpha); - paint.stroke_width = 10; - auto tile_mode = tile_modes[selected_tile_mode]; - - std::vector colors = {Color{0.9568, 0.2627, 0.2118, 1.0}, - Color{0.1294, 0.5882, 0.9529, 1.0}}; - std::vector stops = {0.0, 1.0}; - - paint.color_source = ColorSource::MakeLinearGradient( - {0, 0}, {50, 50}, std::move(colors), std::move(stops), tile_mode, {}); - - Path path = PathBuilder{} - .MoveTo({20, 20}) - .QuadraticCurveTo({60, 20}, {60, 60}) - .Close() - .MoveTo({60, 20}) - .QuadraticCurveTo({60, 60}, {20, 60}) - .TakePath(); - - canvas.Scale(Vector2(scale, scale)); - - if (add_circle_clip) { - auto [handle_a, handle_b] = IMPELLER_PLAYGROUND_LINE( - Point(60, 300), Point(600, 300), 20, Color::Red(), Color::Red()); - - auto screen_to_canvas = canvas.GetCurrentTransform().Invert(); - Point point_a = screen_to_canvas * handle_a * GetContentScale(); - Point point_b = screen_to_canvas * handle_b * GetContentScale(); - - Point middle = (point_a + point_b) / 2; - auto radius = point_a.GetDistance(middle); - canvas.ClipPath(PathBuilder{}.AddCircle(middle, radius).TakePath()); - } - - for (auto join : {Join::kBevel, Join::kRound, Join::kMiter}) { - paint.stroke_join = join; - for (auto cap : {Cap::kButt, Cap::kSquare, Cap::kRound}) { - paint.stroke_cap = cap; - canvas.DrawPath(path.Clone(), paint); - canvas.Translate({80, 0}); - } - canvas.Translate({-240, 60}); - } - - return canvas.EndRecordingAsPicture(); - }; - - ASSERT_TRUE(OpenPlaygroundHere(callback)); -} TEST_P(AiksTest, CoverageOriginShouldBeAccountedForInSubpasses) { auto callback = [&](AiksContext& renderer) -> std::optional { @@ -3825,35 +3998,6 @@ TEST_P(AiksTest, TextForegroundShaderWithTransform) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanCanvasDrawPicture) { - Canvas subcanvas; - subcanvas.DrawRect(Rect::MakeLTRB(-100, -50, 100, 50), - {.color = Color::CornflowerBlue()}); - auto picture = subcanvas.EndRecordingAsPicture(); - - Canvas canvas; - canvas.Translate({200, 200}); - canvas.Rotate(Radians(kPi / 4)); - canvas.DrawPicture(picture); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanCanvasDrawPictureWithAdvancedBlend) { - Canvas subcanvas; - subcanvas.DrawRect( - Rect::MakeLTRB(-100, -50, 100, 50), - {.color = Color::CornflowerBlue(), .blend_mode = BlendMode::kColorDodge}); - auto picture = subcanvas.EndRecordingAsPicture(); - - Canvas canvas; - canvas.DrawPaint({.color = Color::Black()}); - canvas.DrawCircle(Point::MakeXY(150, 150), 25, {.color = Color::Red()}); - canvas.DrawPicture(picture); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - TEST_P(AiksTest, CanDrawMultiContourConvexPath) { PathBuilder builder = {}; for (auto i = 0; i < 10; i++) { @@ -3874,76 +4018,6 @@ TEST_P(AiksTest, CanDrawMultiContourConvexPath) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanCanvasDrawPictureWithBackdropFilter) { - Canvas subcanvas; - subcanvas.SaveLayer({}, {}, - ImageFilter::MakeBlur(Sigma(20.0), Sigma(20.0), - FilterContents::BlurStyle::kNormal, - Entity::TileMode::kDecal)); - auto image = std::make_shared(CreateTextureForFixture("kalimba.jpg")); - Paint paint; - paint.color = Color::Red().WithAlpha(0.5); - subcanvas.DrawImage(image, Point::MakeXY(100.0, 100.0), paint); - - auto picture = subcanvas.EndRecordingAsPicture(); - - Canvas canvas; - canvas.DrawPaint({.color = Color::Black()}); - canvas.DrawCircle(Point::MakeXY(150, 150), 25, {.color = Color::Red()}); - canvas.DrawPicture(picture); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, DrawPictureWithText) { - Canvas subcanvas; - ASSERT_TRUE(RenderTextInCanvasSkia( - GetContext(), subcanvas, - "the quick brown fox jumped over the lazy dog!.?", "Roboto-Regular.ttf")); - subcanvas.Translate({0, 10}); - subcanvas.Scale(Vector2(3, 3)); - ASSERT_TRUE(RenderTextInCanvasSkia( - GetContext(), subcanvas, - "the quick brown fox jumped over the very big lazy dog!.?", - "Roboto-Regular.ttf")); - auto picture = subcanvas.EndRecordingAsPicture(); - - Canvas canvas; - canvas.Scale(Vector2(.2, .2)); - canvas.Save(); - canvas.Translate({200, 200}); - canvas.Scale(Vector2(3.5, 3.5)); // The text must not be blurry after this. - canvas.DrawPicture(picture); - canvas.Restore(); - - canvas.Scale(Vector2(1.5, 1.5)); - ASSERT_TRUE(RenderTextInCanvasSkia( - GetContext(), canvas, - "the quick brown fox jumped over the smaller lazy dog!.?", - "Roboto-Regular.ttf")); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, DrawPictureClipped) { - Canvas subcanvas; - subcanvas.ClipRRect(Rect::MakeLTRB(100, 100, 400, 400), {15, 15}); - subcanvas.DrawPaint({.color = Color::Red()}); - auto picture = subcanvas.EndRecordingAsPicture(); - - Canvas canvas; - canvas.DrawPaint({.color = Color::CornflowerBlue()}); - - // Draw a red RRect via DrawPicture. - canvas.DrawPicture(picture); - - // Draw over the picture with a larger green rectangle, completely covering it - // up. - canvas.ClipRRect(Rect::MakeLTRB(100, 100, 400, 400).Expand(20), {15, 15}); - canvas.DrawPaint({.color = Color::Green()}); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - TEST_P(AiksTest, MatrixSaveLayerFilter) { Canvas canvas; canvas.DrawPaint({.color = Color::Black()}); @@ -4006,59 +4080,6 @@ TEST_P(AiksTest, SolidColorApplyColorFilter) { Color(0.433247, 0.879523, 0.825324, 0.75)); } -#define APPLY_COLOR_FILTER_GRADIENT_TEST(name) \ - TEST_P(AiksTest, name##GradientApplyColorFilter) { \ - auto contents = name##GradientContents(); \ - contents.SetColors({Color::CornflowerBlue().WithAlpha(0.75)}); \ - auto result = contents.ApplyColorFilter([](const Color& color) { \ - return color.Blend(Color::LimeGreen().WithAlpha(0.75), \ - BlendMode::kScreen); \ - }); \ - ASSERT_TRUE(result); \ - \ - std::vector expected = {Color(0.433247, 0.879523, 0.825324, 0.75)}; \ - ASSERT_COLORS_NEAR(contents.GetColors(), expected); \ - } - -APPLY_COLOR_FILTER_GRADIENT_TEST(Linear); -APPLY_COLOR_FILTER_GRADIENT_TEST(Radial); -APPLY_COLOR_FILTER_GRADIENT_TEST(Conical); -APPLY_COLOR_FILTER_GRADIENT_TEST(Sweep); - -TEST_P(AiksTest, DrawScaledTextWithPerspectiveNoSaveLayer) { - Canvas canvas; - // clang-format off - canvas.Transform(Matrix( - 2.000000, 0.000000, 0.000000, 0.000000, - 1.445767, 2.637070, -0.507928, 0.001524, - -2.451887, -0.534662, 0.861399, -0.002584, - 1063.481934, 1025.951416, -48.300270, 1.144901 - )); - // clang-format on - - ASSERT_TRUE(RenderTextInCanvasSkia(GetContext(), canvas, "Hello world", - "Roboto-Regular.ttf")); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, DrawScaledTextWithPerspectiveSaveLayer) { - Canvas canvas; - Paint save_paint; - canvas.SaveLayer(save_paint); - // clang-format off - canvas.Transform(Matrix( - 2.000000, 0.000000, 0.000000, 0.000000, - 1.445767, 2.637070, -0.507928, 0.001524, - -2.451887, -0.534662, 0.861399, -0.002584, - 1063.481934, 1025.951416, -48.300270, 1.144901 - )); - // clang-format on - - ASSERT_TRUE(RenderTextInCanvasSkia(GetContext(), canvas, "Hello world", - "Roboto-Regular.ttf")); -} - TEST_P(AiksTest, PipelineBlendSingleParameter) { Canvas canvas; @@ -4598,5 +4619,6 @@ TEST_P(AiksTest, GaussianBlurRotatedAndClipped) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +} // namespace unsorted } // namespace testing } // namespace impeller From f4f0163b3f568896ae949ee26430b12437f293b1 Mon Sep 17 00:00:00 2001 From: Aaron Clarke Date: Wed, 13 Dec 2023 16:45:56 -0800 Subject: [PATCH 2/4] added clips --- impeller/aiks/aiks_unittests.cc | 409 ++++++++++++++++---------------- 1 file changed, 206 insertions(+), 203 deletions(-) diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc index d4a98acd63569..804ea298e32d7 100644 --- a/impeller/aiks/aiks_unittests.cc +++ b/impeller/aiks/aiks_unittests.cc @@ -1329,124 +1329,8 @@ TEST_P(AiksTest, DrawScaledTextWithPerspectiveSaveLayer) { } // namespace text -namespace unsorted { - -TEST_P(AiksTest, RotateColorFilteredPath) { - Canvas canvas; - canvas.Concat(Matrix::MakeTranslation({300, 300})); - canvas.Concat(Matrix::MakeRotationZ(Radians(kPiOver2))); - auto arrow_stem = - PathBuilder{}.MoveTo({120, 190}).LineTo({120, 50}).TakePath(); - auto arrow_head = PathBuilder{} - .MoveTo({50, 120}) - .LineTo({120, 190}) - .LineTo({190, 120}) - .TakePath(); - auto paint = Paint{ - .stroke_width = 15.0, - .stroke_cap = Cap::kRound, - .stroke_join = Join::kRound, - .style = Paint::Style::kStroke, - .color_filter = - ColorFilter::MakeBlend(BlendMode::kSourceIn, Color::AliceBlue()), - }; - - canvas.DrawPath(std::move(arrow_stem), paint); - canvas.DrawPath(std::move(arrow_head), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanvasCTMCanBeUpdated) { - Canvas canvas; - Matrix identity; - ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), identity); - canvas.Translate(Size{100, 100}); - ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), - Matrix::MakeTranslation({100.0, 100.0, 0.0})); -} - -TEST_P(AiksTest, CanvasCanPushPopCTM) { - Canvas canvas; - ASSERT_EQ(canvas.GetSaveCount(), 1u); - ASSERT_EQ(canvas.Restore(), false); - - canvas.Translate(Size{100, 100}); - canvas.Save(); - ASSERT_EQ(canvas.GetSaveCount(), 2u); - ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), - Matrix::MakeTranslation({100.0, 100.0, 0.0})); - ASSERT_TRUE(canvas.Restore()); - ASSERT_EQ(canvas.GetSaveCount(), 1u); - ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), - Matrix::MakeTranslation({100.0, 100.0, 0.0})); -} - -TEST_P(AiksTest, CanRenderColoredRect) { - Canvas canvas; - Paint paint; - paint.color = Color::Blue(); - canvas.DrawPath(PathBuilder{} - .AddRect(Rect::MakeXYWH(100.0, 100.0, 100.0, 100.0)) - .TakePath(), - paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderColorFilterWithInvertColors) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.color_filter = - ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); - paint.invert_colors = true; - - canvas.DrawRect(Rect::MakeLTRB(0, 0, 100, 100), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderColorFilterWithInvertColorsDrawPaint) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.color_filter = - ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); - paint.invert_colors = true; - - canvas.DrawPaint(paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderStrokes) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.stroke_width = 20.0; - paint.style = Paint::Style::kStroke; - canvas.DrawPath(PathBuilder{}.AddLine({200, 100}, {800, 100}).TakePath(), - paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderCurvedStrokes) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.stroke_width = 25.0; - paint.style = Paint::Style::kStroke; - canvas.DrawPath(PathBuilder{}.AddCircle({500, 500}, 250).TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderThickCurvedStrokes) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.stroke_width = 100.0; - paint.style = Paint::Style::kStroke; - canvas.DrawPath(PathBuilder{}.AddCircle({100, 100}, 50).TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - +/// These tests check the visual results of clipping. +namespace clips { TEST_P(AiksTest, CanRenderClips) { Canvas canvas; Paint paint; @@ -1616,6 +1500,210 @@ TEST_P(AiksTest, ClipsUseCurrentTransform) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +TEST_P(AiksTest, ClipRectElidesNoOpClips) { + Canvas canvas(Rect::MakeXYWH(0, 0, 100, 100)); + canvas.ClipRect(Rect::MakeXYWH(0, 0, 100, 100)); + canvas.ClipRect(Rect::MakeXYWH(-100, -100, 300, 300)); + canvas.DrawPaint({.color = Color::Red(), .blend_mode = BlendMode::kSource}); + canvas.DrawPaint({.color = Color::CornflowerBlue().WithAlpha(0.75), + .blend_mode = BlendMode::kSourceOver}); + + Picture picture = canvas.EndRecordingAsPicture(); + auto expected = Color::Red().Blend(Color::CornflowerBlue().WithAlpha(0.75), + BlendMode::kSourceOver); + ASSERT_EQ(picture.pass->GetClearColor(), expected); + + std::shared_ptr spy = ContextSpy::Make(); + std::shared_ptr real_context = GetContext(); + std::shared_ptr mock_context = spy->MakeContext(real_context); + AiksContext renderer(mock_context, nullptr); + std::shared_ptr image = picture.ToImage(renderer, {300, 300}); + + ASSERT_EQ(spy->render_passes_.size(), 1llu); + std::shared_ptr render_pass = spy->render_passes_[0]; + ASSERT_EQ(render_pass->GetCommands().size(), 0llu); +} + +TEST_P(AiksTest, CanRenderClippedLayers) { + Canvas canvas; + + canvas.DrawPaint({.color = Color::White()}); + + // Draw a green circle on the screen. + { + // Increase the clip depth for the savelayer to contend with. + canvas.ClipPath(PathBuilder{}.AddCircle({100, 100}, 50).TakePath()); + + canvas.SaveLayer({}, Rect::MakeXYWH(50, 50, 100, 100)); + + // Fill the layer with white. + canvas.DrawRect(Rect::MakeSize(Size{400, 400}), {.color = Color::White()}); + // Fill the layer with green, but do so with a color blend that can't be + // collapsed into the parent pass. + // TODO(jonahwilliams): this blend mode was changed from color burn to + // hardlight to work around https://github.com/flutter/flutter/issues/136554 + // . + canvas.DrawRect( + Rect::MakeSize(Size{400, 400}), + {.color = Color::Green(), .blend_mode = BlendMode::kHardLight}); + } + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +// Regression test for https://github.com/flutter/flutter/issues/126701 . +TEST_P(AiksTest, CanRenderClippedRuntimeEffects) { + if (GetParam() != PlaygroundBackend::kMetal) { + GTEST_SKIP_("This backend doesn't support runtime effects."); + } + + auto runtime_stage = + OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr"); + ASSERT_TRUE(runtime_stage->IsDirty()); + + struct FragUniforms { + Vector2 iResolution; + Scalar iTime; + } frag_uniforms = {.iResolution = Vector2(400, 400), .iTime = 100.0}; + auto uniform_data = std::make_shared>(); + uniform_data->resize(sizeof(FragUniforms)); + memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms)); + + std::vector texture_inputs; + + Paint paint; + paint.color_source = ColorSource::MakeRuntimeEffect( + runtime_stage, uniform_data, texture_inputs); + + Canvas canvas; + canvas.Save(); + canvas.ClipRRect(Rect::MakeXYWH(0, 0, 400, 400), {10.0, 10.0}, + Entity::ClipOperation::kIntersect); + canvas.DrawRect(Rect::MakeXYWH(0, 0, 400, 400), paint); + canvas.Restore(); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} +} // namespace clips + +namespace unsorted { + +TEST_P(AiksTest, RotateColorFilteredPath) { + Canvas canvas; + canvas.Concat(Matrix::MakeTranslation({300, 300})); + canvas.Concat(Matrix::MakeRotationZ(Radians(kPiOver2))); + auto arrow_stem = + PathBuilder{}.MoveTo({120, 190}).LineTo({120, 50}).TakePath(); + auto arrow_head = PathBuilder{} + .MoveTo({50, 120}) + .LineTo({120, 190}) + .LineTo({190, 120}) + .TakePath(); + auto paint = Paint{ + .stroke_width = 15.0, + .stroke_cap = Cap::kRound, + .stroke_join = Join::kRound, + .style = Paint::Style::kStroke, + .color_filter = + ColorFilter::MakeBlend(BlendMode::kSourceIn, Color::AliceBlue()), + }; + + canvas.DrawPath(std::move(arrow_stem), paint); + canvas.DrawPath(std::move(arrow_head), paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanvasCTMCanBeUpdated) { + Canvas canvas; + Matrix identity; + ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), identity); + canvas.Translate(Size{100, 100}); + ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), + Matrix::MakeTranslation({100.0, 100.0, 0.0})); +} + +TEST_P(AiksTest, CanvasCanPushPopCTM) { + Canvas canvas; + ASSERT_EQ(canvas.GetSaveCount(), 1u); + ASSERT_EQ(canvas.Restore(), false); + + canvas.Translate(Size{100, 100}); + canvas.Save(); + ASSERT_EQ(canvas.GetSaveCount(), 2u); + ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), + Matrix::MakeTranslation({100.0, 100.0, 0.0})); + ASSERT_TRUE(canvas.Restore()); + ASSERT_EQ(canvas.GetSaveCount(), 1u); + ASSERT_MATRIX_NEAR(canvas.GetCurrentTransform(), + Matrix::MakeTranslation({100.0, 100.0, 0.0})); +} + +TEST_P(AiksTest, CanRenderColoredRect) { + Canvas canvas; + Paint paint; + paint.color = Color::Blue(); + canvas.DrawPath(PathBuilder{} + .AddRect(Rect::MakeXYWH(100.0, 100.0, 100.0, 100.0)) + .TakePath(), + paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanRenderColorFilterWithInvertColors) { + Canvas canvas; + Paint paint; + paint.color = Color::Red(); + paint.color_filter = + ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); + paint.invert_colors = true; + + canvas.DrawRect(Rect::MakeLTRB(0, 0, 100, 100), paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanRenderColorFilterWithInvertColorsDrawPaint) { + Canvas canvas; + Paint paint; + paint.color = Color::Red(); + paint.color_filter = + ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); + paint.invert_colors = true; + + canvas.DrawPaint(paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanRenderStrokes) { + Canvas canvas; + Paint paint; + paint.color = Color::Red(); + paint.stroke_width = 20.0; + paint.style = Paint::Style::kStroke; + canvas.DrawPath(PathBuilder{}.AddLine({200, 100}, {800, 100}).TakePath(), + paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanRenderCurvedStrokes) { + Canvas canvas; + Paint paint; + paint.color = Color::Red(); + paint.stroke_width = 25.0; + paint.style = Paint::Style::kStroke; + canvas.DrawPath(PathBuilder{}.AddCircle({500, 500}, 250).TakePath(), paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanRenderThickCurvedStrokes) { + Canvas canvas; + Paint paint; + paint.color = Color::Red(); + paint.stroke_width = 100.0; + paint.style = Paint::Style::kStroke; + canvas.DrawPath(PathBuilder{}.AddCircle({100, 100}, 50).TakePath(), paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + TEST_P(AiksTest, CanSaveLayerStandalone) { Canvas canvas; @@ -2773,33 +2861,6 @@ TEST_P(AiksTest, SiblingSaveLayerBoundsAreRespected) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderClippedLayers) { - Canvas canvas; - - canvas.DrawPaint({.color = Color::White()}); - - // Draw a green circle on the screen. - { - // Increase the clip depth for the savelayer to contend with. - canvas.ClipPath(PathBuilder{}.AddCircle({100, 100}, 50).TakePath()); - - canvas.SaveLayer({}, Rect::MakeXYWH(50, 50, 100, 100)); - - // Fill the layer with white. - canvas.DrawRect(Rect::MakeSize(Size{400, 400}), {.color = Color::White()}); - // Fill the layer with green, but do so with a color blend that can't be - // collapsed into the parent pass. - // TODO(jonahwilliams): this blend mode was changed from color burn to - // hardlight to work around https://github.com/flutter/flutter/issues/136554 - // . - canvas.DrawRect( - Rect::MakeSize(Size{400, 400}), - {.color = Color::Green(), .blend_mode = BlendMode::kHardLight}); - } - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - TEST_P(AiksTest, SaveLayerFiltersScaleWithTransform) { Canvas canvas; canvas.Scale(GetContentScale()); @@ -3061,30 +3122,6 @@ TEST_P(AiksTest, DrawRectAbsorbsClearsNegative) { ASSERT_EQ(render_pass->GetCommands().size(), 2llu); } -TEST_P(AiksTest, ClipRectElidesNoOpClips) { - Canvas canvas(Rect::MakeXYWH(0, 0, 100, 100)); - canvas.ClipRect(Rect::MakeXYWH(0, 0, 100, 100)); - canvas.ClipRect(Rect::MakeXYWH(-100, -100, 300, 300)); - canvas.DrawPaint({.color = Color::Red(), .blend_mode = BlendMode::kSource}); - canvas.DrawPaint({.color = Color::CornflowerBlue().WithAlpha(0.75), - .blend_mode = BlendMode::kSourceOver}); - - Picture picture = canvas.EndRecordingAsPicture(); - auto expected = Color::Red().Blend(Color::CornflowerBlue().WithAlpha(0.75), - BlendMode::kSourceOver); - ASSERT_EQ(picture.pass->GetClearColor(), expected); - - std::shared_ptr spy = ContextSpy::Make(); - std::shared_ptr real_context = GetContext(); - std::shared_ptr mock_context = spy->MakeContext(real_context); - AiksContext renderer(mock_context, nullptr); - std::shared_ptr image = picture.ToImage(renderer, {300, 300}); - - ASSERT_EQ(spy->render_passes_.size(), 1llu); - std::shared_ptr render_pass = spy->render_passes_[0]; - ASSERT_EQ(render_pass->GetCommands().size(), 0llu); -} - TEST_P(AiksTest, ClearColorOptimizationDoesNotApplyForBackdropFilters) { Canvas canvas; canvas.SaveLayer({}, std::nullopt, @@ -3774,40 +3811,6 @@ TEST_P(AiksTest, CanRenderForegroundAdvancedBlendWithMaskBlur) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -// Regression test for https://github.com/flutter/flutter/issues/126701 . -TEST_P(AiksTest, CanRenderClippedRuntimeEffects) { - if (GetParam() != PlaygroundBackend::kMetal) { - GTEST_SKIP_("This backend doesn't support runtime effects."); - } - - auto runtime_stage = - OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr"); - ASSERT_TRUE(runtime_stage->IsDirty()); - - struct FragUniforms { - Vector2 iResolution; - Scalar iTime; - } frag_uniforms = {.iResolution = Vector2(400, 400), .iTime = 100.0}; - auto uniform_data = std::make_shared>(); - uniform_data->resize(sizeof(FragUniforms)); - memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms)); - - std::vector texture_inputs; - - Paint paint; - paint.color_source = ColorSource::MakeRuntimeEffect( - runtime_stage, uniform_data, texture_inputs); - - Canvas canvas; - canvas.Save(); - canvas.ClipRRect(Rect::MakeXYWH(0, 0, 400, 400), {10.0, 10.0}, - Entity::ClipOperation::kIntersect); - canvas.DrawRect(Rect::MakeXYWH(0, 0, 400, 400), paint); - canvas.Restore(); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - TEST_P(AiksTest, DrawPaintTransformsBounds) { if (GetParam() != PlaygroundBackend::kMetal) { GTEST_SKIP_("This backend doesn't support runtime effects."); From 89addc6897b204139d2ece712df2cb5413462fd6 Mon Sep 17 00:00:00 2001 From: Aaron Clarke Date: Wed, 13 Dec 2023 16:46:14 -0800 Subject: [PATCH 3/4] format --- impeller/aiks/aiks_unittests.cc | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc index 804ea298e32d7..b93568d76483b 100644 --- a/impeller/aiks/aiks_unittests.cc +++ b/impeller/aiks/aiks_unittests.cc @@ -926,7 +926,6 @@ TEST_P(AiksTest, CanPictureConvertToImage) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } - TEST_P(AiksTest, CanCanvasDrawPicture) { Canvas subcanvas; subcanvas.DrawRect(Rect::MakeLTRB(-100, -50, 100, 50), @@ -996,7 +995,7 @@ TEST_P(AiksTest, DrawPictureClipped) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -} +} // namespace pictures /// These tests check the visual results of drawing text. namespace text { @@ -1189,8 +1188,8 @@ TEST_P(AiksTest, CanRenderEmojiTextFrame) { Canvas canvas; canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - ASSERT_TRUE(RenderTextInCanvasSkia(GetContext(), canvas, - "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 😊", kFontFixture)); + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), canvas, "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 😊", kFontFixture)); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } @@ -1211,9 +1210,9 @@ TEST_P(AiksTest, CanRenderEmojiTextFrameWithAlpha) { Canvas canvas; canvas.DrawPaint({.color = Color(0.1, 0.1, 0.1, 1.0)}); - ASSERT_TRUE(RenderTextInCanvasSkia(GetContext(), canvas, - "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 😊", kFontFixture, - {.color = Color::Black().WithAlpha(0.5)})); + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), canvas, "😀 😃 😄 😁 😆 😅 😂 🤣 🥲 😊", kFontFixture, + {.color = Color::Black().WithAlpha(0.5)})); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } @@ -2738,7 +2737,6 @@ TEST_P(AiksTest, FilledRoundRectsRenderCorrectly) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } - TEST_P(AiksTest, CoverageOriginShouldBeAccountedForInSubpasses) { auto callback = [&](AiksContext& renderer) -> std::optional { Canvas canvas; From 4349670fe7c22a5f6f3634cef8f80a66d99ae14c Mon Sep 17 00:00:00 2001 From: Aaron Clarke Date: Wed, 13 Dec 2023 17:07:57 -0800 Subject: [PATCH 4/4] added paths --- impeller/aiks/aiks_unittests.cc | 572 ++++++++++++++++---------------- 1 file changed, 288 insertions(+), 284 deletions(-) diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc index b93568d76483b..181bed827e9d8 100644 --- a/impeller/aiks/aiks_unittests.cc +++ b/impeller/aiks/aiks_unittests.cc @@ -1585,7 +1585,278 @@ TEST_P(AiksTest, CanRenderClippedRuntimeEffects) { } } // namespace clips -namespace unsorted { +/// These tests check the visual results of drawing paths. +namespace paths { +TEST_P(AiksTest, CanRenderStrokes) { + Canvas canvas; + Paint paint; + paint.color = Color::Red(); + paint.stroke_width = 20.0; + paint.style = Paint::Style::kStroke; + canvas.DrawPath(PathBuilder{}.AddLine({200, 100}, {800, 100}).TakePath(), + paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanRenderCurvedStrokes) { + Canvas canvas; + Paint paint; + paint.color = Color::Red(); + paint.stroke_width = 25.0; + paint.style = Paint::Style::kStroke; + canvas.DrawPath(PathBuilder{}.AddCircle({500, 500}, 250).TakePath(), paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanRenderThickCurvedStrokes) { + Canvas canvas; + Paint paint; + paint.color = Color::Red(); + paint.stroke_width = 100.0; + paint.style = Paint::Style::kStroke; + canvas.DrawPath(PathBuilder{}.AddCircle({100, 100}, 50).TakePath(), paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanRenderRoundedRectWithNonUniformRadii) { + Canvas canvas; + + Paint paint; + paint.color = Color::Red(); + + PathBuilder::RoundingRadii radii; + radii.top_left = {50, 25}; + radii.top_right = {25, 50}; + radii.bottom_right = {50, 25}; + radii.bottom_left = {25, 50}; + + auto path = PathBuilder{} + .AddRoundedRect(Rect::MakeXYWH(100, 100, 500, 500), radii) + .TakePath(); + + canvas.DrawPath(std::move(path), paint); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanRenderStrokePathThatEndsAtSharpTurn) { + Canvas canvas; + + Paint paint; + paint.color = Color::Red(); + paint.style = Paint::Style::kStroke; + paint.stroke_width = 200; + + Rect rect = Rect::MakeXYWH(100, 100, 200, 200); + PathBuilder builder; + builder.AddArc(rect, Degrees(0), Degrees(90), false); + + canvas.DrawPath(builder.TakePath(), paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanRenderStrokePathWithCubicLine) { + Canvas canvas; + + Paint paint; + paint.color = Color::Red(); + paint.style = Paint::Style::kStroke; + paint.stroke_width = 20; + + PathBuilder builder; + builder.AddCubicCurve({0, 200}, {50, 400}, {350, 0}, {400, 200}); + + canvas.DrawPath(builder.TakePath(), paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanRenderDifferencePaths) { + Canvas canvas; + + Paint paint; + paint.color = Color::Red(); + + PathBuilder builder; + + PathBuilder::RoundingRadii radii; + radii.top_left = {50, 25}; + radii.top_right = {25, 50}; + radii.bottom_right = {50, 25}; + radii.bottom_left = {25, 50}; + + builder.AddRoundedRect(Rect::MakeXYWH(100, 100, 200, 200), radii); + builder.AddCircle({200, 200}, 50); + auto path = builder.TakePath(FillType::kOdd); + + canvas.DrawImage( + std::make_shared(CreateTextureForFixture("boston.jpg")), {10, 10}, + Paint{}); + canvas.DrawPath(std::move(path), paint); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +// Regression test for https://github.com/flutter/flutter/issues/134816. +// +// It should be possible to draw 3 lines, and not have an implicit close path. +TEST_P(AiksTest, CanDrawAnOpenPath) { + Canvas canvas; + + // Starting at (50, 50), draw lines from: + // 1. (50, height) + // 2. (width, height) + // 3. (width, 50) + PathBuilder builder; + builder.MoveTo({50, 50}); + builder.LineTo({50, 100}); + builder.LineTo({100, 100}); + builder.LineTo({100, 50}); + + Paint paint; + paint.color = Color::Red(); + paint.style = Paint::Style::kStroke; + paint.stroke_width = 10; + + canvas.DrawPath(builder.TakePath(), paint); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanDrawAnOpenPathThatIsntARect) { + Canvas canvas; + + // Draw a stroked path that is explicitly closed to verify + // It doesn't become a rectangle. + PathBuilder builder; + builder.MoveTo({50, 50}); + builder.LineTo({520, 120}); + builder.LineTo({300, 310}); + builder.LineTo({100, 50}); + builder.Close(); + + Paint paint; + paint.color = Color::Red(); + paint.style = Paint::Style::kStroke; + paint.stroke_width = 10; + + canvas.DrawPath(builder.TakePath(), paint); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, SolidStrokesRenderCorrectly) { + // Compare with https://fiddle.skia.org/c/027392122bec8ac2b5d5de00a4b9bbe2 + auto callback = [&](AiksContext& renderer) -> std::optional { + static Color color = Color::Black().WithAlpha(0.5); + static float scale = 3; + static bool add_circle_clip = true; + + ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::ColorEdit4("Color", reinterpret_cast(&color)); + ImGui::SliderFloat("Scale", &scale, 0, 6); + ImGui::Checkbox("Circle clip", &add_circle_clip); + ImGui::End(); + + Canvas canvas; + canvas.Scale(GetContentScale()); + Paint paint; + + paint.color = Color::White(); + canvas.DrawPaint(paint); + + paint.color = color; + paint.style = Paint::Style::kStroke; + paint.stroke_width = 10; + + Path path = PathBuilder{} + .MoveTo({20, 20}) + .QuadraticCurveTo({60, 20}, {60, 60}) + .Close() + .MoveTo({60, 20}) + .QuadraticCurveTo({60, 60}, {20, 60}) + .TakePath(); + + canvas.Scale(Vector2(scale, scale)); + + if (add_circle_clip) { + auto [handle_a, handle_b] = IMPELLER_PLAYGROUND_LINE( + Point(60, 300), Point(600, 300), 20, Color::Red(), Color::Red()); + + auto screen_to_canvas = canvas.GetCurrentTransform().Invert(); + Point point_a = screen_to_canvas * handle_a * GetContentScale(); + Point point_b = screen_to_canvas * handle_b * GetContentScale(); + + Point middle = (point_a + point_b) / 2; + auto radius = point_a.GetDistance(middle); + canvas.ClipPath(PathBuilder{}.AddCircle(middle, radius).TakePath()); + } + + for (auto join : {Join::kBevel, Join::kRound, Join::kMiter}) { + paint.stroke_join = join; + for (auto cap : {Cap::kButt, Cap::kSquare, Cap::kRound}) { + paint.stroke_cap = cap; + canvas.DrawPath(path.Clone(), paint); + canvas.Translate({80, 0}); + } + canvas.Translate({-240, 60}); + } + + return canvas.EndRecordingAsPicture(); + }; + + ASSERT_TRUE(OpenPlaygroundHere(callback)); +} + +TEST_P(AiksTest, DrawRectStrokesRenderCorrectly) { + Canvas canvas; + Paint paint; + paint.color = Color::Red(); + paint.style = Paint::Style::kStroke; + paint.stroke_width = 10; + + canvas.Translate({100, 100}); + canvas.DrawPath( + PathBuilder{}.AddRect(Rect::MakeSize(Size{100, 100})).TakePath(), + {paint}); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, DrawRectStrokesWithBevelJoinRenderCorrectly) { + Canvas canvas; + Paint paint; + paint.color = Color::Red(); + paint.style = Paint::Style::kStroke; + paint.stroke_width = 10; + paint.stroke_join = Join::kBevel; + + canvas.Translate({100, 100}); + canvas.DrawPath( + PathBuilder{}.AddRect(Rect::MakeSize(Size{100, 100})).TakePath(), + {paint}); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, CanDrawMultiContourConvexPath) { + PathBuilder builder = {}; + for (auto i = 0; i < 10; i++) { + if (i % 2 == 0) { + builder.AddCircle(Point(100 + 50 * i, 100 + 50 * i), 100); + } else { + builder.MoveTo({100.f + 50.f * i - 100, 100.f + 50.f * i}); + builder.LineTo({100.f + 50.f * i, 100.f + 50.f * i - 100}); + builder.LineTo({100.f + 50.f * i - 100, 100.f + 50.f * i - 100}); + builder.Close(); + } + } + builder.SetConvexity(Convexity::kConvex); + + Canvas canvas; + canvas.DrawPath(builder.TakePath(), {.color = Color::Red().WithAlpha(0.4)}); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} TEST_P(AiksTest, RotateColorFilteredPath) { Canvas canvas; @@ -1612,6 +1883,10 @@ TEST_P(AiksTest, RotateColorFilteredPath) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +} // namespace paths + +namespace unsorted { + TEST_P(AiksTest, CanvasCTMCanBeUpdated) { Canvas canvas; Matrix identity; @@ -1651,55 +1926,24 @@ TEST_P(AiksTest, CanRenderColoredRect) { TEST_P(AiksTest, CanRenderColorFilterWithInvertColors) { Canvas canvas; Paint paint; - paint.color = Color::Red(); - paint.color_filter = - ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); - paint.invert_colors = true; - - canvas.DrawRect(Rect::MakeLTRB(0, 0, 100, 100), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderColorFilterWithInvertColorsDrawPaint) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.color_filter = - ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); - paint.invert_colors = true; - - canvas.DrawPaint(paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderStrokes) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.stroke_width = 20.0; - paint.style = Paint::Style::kStroke; - canvas.DrawPath(PathBuilder{}.AddLine({200, 100}, {800, 100}).TakePath(), - paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderCurvedStrokes) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.stroke_width = 25.0; - paint.style = Paint::Style::kStroke; - canvas.DrawPath(PathBuilder{}.AddCircle({500, 500}, 250).TakePath(), paint); + paint.color = Color::Red(); + paint.color_filter = + ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); + paint.invert_colors = true; + + canvas.DrawRect(Rect::MakeLTRB(0, 0, 100, 100), paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderThickCurvedStrokes) { +TEST_P(AiksTest, CanRenderColorFilterWithInvertColorsDrawPaint) { Canvas canvas; Paint paint; paint.color = Color::Red(); - paint.stroke_width = 100.0; - paint.style = Paint::Style::kStroke; - canvas.DrawPath(PathBuilder{}.AddCircle({100, 100}, 50).TakePath(), paint); + paint.color_filter = + ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Yellow()); + paint.invert_colors = true; + + canvas.DrawPaint(paint); ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } @@ -1914,132 +2158,6 @@ TEST_P(AiksTest, ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderRoundedRectWithNonUniformRadii) { - Canvas canvas; - - Paint paint; - paint.color = Color::Red(); - - PathBuilder::RoundingRadii radii; - radii.top_left = {50, 25}; - radii.top_right = {25, 50}; - radii.bottom_right = {50, 25}; - radii.bottom_left = {25, 50}; - - auto path = PathBuilder{} - .AddRoundedRect(Rect::MakeXYWH(100, 100, 500, 500), radii) - .TakePath(); - - canvas.DrawPath(std::move(path), paint); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderStrokePathThatEndsAtSharpTurn) { - Canvas canvas; - - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 200; - - Rect rect = Rect::MakeXYWH(100, 100, 200, 200); - PathBuilder builder; - builder.AddArc(rect, Degrees(0), Degrees(90), false); - - canvas.DrawPath(builder.TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderStrokePathWithCubicLine) { - Canvas canvas; - - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 20; - - PathBuilder builder; - builder.AddCubicCurve({0, 200}, {50, 400}, {350, 0}, {400, 200}); - - canvas.DrawPath(builder.TakePath(), paint); - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanRenderDifferencePaths) { - Canvas canvas; - - Paint paint; - paint.color = Color::Red(); - - PathBuilder builder; - - PathBuilder::RoundingRadii radii; - radii.top_left = {50, 25}; - radii.top_right = {25, 50}; - radii.bottom_right = {50, 25}; - radii.bottom_left = {25, 50}; - - builder.AddRoundedRect(Rect::MakeXYWH(100, 100, 200, 200), radii); - builder.AddCircle({200, 200}, 50); - auto path = builder.TakePath(FillType::kOdd); - - canvas.DrawImage( - std::make_shared(CreateTextureForFixture("boston.jpg")), {10, 10}, - Paint{}); - canvas.DrawPath(std::move(path), paint); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -// Regression test for https://github.com/flutter/flutter/issues/134816. -// -// It should be possible to draw 3 lines, and not have an implicit close path. -TEST_P(AiksTest, CanDrawAnOpenPath) { - Canvas canvas; - - // Starting at (50, 50), draw lines from: - // 1. (50, height) - // 2. (width, height) - // 3. (width, 50) - PathBuilder builder; - builder.MoveTo({50, 50}); - builder.LineTo({50, 100}); - builder.LineTo({100, 100}); - builder.LineTo({100, 50}); - - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 10; - - canvas.DrawPath(builder.TakePath(), paint); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, CanDrawAnOpenPathThatIsntARect) { - Canvas canvas; - - // Draw a stroked path that is explicitly closed to verify - // It doesn't become a rectangle. - PathBuilder builder; - builder.MoveTo({50, 50}); - builder.LineTo({520, 120}); - builder.LineTo({300, 310}); - builder.LineTo({100, 50}); - builder.Close(); - - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 10; - - canvas.DrawPath(builder.TakePath(), paint); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - TEST_P(AiksTest, CanDrawPaint) { Canvas canvas; canvas.Scale(Vector2(0.2, 0.2)); @@ -2348,69 +2466,6 @@ TEST_P(AiksTest, TransformMultipliesCorrectly) { // clang-format on } -TEST_P(AiksTest, SolidStrokesRenderCorrectly) { - // Compare with https://fiddle.skia.org/c/027392122bec8ac2b5d5de00a4b9bbe2 - auto callback = [&](AiksContext& renderer) -> std::optional { - static Color color = Color::Black().WithAlpha(0.5); - static float scale = 3; - static bool add_circle_clip = true; - - ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); - ImGui::ColorEdit4("Color", reinterpret_cast(&color)); - ImGui::SliderFloat("Scale", &scale, 0, 6); - ImGui::Checkbox("Circle clip", &add_circle_clip); - ImGui::End(); - - Canvas canvas; - canvas.Scale(GetContentScale()); - Paint paint; - - paint.color = Color::White(); - canvas.DrawPaint(paint); - - paint.color = color; - paint.style = Paint::Style::kStroke; - paint.stroke_width = 10; - - Path path = PathBuilder{} - .MoveTo({20, 20}) - .QuadraticCurveTo({60, 20}, {60, 60}) - .Close() - .MoveTo({60, 20}) - .QuadraticCurveTo({60, 60}, {20, 60}) - .TakePath(); - - canvas.Scale(Vector2(scale, scale)); - - if (add_circle_clip) { - auto [handle_a, handle_b] = IMPELLER_PLAYGROUND_LINE( - Point(60, 300), Point(600, 300), 20, Color::Red(), Color::Red()); - - auto screen_to_canvas = canvas.GetCurrentTransform().Invert(); - Point point_a = screen_to_canvas * handle_a * GetContentScale(); - Point point_b = screen_to_canvas * handle_b * GetContentScale(); - - Point middle = (point_a + point_b) / 2; - auto radius = point_a.GetDistance(middle); - canvas.ClipPath(PathBuilder{}.AddCircle(middle, radius).TakePath()); - } - - for (auto join : {Join::kBevel, Join::kRound, Join::kMiter}) { - paint.stroke_join = join; - for (auto cap : {Cap::kButt, Cap::kSquare, Cap::kRound}) { - paint.stroke_cap = cap; - canvas.DrawPath(path.Clone(), paint); - canvas.Translate({80, 0}); - } - canvas.Translate({-240, 60}); - } - - return canvas.EndRecordingAsPicture(); - }; - - ASSERT_TRUE(OpenPlaygroundHere(callback)); -} - TEST_P(AiksTest, DrawLinesRenderCorrectly) { Canvas canvas; canvas.Scale(GetContentScale()); @@ -2774,37 +2829,6 @@ TEST_P(AiksTest, CoverageOriginShouldBeAccountedForInSubpasses) { ASSERT_TRUE(OpenPlaygroundHere(callback)); } -TEST_P(AiksTest, DrawRectStrokesRenderCorrectly) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 10; - - canvas.Translate({100, 100}); - canvas.DrawPath( - PathBuilder{}.AddRect(Rect::MakeSize(Size{100, 100})).TakePath(), - {paint}); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - -TEST_P(AiksTest, DrawRectStrokesWithBevelJoinRenderCorrectly) { - Canvas canvas; - Paint paint; - paint.color = Color::Red(); - paint.style = Paint::Style::kStroke; - paint.stroke_width = 10; - paint.stroke_join = Join::kBevel; - - canvas.Translate({100, 100}); - canvas.DrawPath( - PathBuilder{}.AddRect(Rect::MakeSize(Size{100, 100})).TakePath(), - {paint}); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - TEST_P(AiksTest, SaveLayerDrawsBehindSubsequentEntities) { // Compare with https://fiddle.skia.org/c/9e03de8567ffb49e7e83f53b64bcf636 Canvas canvas; @@ -3999,26 +4023,6 @@ TEST_P(AiksTest, TextForegroundShaderWithTransform) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanDrawMultiContourConvexPath) { - PathBuilder builder = {}; - for (auto i = 0; i < 10; i++) { - if (i % 2 == 0) { - builder.AddCircle(Point(100 + 50 * i, 100 + 50 * i), 100); - } else { - builder.MoveTo({100.f + 50.f * i - 100, 100.f + 50.f * i}); - builder.LineTo({100.f + 50.f * i, 100.f + 50.f * i - 100}); - builder.LineTo({100.f + 50.f * i - 100, 100.f + 50.f * i - 100}); - builder.Close(); - } - } - builder.SetConvexity(Convexity::kConvex); - - Canvas canvas; - canvas.DrawPath(builder.TakePath(), {.color = Color::Red().WithAlpha(0.4)}); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - TEST_P(AiksTest, MatrixSaveLayerFilter) { Canvas canvas; canvas.DrawPaint({.color = Color::Black()});