| // Copyright 2013 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include <algorithm> | |
| #include <cstring> | |
| #include <memory> | |
| #include <optional> | |
| #include <utility> | |
| #include <vector> | |
| #include "flutter/display_list/testing/dl_test_snippets.h" | |
| #include "fml/logging.h" | |
| #include "gtest/gtest.h" | |
| #include "impeller/core/formats.h" | |
| #include "impeller/core/texture_descriptor.h" | |
| #include "impeller/entity/contents/atlas_contents.h" | |
| #include "impeller/entity/contents/clip_contents.h" | |
| #include "impeller/entity/contents/conical_gradient_contents.h" | |
| #include "impeller/entity/contents/content_context.h" | |
| #include "impeller/entity/contents/contents.h" | |
| #include "impeller/entity/contents/filters/color_filter_contents.h" | |
| #include "impeller/entity/contents/filters/filter_contents.h" | |
| #include "impeller/entity/contents/filters/gaussian_blur_filter_contents.h" | |
| #include "impeller/entity/contents/filters/inputs/filter_input.h" | |
| #include "impeller/entity/contents/linear_gradient_contents.h" | |
| #include "impeller/entity/contents/radial_gradient_contents.h" | |
| #include "impeller/entity/contents/runtime_effect_contents.h" | |
| #include "impeller/entity/contents/solid_color_contents.h" | |
| #include "impeller/entity/contents/solid_rrect_blur_contents.h" | |
| #include "impeller/entity/contents/text_contents.h" | |
| #include "impeller/entity/contents/texture_contents.h" | |
| #include "impeller/entity/contents/tiled_texture_contents.h" | |
| #include "impeller/entity/entity.h" | |
| #include "impeller/entity/entity_pass.h" | |
| #include "impeller/entity/entity_pass_delegate.h" | |
| #include "impeller/entity/entity_playground.h" | |
| #include "impeller/entity/geometry/geometry.h" | |
| #include "impeller/entity/geometry/point_field_geometry.h" | |
| #include "impeller/entity/geometry/stroke_path_geometry.h" | |
| #include "impeller/entity/render_target_cache.h" | |
| #include "impeller/geometry/color.h" | |
| #include "impeller/geometry/geometry_asserts.h" | |
| #include "impeller/geometry/path_builder.h" | |
| #include "impeller/geometry/point.h" | |
| #include "impeller/geometry/sigma.h" | |
| #include "impeller/geometry/vector.h" | |
| #include "impeller/playground/playground.h" | |
| #include "impeller/playground/widgets.h" | |
| #include "impeller/renderer/command.h" | |
| #include "impeller/renderer/pipeline_descriptor.h" | |
| #include "impeller/renderer/render_pass.h" | |
| #include "impeller/renderer/render_target.h" | |
| #include "impeller/renderer/testing/mocks.h" | |
| #include "impeller/renderer/vertex_buffer_builder.h" | |
| #include "impeller/typographer/backends/skia/text_frame_skia.h" | |
| #include "impeller/typographer/backends/skia/typographer_context_skia.h" | |
| #include "third_party/imgui/imgui.h" | |
| #include "third_party/skia/include/core/SkTextBlob.h" | |
| // TODO(zanderso): https://github.com/flutter/flutter/issues/127701 | |
| // NOLINTBEGIN(bugprone-unchecked-optional-access) | |
| namespace impeller { | |
| namespace testing { | |
| using EntityTest = EntityPlayground; | |
| INSTANTIATE_PLAYGROUND_SUITE(EntityTest); | |
| TEST_P(EntityTest, CanCreateEntity) { | |
| Entity entity; | |
| ASSERT_TRUE(entity.GetTransform().IsIdentity()); | |
| } | |
| class TestPassDelegate final : public EntityPassDelegate { | |
| public: | |
| explicit TestPassDelegate(bool collapse = false) : collapse_(collapse) {} | |
| // |EntityPassDelegate| | |
| ~TestPassDelegate() override = default; | |
| // |EntityPassDelgate| | |
| bool CanElide() override { return false; } | |
| // |EntityPassDelgate| | |
| bool CanCollapseIntoParentPass(EntityPass* entity_pass) override { | |
| return collapse_; | |
| } | |
| // |EntityPassDelgate| | |
| std::shared_ptr<Contents> CreateContentsForSubpassTarget( | |
| std::shared_ptr<Texture> target, | |
| const Matrix& transform) override { | |
| return nullptr; | |
| } | |
| // |EntityPassDelegate| | |
| std::shared_ptr<FilterContents> WithImageFilter( | |
| const FilterInput::Variant& input, | |
| const Matrix& effect_transform) const override { | |
| return nullptr; | |
| } | |
| private: | |
| const std::optional<Rect> coverage_; | |
| const bool collapse_; | |
| }; | |
| auto CreatePassWithRectPath( | |
| Rect rect, | |
| std::optional<Rect> bounds_hint, | |
| ContentBoundsPromise bounds_promise = ContentBoundsPromise::kUnknown, | |
| bool collapse = false) { | |
| auto subpass = std::make_unique<EntityPass>(); | |
| Entity entity; | |
| entity.SetContents(SolidColorContents::Make( | |
| PathBuilder{}.AddRect(rect).TakePath(), Color::Red())); | |
| subpass->AddEntity(std::move(entity)); | |
| subpass->SetDelegate(std::make_unique<TestPassDelegate>(collapse)); | |
| subpass->SetBoundsLimit(bounds_hint, bounds_promise); | |
| return subpass; | |
| } | |
| TEST_P(EntityTest, EntityPassRespectsUntrustedSubpassBoundsLimit) { | |
| EntityPass pass; | |
| auto subpass0 = CreatePassWithRectPath(Rect::MakeLTRB(0, 0, 100, 100), | |
| Rect::MakeLTRB(50, 50, 150, 150)); | |
| auto subpass1 = CreatePassWithRectPath(Rect::MakeLTRB(500, 500, 1000, 1000), | |
| Rect::MakeLTRB(800, 800, 900, 900)); | |
| auto subpass0_coverage = | |
| pass.GetSubpassCoverage(*subpass0.get(), std::nullopt); | |
| ASSERT_TRUE(subpass0_coverage.has_value()); | |
| ASSERT_RECT_NEAR(subpass0_coverage.value(), Rect::MakeLTRB(50, 50, 100, 100)); | |
| auto subpass1_coverage = | |
| pass.GetSubpassCoverage(*subpass1.get(), std::nullopt); | |
| ASSERT_TRUE(subpass1_coverage.has_value()); | |
| ASSERT_RECT_NEAR(subpass1_coverage.value(), | |
| Rect::MakeLTRB(800, 800, 900, 900)); | |
| pass.AddSubpass(std::move(subpass0)); | |
| pass.AddSubpass(std::move(subpass1)); | |
| auto coverage = pass.GetElementsCoverage(std::nullopt); | |
| ASSERT_TRUE(coverage.has_value()); | |
| ASSERT_RECT_NEAR(coverage.value(), Rect::MakeLTRB(50, 50, 900, 900)); | |
| } | |
| TEST_P(EntityTest, EntityPassTrustsSnugSubpassBoundsLimit) { | |
| EntityPass pass; | |
| auto subpass0 = // | |
| CreatePassWithRectPath(Rect::MakeLTRB(10, 10, 90, 90), | |
| Rect::MakeLTRB(5, 5, 95, 95), | |
| ContentBoundsPromise::kContainsContents); | |
| auto subpass1 = // | |
| CreatePassWithRectPath(Rect::MakeLTRB(500, 500, 1000, 1000), | |
| Rect::MakeLTRB(495, 495, 1005, 1005), | |
| ContentBoundsPromise::kContainsContents); | |
| auto subpass0_coverage = | |
| pass.GetSubpassCoverage(*subpass0.get(), std::nullopt); | |
| EXPECT_TRUE(subpass0_coverage.has_value()); | |
| // Result should be the overridden bounds | |
| // (we lied about them being snug, but the property is respected) | |
| EXPECT_RECT_NEAR(subpass0_coverage.value(), Rect::MakeLTRB(5, 5, 95, 95)); | |
| auto subpass1_coverage = | |
| pass.GetSubpassCoverage(*subpass1.get(), std::nullopt); | |
| EXPECT_TRUE(subpass1_coverage.has_value()); | |
| // Result should be the overridden bounds | |
| // (we lied about them being snug, but the property is respected) | |
| EXPECT_RECT_NEAR(subpass1_coverage.value(), | |
| Rect::MakeLTRB(495, 495, 1005, 1005)); | |
| pass.AddSubpass(std::move(subpass0)); | |
| pass.AddSubpass(std::move(subpass1)); | |
| auto coverage = pass.GetElementsCoverage(std::nullopt); | |
| EXPECT_TRUE(coverage.has_value()); | |
| // This result should be the union of the overridden bounds | |
| EXPECT_RECT_NEAR(coverage.value(), Rect::MakeLTRB(5, 5, 1005, 1005)); | |
| } | |
| TEST_P(EntityTest, EntityPassCanMergeSubpassIntoParent) { | |
| // Both a red and a blue box should appear if the pass merging has worked | |
| // correctly. | |
| EntityPass pass; | |
| auto subpass = CreatePassWithRectPath(Rect::MakeLTRB(0, 0, 100, 100), | |
| Rect::MakeLTRB(50, 50, 150, 150), | |
| ContentBoundsPromise::kUnknown, true); | |
| pass.AddSubpass(std::move(subpass)); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| auto contents = std::make_unique<SolidColorContents>(); | |
| contents->SetGeometry(Geometry::MakeRect(Rect::MakeLTRB(100, 100, 200, 200))); | |
| contents->SetColor(Color::Blue()); | |
| entity.SetContents(std::move(contents)); | |
| pass.AddEntity(std::move(entity)); | |
| ASSERT_TRUE(OpenPlaygroundHere(pass)); | |
| } | |
| TEST_P(EntityTest, EntityPassCoverageRespectsCoverageLimit) { | |
| // Rect is drawn entirely in negative area. | |
| auto pass = CreatePassWithRectPath(Rect::MakeLTRB(-200, -200, -100, -100), | |
| std::nullopt); | |
| // Without coverage limit. | |
| { | |
| auto pass_coverage = pass->GetElementsCoverage(std::nullopt); | |
| ASSERT_TRUE(pass_coverage.has_value()); | |
| ASSERT_RECT_NEAR(pass_coverage.value(), | |
| Rect::MakeLTRB(-200, -200, -100, -100)); | |
| } | |
| // With limit that doesn't overlap. | |
| { | |
| auto pass_coverage = | |
| pass->GetElementsCoverage(Rect::MakeLTRB(0, 0, 100, 100)); | |
| ASSERT_FALSE(pass_coverage.has_value()); | |
| } | |
| // With limit that partially overlaps. | |
| { | |
| auto pass_coverage = | |
| pass->GetElementsCoverage(Rect::MakeLTRB(-150, -150, 0, 0)); | |
| ASSERT_TRUE(pass_coverage.has_value()); | |
| ASSERT_RECT_NEAR(pass_coverage.value(), | |
| Rect::MakeLTRB(-150, -150, -100, -100)); | |
| } | |
| } | |
| TEST_P(EntityTest, FilterCoverageRespectsCropRect) { | |
| auto image = CreateTextureForFixture("boston.jpg"); | |
| auto filter = ColorFilterContents::MakeBlend(BlendMode::kSoftLight, | |
| FilterInput::Make({image})); | |
| // Without the crop rect (default behavior). | |
| { | |
| auto actual = filter->GetCoverage({}); | |
| auto expected = Rect::MakeSize(image->GetSize()); | |
| ASSERT_TRUE(actual.has_value()); | |
| ASSERT_RECT_NEAR(actual.value(), expected); | |
| } | |
| // With the crop rect. | |
| { | |
| auto expected = Rect::MakeLTRB(50, 50, 100, 100); | |
| filter->SetCoverageHint(expected); | |
| auto actual = filter->GetCoverage({}); | |
| ASSERT_TRUE(actual.has_value()); | |
| ASSERT_RECT_NEAR(actual.value(), expected); | |
| } | |
| } | |
| TEST_P(EntityTest, CanDrawRect) { | |
| auto contents = std::make_shared<SolidColorContents>(); | |
| contents->SetGeometry(Geometry::MakeRect(Rect::MakeXYWH(100, 100, 100, 100))); | |
| contents->SetColor(Color::Red()); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| entity.SetContents(contents); | |
| ASSERT_TRUE(OpenPlaygroundHere(std::move(entity))); | |
| } | |
| TEST_P(EntityTest, CanDrawRRect) { | |
| auto contents = std::make_shared<SolidColorContents>(); | |
| auto path = PathBuilder{} | |
| .SetConvexity(Convexity::kConvex) | |
| .AddRoundedRect(Rect::MakeXYWH(100, 100, 100, 100), 10.0) | |
| .TakePath(); | |
| contents->SetGeometry(Geometry::MakeFillPath(path)); | |
| contents->SetColor(Color::Red()); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| entity.SetContents(contents); | |
| ASSERT_TRUE(OpenPlaygroundHere(std::move(entity))); | |
| } | |
| TEST_P(EntityTest, GeometryBoundsAreTransformed) { | |
| auto geometry = Geometry::MakeRect(Rect::MakeXYWH(100, 100, 100, 100)); | |
| auto transform = Matrix::MakeScale({2.0, 2.0, 2.0}); | |
| ASSERT_RECT_NEAR(geometry->GetCoverage(transform).value(), | |
| Rect::MakeXYWH(200, 200, 200, 200)); | |
| } | |
| TEST_P(EntityTest, ThreeStrokesInOnePath) { | |
| Path path = PathBuilder{} | |
| .MoveTo({100, 100}) | |
| .LineTo({100, 200}) | |
| .MoveTo({100, 300}) | |
| .LineTo({100, 400}) | |
| .MoveTo({100, 500}) | |
| .LineTo({100, 600}) | |
| .TakePath(); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| auto contents = std::make_unique<SolidColorContents>(); | |
| contents->SetGeometry(Geometry::MakeStrokePath(path, 5.0)); | |
| contents->SetColor(Color::Red()); | |
| entity.SetContents(std::move(contents)); | |
| ASSERT_TRUE(OpenPlaygroundHere(std::move(entity))); | |
| } | |
| TEST_P(EntityTest, StrokeWithTextureContents) { | |
| auto bridge = CreateTextureForFixture("bay_bridge.jpg"); | |
| Path path = PathBuilder{} | |
| .MoveTo({100, 100}) | |
| .LineTo({100, 200}) | |
| .MoveTo({100, 300}) | |
| .LineTo({100, 400}) | |
| .MoveTo({100, 500}) | |
| .LineTo({100, 600}) | |
| .TakePath(); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| auto contents = std::make_unique<TiledTextureContents>(); | |
| contents->SetGeometry(Geometry::MakeStrokePath(path, 100.0)); | |
| contents->SetTexture(bridge); | |
| contents->SetTileModes(Entity::TileMode::kClamp, Entity::TileMode::kClamp); | |
| entity.SetContents(std::move(contents)); | |
| ASSERT_TRUE(OpenPlaygroundHere(std::move(entity))); | |
| } | |
| TEST_P(EntityTest, TriangleInsideASquare) { | |
| auto callback = [&](ContentContext& context, RenderPass& pass) { | |
| Point offset(100, 100); | |
| static PlaygroundPoint point_a(Point(10, 10) + offset, 20, Color::White()); | |
| Point a = DrawPlaygroundPoint(point_a); | |
| static PlaygroundPoint point_b(Point(210, 10) + offset, 20, Color::White()); | |
| Point b = DrawPlaygroundPoint(point_b); | |
| static PlaygroundPoint point_c(Point(210, 210) + offset, 20, | |
| Color::White()); | |
| Point c = DrawPlaygroundPoint(point_c); | |
| static PlaygroundPoint point_d(Point(10, 210) + offset, 20, Color::White()); | |
| Point d = DrawPlaygroundPoint(point_d); | |
| static PlaygroundPoint point_e(Point(50, 50) + offset, 20, Color::White()); | |
| Point e = DrawPlaygroundPoint(point_e); | |
| static PlaygroundPoint point_f(Point(100, 50) + offset, 20, Color::White()); | |
| Point f = DrawPlaygroundPoint(point_f); | |
| static PlaygroundPoint point_g(Point(50, 150) + offset, 20, Color::White()); | |
| Point g = DrawPlaygroundPoint(point_g); | |
| Path path = PathBuilder{} | |
| .MoveTo(a) | |
| .LineTo(b) | |
| .LineTo(c) | |
| .LineTo(d) | |
| .Close() | |
| .MoveTo(e) | |
| .LineTo(f) | |
| .LineTo(g) | |
| .Close() | |
| .TakePath(); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| auto contents = std::make_unique<SolidColorContents>(); | |
| contents->SetGeometry(Geometry::MakeStrokePath(path, 20.0)); | |
| contents->SetColor(Color::Red()); | |
| entity.SetContents(std::move(contents)); | |
| return entity.Render(context, pass); | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, StrokeCapAndJoinTest) { | |
| const Point padding(300, 250); | |
| const Point margin(140, 180); | |
| auto callback = [&](ContentContext& context, RenderPass& pass) { | |
| // Slightly above sqrt(2) by default, so that right angles are just below | |
| // the limit and acute angles are over the limit (causing them to get | |
| // beveled). | |
| static Scalar miter_limit = 1.41421357; | |
| static Scalar width = 30; | |
| ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); | |
| { | |
| ImGui::SliderFloat("Miter limit", &miter_limit, 0, 30); | |
| ImGui::SliderFloat("Stroke width", &width, 0, 100); | |
| if (ImGui::Button("Reset")) { | |
| miter_limit = 1.41421357; | |
| width = 30; | |
| } | |
| } | |
| ImGui::End(); | |
| auto world_matrix = Matrix::MakeScale(GetContentScale()); | |
| auto render_path = [width = width, &context, &pass, &world_matrix]( | |
| const Path& path, Cap cap, Join join) { | |
| auto contents = std::make_unique<SolidColorContents>(); | |
| contents->SetGeometry( | |
| Geometry::MakeStrokePath(path, width, miter_limit, cap, join)); | |
| contents->SetColor(Color::Red()); | |
| Entity entity; | |
| entity.SetTransform(world_matrix); | |
| entity.SetContents(std::move(contents)); | |
| auto coverage = entity.GetCoverage(); | |
| if (coverage.has_value()) { | |
| auto bounds_contents = std::make_unique<SolidColorContents>(); | |
| bounds_contents->SetGeometry(Geometry::MakeFillPath( | |
| PathBuilder{}.AddRect(entity.GetCoverage().value()).TakePath())); | |
| bounds_contents->SetColor(Color::Green().WithAlpha(0.5)); | |
| Entity bounds_entity; | |
| bounds_entity.SetContents(std::move(bounds_contents)); | |
| bounds_entity.Render(context, pass); | |
| } | |
| entity.Render(context, pass); | |
| }; | |
| const Point a_def(0, 0), b_def(0, 100), c_def(150, 0), d_def(150, -100), | |
| e_def(75, 75); | |
| const Scalar r = 30; | |
| // Cap::kButt demo. | |
| { | |
| Point off = Point(0, 0) * padding + margin; | |
| static PlaygroundPoint point_a(off + a_def, r, Color::Black()); | |
| static PlaygroundPoint point_b(off + b_def, r, Color::White()); | |
| auto [a, b] = DrawPlaygroundLine(point_a, point_b); | |
| static PlaygroundPoint point_c(off + c_def, r, Color::Black()); | |
| static PlaygroundPoint point_d(off + d_def, r, Color::White()); | |
| auto [c, d] = DrawPlaygroundLine(point_c, point_d); | |
| render_path(PathBuilder{}.AddCubicCurve(a, b, d, c).TakePath(), | |
| Cap::kButt, Join::kBevel); | |
| } | |
| // Cap::kSquare demo. | |
| { | |
| Point off = Point(1, 0) * padding + margin; | |
| static PlaygroundPoint point_a(off + a_def, r, Color::Black()); | |
| static PlaygroundPoint point_b(off + b_def, r, Color::White()); | |
| auto [a, b] = DrawPlaygroundLine(point_a, point_b); | |
| static PlaygroundPoint point_c(off + c_def, r, Color::Black()); | |
| static PlaygroundPoint point_d(off + d_def, r, Color::White()); | |
| auto [c, d] = DrawPlaygroundLine(point_c, point_d); | |
| render_path(PathBuilder{}.AddCubicCurve(a, b, d, c).TakePath(), | |
| Cap::kSquare, Join::kBevel); | |
| } | |
| // Cap::kRound demo. | |
| { | |
| Point off = Point(2, 0) * padding + margin; | |
| static PlaygroundPoint point_a(off + a_def, r, Color::Black()); | |
| static PlaygroundPoint point_b(off + b_def, r, Color::White()); | |
| auto [a, b] = DrawPlaygroundLine(point_a, point_b); | |
| static PlaygroundPoint point_c(off + c_def, r, Color::Black()); | |
| static PlaygroundPoint point_d(off + d_def, r, Color::White()); | |
| auto [c, d] = DrawPlaygroundLine(point_c, point_d); | |
| render_path(PathBuilder{}.AddCubicCurve(a, b, d, c).TakePath(), | |
| Cap::kRound, Join::kBevel); | |
| } | |
| // Join::kBevel demo. | |
| { | |
| Point off = Point(0, 1) * padding + margin; | |
| static PlaygroundPoint point_a = | |
| PlaygroundPoint(off + a_def, r, Color::White()); | |
| static PlaygroundPoint point_b = | |
| PlaygroundPoint(off + e_def, r, Color::White()); | |
| static PlaygroundPoint point_c = | |
| PlaygroundPoint(off + c_def, r, Color::White()); | |
| Point a = DrawPlaygroundPoint(point_a); | |
| Point b = DrawPlaygroundPoint(point_b); | |
| Point c = DrawPlaygroundPoint(point_c); | |
| render_path( | |
| PathBuilder{}.MoveTo(a).LineTo(b).LineTo(c).Close().TakePath(), | |
| Cap::kButt, Join::kBevel); | |
| } | |
| // Join::kMiter demo. | |
| { | |
| Point off = Point(1, 1) * padding + margin; | |
| static PlaygroundPoint point_a(off + a_def, r, Color::White()); | |
| static PlaygroundPoint point_b(off + e_def, r, Color::White()); | |
| static PlaygroundPoint point_c(off + c_def, r, Color::White()); | |
| Point a = DrawPlaygroundPoint(point_a); | |
| Point b = DrawPlaygroundPoint(point_b); | |
| Point c = DrawPlaygroundPoint(point_c); | |
| render_path( | |
| PathBuilder{}.MoveTo(a).LineTo(b).LineTo(c).Close().TakePath(), | |
| Cap::kButt, Join::kMiter); | |
| } | |
| // Join::kRound demo. | |
| { | |
| Point off = Point(2, 1) * padding + margin; | |
| static PlaygroundPoint point_a(off + a_def, r, Color::White()); | |
| static PlaygroundPoint point_b(off + e_def, r, Color::White()); | |
| static PlaygroundPoint point_c(off + c_def, r, Color::White()); | |
| Point a = DrawPlaygroundPoint(point_a); | |
| Point b = DrawPlaygroundPoint(point_b); | |
| Point c = DrawPlaygroundPoint(point_c); | |
| render_path( | |
| PathBuilder{}.MoveTo(a).LineTo(b).LineTo(c).Close().TakePath(), | |
| Cap::kButt, Join::kRound); | |
| } | |
| return true; | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, CubicCurveTest) { | |
| // Compare with https://fiddle.skia.org/c/b3625f26122c9de7afe7794fcf25ead3 | |
| Path path = | |
| PathBuilder{} | |
| .MoveTo({237.164, 125.003}) | |
| .CubicCurveTo({236.709, 125.184}, {236.262, 125.358}, | |
| {235.81, 125.538}) | |
| .CubicCurveTo({235.413, 125.68}, {234.994, 125.832}, | |
| {234.592, 125.977}) | |
| .CubicCurveTo({234.592, 125.977}, {234.591, 125.977}, | |
| {234.59, 125.977}) | |
| .CubicCurveTo({222.206, 130.435}, {207.708, 135.753}, | |
| {192.381, 141.429}) | |
| .CubicCurveTo({162.77, 151.336}, {122.17, 156.894}, {84.1123, 160}) | |
| .Close() | |
| .TakePath(); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| entity.SetContents(SolidColorContents::Make(path, Color::Red())); | |
| ASSERT_TRUE(OpenPlaygroundHere(std::move(entity))); | |
| } | |
| TEST_P(EntityTest, CanDrawCorrectlyWithRotatedTransform) { | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| const char* input_axis[] = {"X", "Y", "Z"}; | |
| static int rotation_axis_index = 0; | |
| static float rotation = 0; | |
| ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); | |
| ImGui::SliderFloat("Rotation", &rotation, -kPi, kPi); | |
| ImGui::Combo("Rotation Axis", &rotation_axis_index, input_axis, | |
| sizeof(input_axis) / sizeof(char*)); | |
| Matrix rotation_matrix; | |
| switch (rotation_axis_index) { | |
| case 0: | |
| rotation_matrix = Matrix::MakeRotationX(Radians(rotation)); | |
| break; | |
| case 1: | |
| rotation_matrix = Matrix::MakeRotationY(Radians(rotation)); | |
| break; | |
| case 2: | |
| rotation_matrix = Matrix::MakeRotationZ(Radians(rotation)); | |
| break; | |
| default: | |
| rotation_matrix = Matrix{}; | |
| break; | |
| } | |
| if (ImGui::Button("Reset")) { | |
| rotation = 0; | |
| } | |
| ImGui::End(); | |
| Matrix current_transform = | |
| Matrix::MakeScale(GetContentScale()) | |
| .MakeTranslation( | |
| Vector3(Point(pass.GetRenderTargetSize().width / 2.0, | |
| pass.GetRenderTargetSize().height / 2.0))); | |
| Matrix result_transform = current_transform * rotation_matrix; | |
| Path path = | |
| PathBuilder{}.AddRect(Rect::MakeXYWH(-300, -400, 600, 800)).TakePath(); | |
| Entity entity; | |
| entity.SetTransform(result_transform); | |
| entity.SetContents(SolidColorContents::Make(path, Color::Red())); | |
| return entity.Render(context, pass); | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, CubicCurveAndOverlapTest) { | |
| // Compare with https://fiddle.skia.org/c/7a05a3e186c65a8dfb732f68020aae06 | |
| Path path = | |
| PathBuilder{} | |
| .MoveTo({359.934, 96.6335}) | |
| .CubicCurveTo({358.189, 96.7055}, {356.436, 96.7908}, | |
| {354.673, 96.8895}) | |
| .CubicCurveTo({354.571, 96.8953}, {354.469, 96.9016}, | |
| {354.367, 96.9075}) | |
| .CubicCurveTo({352.672, 97.0038}, {350.969, 97.113}, | |
| {349.259, 97.2355}) | |
| .CubicCurveTo({349.048, 97.2506}, {348.836, 97.2678}, | |
| {348.625, 97.2834}) | |
| .CubicCurveTo({347.019, 97.4014}, {345.407, 97.5299}, | |
| {343.789, 97.6722}) | |
| .CubicCurveTo({343.428, 97.704}, {343.065, 97.7402}, | |
| {342.703, 97.7734}) | |
| .CubicCurveTo({341.221, 97.9086}, {339.736, 98.0505}, | |
| {338.246, 98.207}) | |
| .CubicCurveTo({337.702, 98.2642}, {337.156, 98.3292}, | |
| {336.612, 98.3894}) | |
| .CubicCurveTo({335.284, 98.5356}, {333.956, 98.6837}, | |
| {332.623, 98.8476}) | |
| .CubicCurveTo({332.495, 98.8635}, {332.366, 98.8818}, | |
| {332.237, 98.8982}) | |
| .LineTo({332.237, 102.601}) | |
| .LineTo({321.778, 102.601}) | |
| .LineTo({321.778, 100.382}) | |
| .CubicCurveTo({321.572, 100.413}, {321.367, 100.442}, | |
| {321.161, 100.476}) | |
| .CubicCurveTo({319.22, 100.79}, {317.277, 101.123}, | |
| {315.332, 101.479}) | |
| .CubicCurveTo({315.322, 101.481}, {315.311, 101.482}, | |
| {315.301, 101.484}) | |
| .LineTo({310.017, 105.94}) | |
| .LineTo({309.779, 105.427}) | |
| .LineTo({314.403, 101.651}) | |
| .CubicCurveTo({314.391, 101.653}, {314.379, 101.656}, | |
| {314.368, 101.658}) | |
| .CubicCurveTo({312.528, 102.001}, {310.687, 102.366}, | |
| {308.846, 102.748}) | |
| .CubicCurveTo({307.85, 102.955}, {306.855, 103.182}, {305.859, 103.4}) | |
| .CubicCurveTo({305.048, 103.579}, {304.236, 103.75}, | |
| {303.425, 103.936}) | |
| .LineTo({299.105, 107.578}) | |
| .LineTo({298.867, 107.065}) | |
| .LineTo({302.394, 104.185}) | |
| .LineTo({302.412, 104.171}) | |
| .CubicCurveTo({301.388, 104.409}, {300.366, 104.67}, | |
| {299.344, 104.921}) | |
| .CubicCurveTo({298.618, 105.1}, {297.89, 105.269}, {297.165, 105.455}) | |
| .CubicCurveTo({295.262, 105.94}, {293.36, 106.445}, | |
| {291.462, 106.979}) | |
| .CubicCurveTo({291.132, 107.072}, {290.802, 107.163}, | |
| {290.471, 107.257}) | |
| .CubicCurveTo({289.463, 107.544}, {288.455, 107.839}, | |
| {287.449, 108.139}) | |
| .CubicCurveTo({286.476, 108.431}, {285.506, 108.73}, | |
| {284.536, 109.035}) | |
| .CubicCurveTo({283.674, 109.304}, {282.812, 109.579}, | |
| {281.952, 109.859}) | |
| .CubicCurveTo({281.177, 110.112}, {280.406, 110.377}, | |
| {279.633, 110.638}) | |
| .CubicCurveTo({278.458, 111.037}, {277.256, 111.449}, | |
| {276.803, 111.607}) | |
| .CubicCurveTo({276.76, 111.622}, {276.716, 111.637}, | |
| {276.672, 111.653}) | |
| .CubicCurveTo({275.017, 112.239}, {273.365, 112.836}, | |
| {271.721, 113.463}) | |
| .LineTo({271.717, 113.449}) | |
| .CubicCurveTo({271.496, 113.496}, {271.238, 113.559}, | |
| {270.963, 113.628}) | |
| .CubicCurveTo({270.893, 113.645}, {270.822, 113.663}, | |
| {270.748, 113.682}) | |
| .CubicCurveTo({270.468, 113.755}, {270.169, 113.834}, | |
| {269.839, 113.926}) | |
| .CubicCurveTo({269.789, 113.94}, {269.732, 113.957}, | |
| {269.681, 113.972}) | |
| .CubicCurveTo({269.391, 114.053}, {269.081, 114.143}, | |
| {268.756, 114.239}) | |
| .CubicCurveTo({268.628, 114.276}, {268.5, 114.314}, | |
| {268.367, 114.354}) | |
| .CubicCurveTo({268.172, 114.412}, {267.959, 114.478}, | |
| {267.752, 114.54}) | |
| .CubicCurveTo({263.349, 115.964}, {258.058, 117.695}, | |
| {253.564, 119.252}) | |
| .CubicCurveTo({253.556, 119.255}, {253.547, 119.258}, | |
| {253.538, 119.261}) | |
| .CubicCurveTo({251.844, 119.849}, {250.056, 120.474}, | |
| {248.189, 121.131}) | |
| .CubicCurveTo({248, 121.197}, {247.812, 121.264}, {247.621, 121.331}) | |
| .CubicCurveTo({247.079, 121.522}, {246.531, 121.715}, | |
| {245.975, 121.912}) | |
| .CubicCurveTo({245.554, 122.06}, {245.126, 122.212}, | |
| {244.698, 122.364}) | |
| .CubicCurveTo({244.071, 122.586}, {243.437, 122.811}, | |
| {242.794, 123.04}) | |
| .CubicCurveTo({242.189, 123.255}, {241.58, 123.472}, | |
| {240.961, 123.693}) | |
| .CubicCurveTo({240.659, 123.801}, {240.357, 123.909}, | |
| {240.052, 124.018}) | |
| .CubicCurveTo({239.12, 124.351}, {238.18, 124.687}, {237.22, 125.032}) | |
| .LineTo({237.164, 125.003}) | |
| .CubicCurveTo({236.709, 125.184}, {236.262, 125.358}, | |
| {235.81, 125.538}) | |
| .CubicCurveTo({235.413, 125.68}, {234.994, 125.832}, | |
| {234.592, 125.977}) | |
| .CubicCurveTo({234.592, 125.977}, {234.591, 125.977}, | |
| {234.59, 125.977}) | |
| .CubicCurveTo({222.206, 130.435}, {207.708, 135.753}, | |
| {192.381, 141.429}) | |
| .CubicCurveTo({162.77, 151.336}, {122.17, 156.894}, {84.1123, 160}) | |
| .LineTo({360, 160}) | |
| .LineTo({360, 119.256}) | |
| .LineTo({360, 106.332}) | |
| .LineTo({360, 96.6307}) | |
| .CubicCurveTo({359.978, 96.6317}, {359.956, 96.6326}, | |
| {359.934, 96.6335}) | |
| .Close() | |
| .MoveTo({337.336, 124.143}) | |
| .CubicCurveTo({337.274, 122.359}, {338.903, 121.511}, | |
| {338.903, 121.511}) | |
| .CubicCurveTo({338.903, 121.511}, {338.96, 123.303}, | |
| {337.336, 124.143}) | |
| .Close() | |
| .MoveTo({340.082, 121.849}) | |
| .CubicCurveTo({340.074, 121.917}, {340.062, 121.992}, | |
| {340.046, 122.075}) | |
| .CubicCurveTo({340.039, 122.109}, {340.031, 122.142}, | |
| {340.023, 122.177}) | |
| .CubicCurveTo({340.005, 122.26}, {339.98, 122.346}, | |
| {339.952, 122.437}) | |
| .CubicCurveTo({339.941, 122.473}, {339.931, 122.507}, | |
| {339.918, 122.544}) | |
| .CubicCurveTo({339.873, 122.672}, {339.819, 122.804}, | |
| {339.75, 122.938}) | |
| .CubicCurveTo({339.747, 122.944}, {339.743, 122.949}, | |
| {339.74, 122.955}) | |
| .CubicCurveTo({339.674, 123.08}, {339.593, 123.205}, | |
| {339.501, 123.328}) | |
| .CubicCurveTo({339.473, 123.366}, {339.441, 123.401}, | |
| {339.41, 123.438}) | |
| .CubicCurveTo({339.332, 123.534}, {339.243, 123.625}, | |
| {339.145, 123.714}) | |
| .CubicCurveTo({339.105, 123.75}, {339.068, 123.786}, | |
| {339.025, 123.821}) | |
| .CubicCurveTo({338.881, 123.937}, {338.724, 124.048}, | |
| {338.539, 124.143}) | |
| .CubicCurveTo({338.532, 123.959}, {338.554, 123.79}, | |
| {338.58, 123.626}) | |
| .CubicCurveTo({338.58, 123.625}, {338.58, 123.625}, {338.58, 123.625}) | |
| .CubicCurveTo({338.607, 123.455}, {338.65, 123.299}, | |
| {338.704, 123.151}) | |
| .CubicCurveTo({338.708, 123.14}, {338.71, 123.127}, | |
| {338.714, 123.117}) | |
| .CubicCurveTo({338.769, 122.971}, {338.833, 122.838}, | |
| {338.905, 122.712}) | |
| .CubicCurveTo({338.911, 122.702}, {338.916, 122.69200000000001}, | |
| {338.922, 122.682}) | |
| .CubicCurveTo({338.996, 122.557}, {339.072, 122.444}, | |
| {339.155, 122.34}) | |
| .CubicCurveTo({339.161, 122.333}, {339.166, 122.326}, | |
| {339.172, 122.319}) | |
| .CubicCurveTo({339.256, 122.215}, {339.339, 122.12}, | |
| {339.425, 122.037}) | |
| .CubicCurveTo({339.428, 122.033}, {339.431, 122.03}, | |
| {339.435, 122.027}) | |
| .CubicCurveTo({339.785, 121.687}, {340.106, 121.511}, | |
| {340.106, 121.511}) | |
| .CubicCurveTo({340.106, 121.511}, {340.107, 121.645}, | |
| {340.082, 121.849}) | |
| .Close() | |
| .MoveTo({340.678, 113.245}) | |
| .CubicCurveTo({340.594, 113.488}, {340.356, 113.655}, | |
| {340.135, 113.775}) | |
| .CubicCurveTo({339.817, 113.948}, {339.465, 114.059}, | |
| {339.115, 114.151}) | |
| .CubicCurveTo({338.251, 114.379}, {337.34, 114.516}, | |
| {336.448, 114.516}) | |
| .CubicCurveTo({335.761, 114.516}, {335.072, 114.527}, | |
| {334.384, 114.513}) | |
| .CubicCurveTo({334.125, 114.508}, {333.862, 114.462}, | |
| {333.605, 114.424}) | |
| .CubicCurveTo({332.865, 114.318}, {332.096, 114.184}, | |
| {331.41, 113.883}) | |
| .CubicCurveTo({330.979, 113.695}, {330.442, 113.34}, | |
| {330.672, 112.813}) | |
| .CubicCurveTo({331.135, 111.755}, {333.219, 112.946}, | |
| {334.526, 113.833}) | |
| .CubicCurveTo({334.54, 113.816}, {334.554, 113.8}, {334.569, 113.784}) | |
| .CubicCurveTo({333.38, 112.708}, {331.749, 110.985}, | |
| {332.76, 110.402}) | |
| .CubicCurveTo({333.769, 109.82}, {334.713, 111.93}, | |
| {335.228, 113.395}) | |
| .CubicCurveTo({334.915, 111.889}, {334.59, 109.636}, | |
| {335.661, 109.592}) | |
| .CubicCurveTo({336.733, 109.636}, {336.408, 111.889}, | |
| {336.07, 113.389}) | |
| .CubicCurveTo({336.609, 111.93}, {337.553, 109.82}, | |
| {338.563, 110.402}) | |
| .CubicCurveTo({339.574, 110.984}, {337.942, 112.708}, | |
| {336.753, 113.784}) | |
| .CubicCurveTo({336.768, 113.8}, {336.782, 113.816}, | |
| {336.796, 113.833}) | |
| .CubicCurveTo({338.104, 112.946}, {340.187, 111.755}, | |
| {340.65, 112.813}) | |
| .CubicCurveTo({340.71, 112.95}, {340.728, 113.102}, | |
| {340.678, 113.245}) | |
| .Close() | |
| .MoveTo({346.357, 106.771}) | |
| .CubicCurveTo({346.295, 104.987}, {347.924, 104.139}, | |
| {347.924, 104.139}) | |
| .CubicCurveTo({347.924, 104.139}, {347.982, 105.931}, | |
| {346.357, 106.771}) | |
| .Close() | |
| .MoveTo({347.56, 106.771}) | |
| .CubicCurveTo({347.498, 104.987}, {349.127, 104.139}, | |
| {349.127, 104.139}) | |
| .CubicCurveTo({349.127, 104.139}, {349.185, 105.931}, | |
| {347.56, 106.771}) | |
| .Close() | |
| .TakePath(); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| entity.SetContents(SolidColorContents::Make(path, Color::Red())); | |
| ASSERT_TRUE(OpenPlaygroundHere(std::move(entity))); | |
| } | |
| TEST_P(EntityTest, SolidColorContentsStrokeSetStrokeCapsAndJoins) { | |
| { | |
| auto geometry = Geometry::MakeStrokePath(Path{}); | |
| auto path_geometry = static_cast<StrokePathGeometry*>(geometry.get()); | |
| // Defaults. | |
| ASSERT_EQ(path_geometry->GetStrokeCap(), Cap::kButt); | |
| ASSERT_EQ(path_geometry->GetStrokeJoin(), Join::kMiter); | |
| } | |
| { | |
| auto geometry = Geometry::MakeStrokePath(Path{}, 1.0, 4.0, Cap::kSquare); | |
| auto path_geometry = static_cast<StrokePathGeometry*>(geometry.get()); | |
| ASSERT_EQ(path_geometry->GetStrokeCap(), Cap::kSquare); | |
| } | |
| { | |
| auto geometry = Geometry::MakeStrokePath(Path{}, 1.0, 4.0, Cap::kRound); | |
| auto path_geometry = static_cast<StrokePathGeometry*>(geometry.get()); | |
| ASSERT_EQ(path_geometry->GetStrokeCap(), Cap::kRound); | |
| } | |
| } | |
| TEST_P(EntityTest, SolidColorContentsStrokeSetMiterLimit) { | |
| { | |
| auto geometry = Geometry::MakeStrokePath(Path{}); | |
| auto path_geometry = static_cast<StrokePathGeometry*>(geometry.get()); | |
| ASSERT_FLOAT_EQ(path_geometry->GetMiterLimit(), 4); | |
| } | |
| { | |
| auto geometry = Geometry::MakeStrokePath(Path{}, 1.0, /*miter_limit=*/8.0); | |
| auto path_geometry = static_cast<StrokePathGeometry*>(geometry.get()); | |
| ASSERT_FLOAT_EQ(path_geometry->GetMiterLimit(), 8); | |
| } | |
| { | |
| auto geometry = Geometry::MakeStrokePath(Path{}, 1.0, /*miter_limit=*/-1.0); | |
| auto path_geometry = static_cast<StrokePathGeometry*>(geometry.get()); | |
| ASSERT_FLOAT_EQ(path_geometry->GetMiterLimit(), 4); | |
| } | |
| } | |
| TEST_P(EntityTest, BlendingModeOptions) { | |
| std::vector<const char*> blend_mode_names; | |
| std::vector<BlendMode> blend_mode_values; | |
| { | |
| // Force an exhausiveness check with a switch. When adding blend modes, | |
| // update this switch with a new name/value to make it selectable in the | |
| // test GUI. | |
| const BlendMode b{}; | |
| static_assert(b == BlendMode::kClear); // Ensure the first item in | |
| // the switch is the first | |
| // item in the enum. | |
| static_assert(Entity::kLastPipelineBlendMode == BlendMode::kModulate); | |
| switch (b) { | |
| case BlendMode::kClear: | |
| blend_mode_names.push_back("Clear"); | |
| blend_mode_values.push_back(BlendMode::kClear); | |
| case BlendMode::kSource: | |
| blend_mode_names.push_back("Source"); | |
| blend_mode_values.push_back(BlendMode::kSource); | |
| case BlendMode::kDestination: | |
| blend_mode_names.push_back("Destination"); | |
| blend_mode_values.push_back(BlendMode::kDestination); | |
| case BlendMode::kSourceOver: | |
| blend_mode_names.push_back("SourceOver"); | |
| blend_mode_values.push_back(BlendMode::kSourceOver); | |
| case BlendMode::kDestinationOver: | |
| blend_mode_names.push_back("DestinationOver"); | |
| blend_mode_values.push_back(BlendMode::kDestinationOver); | |
| case BlendMode::kSourceIn: | |
| blend_mode_names.push_back("SourceIn"); | |
| blend_mode_values.push_back(BlendMode::kSourceIn); | |
| case BlendMode::kDestinationIn: | |
| blend_mode_names.push_back("DestinationIn"); | |
| blend_mode_values.push_back(BlendMode::kDestinationIn); | |
| case BlendMode::kSourceOut: | |
| blend_mode_names.push_back("SourceOut"); | |
| blend_mode_values.push_back(BlendMode::kSourceOut); | |
| case BlendMode::kDestinationOut: | |
| blend_mode_names.push_back("DestinationOut"); | |
| blend_mode_values.push_back(BlendMode::kDestinationOut); | |
| case BlendMode::kSourceATop: | |
| blend_mode_names.push_back("SourceATop"); | |
| blend_mode_values.push_back(BlendMode::kSourceATop); | |
| case BlendMode::kDestinationATop: | |
| blend_mode_names.push_back("DestinationATop"); | |
| blend_mode_values.push_back(BlendMode::kDestinationATop); | |
| case BlendMode::kXor: | |
| blend_mode_names.push_back("Xor"); | |
| blend_mode_values.push_back(BlendMode::kXor); | |
| case BlendMode::kPlus: | |
| blend_mode_names.push_back("Plus"); | |
| blend_mode_values.push_back(BlendMode::kPlus); | |
| case BlendMode::kModulate: | |
| blend_mode_names.push_back("Modulate"); | |
| blend_mode_values.push_back(BlendMode::kModulate); | |
| }; | |
| } | |
| auto callback = [&](ContentContext& context, RenderPass& pass) { | |
| auto world_matrix = Matrix::MakeScale(GetContentScale()); | |
| auto draw_rect = [&context, &pass, &world_matrix]( | |
| Rect rect, Color color, BlendMode blend_mode) -> bool { | |
| using VS = SolidFillPipeline::VertexShader; | |
| VertexBufferBuilder<VS::PerVertexData> vtx_builder; | |
| { | |
| auto r = rect.GetLTRB(); | |
| vtx_builder.AddVertices({ | |
| {Point(r[0], r[1])}, | |
| {Point(r[2], r[1])}, | |
| {Point(r[2], r[3])}, | |
| {Point(r[0], r[1])}, | |
| {Point(r[2], r[3])}, | |
| {Point(r[0], r[3])}, | |
| }); | |
| } | |
| pass.SetCommandLabel("Blended Rectangle"); | |
| auto options = OptionsFromPass(pass); | |
| options.blend_mode = blend_mode; | |
| options.primitive_type = PrimitiveType::kTriangle; | |
| pass.SetPipeline(context.GetSolidFillPipeline(options)); | |
| pass.SetVertexBuffer( | |
| vtx_builder.CreateVertexBuffer(context.GetTransientsBuffer())); | |
| VS::FrameInfo frame_info; | |
| frame_info.mvp = pass.GetOrthographicTransform() * world_matrix; | |
| frame_info.color = color.Premultiply(); | |
| VS::BindFrameInfo( | |
| pass, context.GetTransientsBuffer().EmplaceUniform(frame_info)); | |
| return pass.Draw().ok(); | |
| }; | |
| ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); | |
| static Color color1(1, 0, 0, 0.5), color2(0, 1, 0, 0.5); | |
| ImGui::ColorEdit4("Color 1", reinterpret_cast<float*>(&color1)); | |
| ImGui::ColorEdit4("Color 2", reinterpret_cast<float*>(&color2)); | |
| static int current_blend_index = 3; | |
| ImGui::ListBox("Blending mode", ¤t_blend_index, | |
| blend_mode_names.data(), blend_mode_names.size()); | |
| ImGui::End(); | |
| BlendMode selected_mode = blend_mode_values[current_blend_index]; | |
| Point a, b, c, d; | |
| static PlaygroundPoint point_a(Point(400, 100), 20, Color::White()); | |
| static PlaygroundPoint point_b(Point(200, 300), 20, Color::White()); | |
| std::tie(a, b) = DrawPlaygroundLine(point_a, point_b); | |
| static PlaygroundPoint point_c(Point(470, 190), 20, Color::White()); | |
| static PlaygroundPoint point_d(Point(270, 390), 20, Color::White()); | |
| std::tie(c, d) = DrawPlaygroundLine(point_c, point_d); | |
| bool result = true; | |
| result = result && | |
| draw_rect(Rect::MakeXYWH(0, 0, pass.GetRenderTargetSize().width, | |
| pass.GetRenderTargetSize().height), | |
| Color(), BlendMode::kClear); | |
| result = result && draw_rect(Rect::MakeLTRB(a.x, a.y, b.x, b.y), color1, | |
| BlendMode::kSourceOver); | |
| result = result && draw_rect(Rect::MakeLTRB(c.x, c.y, d.x, d.y), color2, | |
| selected_mode); | |
| return result; | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, BezierCircleScaled) { | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| static float scale = 20; | |
| ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); | |
| ImGui::SliderFloat("Scale", &scale, 1, 100); | |
| ImGui::End(); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| auto path = PathBuilder{} | |
| .MoveTo({97.325, 34.818}) | |
| .CubicCurveTo({98.50862885295136, 34.81812293973836}, | |
| {99.46822048142015, 33.85863261475589}, | |
| {99.46822048142015, 32.67499810206613}) | |
| .CubicCurveTo({99.46822048142015, 31.491363589376355}, | |
| {98.50862885295136, 30.53187326439389}, | |
| {97.32499434685802, 30.531998226542708}) | |
| .CubicCurveTo({96.14153655073771, 30.532123170035373}, | |
| {95.18222070648729, 31.491540299350355}, | |
| {95.18222070648729, 32.67499810206613}) | |
| .CubicCurveTo({95.18222070648729, 33.85845590478189}, | |
| {96.14153655073771, 34.81787303409686}, | |
| {97.32499434685802, 34.81799797758954}) | |
| .Close() | |
| .TakePath(); | |
| entity.SetTransform( | |
| Matrix::MakeScale({scale, scale, 1.0}).Translate({-90, -20, 0})); | |
| entity.SetContents(SolidColorContents::Make(path, Color::Red())); | |
| return entity.Render(context, pass); | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, Filters) { | |
| auto bridge = CreateTextureForFixture("bay_bridge.jpg"); | |
| auto boston = CreateTextureForFixture("boston.jpg"); | |
| auto kalimba = CreateTextureForFixture("kalimba.jpg"); | |
| ASSERT_TRUE(bridge && boston && kalimba); | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| auto fi_bridge = FilterInput::Make(bridge); | |
| auto fi_boston = FilterInput::Make(boston); | |
| auto fi_kalimba = FilterInput::Make(kalimba); | |
| std::shared_ptr<FilterContents> blend0 = ColorFilterContents::MakeBlend( | |
| BlendMode::kModulate, {fi_kalimba, fi_boston}); | |
| auto blend1 = ColorFilterContents::MakeBlend( | |
| BlendMode::kScreen, | |
| {FilterInput::Make(blend0), fi_bridge, fi_bridge, fi_bridge}); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale()) * | |
| Matrix::MakeTranslation({500, 300}) * | |
| Matrix::MakeScale(Vector2{0.5, 0.5})); | |
| entity.SetContents(blend1); | |
| return entity.Render(context, pass); | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, GaussianBlurFilter) { | |
| auto boston = | |
| CreateTextureForFixture("boston.jpg", /*enable_mipmapping=*/true); | |
| ASSERT_TRUE(boston); | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| const char* input_type_names[] = {"Texture", "Solid Color"}; | |
| const char* blur_type_names[] = {"Image blur", "Mask blur"}; | |
| const char* pass_variation_names[] = {"New"}; | |
| const char* blur_style_names[] = {"Normal", "Solid", "Outer", "Inner"}; | |
| const char* tile_mode_names[] = {"Clamp", "Repeat", "Mirror", "Decal"}; | |
| const FilterContents::BlurStyle blur_styles[] = { | |
| FilterContents::BlurStyle::kNormal, FilterContents::BlurStyle::kSolid, | |
| FilterContents::BlurStyle::kOuter, FilterContents::BlurStyle::kInner}; | |
| const Entity::TileMode tile_modes[] = { | |
| Entity::TileMode::kClamp, Entity::TileMode::kRepeat, | |
| Entity::TileMode::kMirror, Entity::TileMode::kDecal}; | |
| // UI state. | |
| static int selected_input_type = 0; | |
| static Color input_color = Color::Black(); | |
| static int selected_blur_type = 0; | |
| static int selected_pass_variation = 0; | |
| static bool combined_sigma = false; | |
| static float blur_amount_coarse[2] = {0, 0}; | |
| static float blur_amount_fine[2] = {10, 10}; | |
| static int selected_blur_style = 0; | |
| static int selected_tile_mode = 3; | |
| static Color cover_color(1, 0, 0, 0.2); | |
| static Color bounds_color(0, 1, 0, 0.1); | |
| static float offset[2] = {500, 400}; | |
| static float rotation = 0; | |
| static float scale[2] = {0.65, 0.65}; | |
| static float skew[2] = {0, 0}; | |
| static float path_rect[4] = {0, 0, | |
| static_cast<float>(boston->GetSize().width), | |
| static_cast<float>(boston->GetSize().height)}; | |
| ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); | |
| { | |
| ImGui::Combo("Input type", &selected_input_type, input_type_names, | |
| sizeof(input_type_names) / sizeof(char*)); | |
| if (selected_input_type == 0) { | |
| ImGui::SliderFloat("Input opacity", &input_color.alpha, 0, 1); | |
| } else { | |
| ImGui::ColorEdit4("Input color", | |
| reinterpret_cast<float*>(&input_color)); | |
| } | |
| ImGui::Combo("Blur type", &selected_blur_type, blur_type_names, | |
| sizeof(blur_type_names) / sizeof(char*)); | |
| if (selected_blur_type == 0) { | |
| ImGui::Combo("Pass variation", &selected_pass_variation, | |
| pass_variation_names, | |
| sizeof(pass_variation_names) / sizeof(char*)); | |
| } | |
| ImGui::Checkbox("Combined sigma", &combined_sigma); | |
| if (combined_sigma) { | |
| ImGui::SliderFloat("Sigma (coarse)", blur_amount_coarse, 0, 1000); | |
| ImGui::SliderFloat("Sigma (fine)", blur_amount_fine, 0, 10); | |
| blur_amount_coarse[1] = blur_amount_coarse[0]; | |
| blur_amount_fine[1] = blur_amount_fine[0]; | |
| } else { | |
| ImGui::SliderFloat2("Sigma (coarse)", blur_amount_coarse, 0, 1000); | |
| ImGui::SliderFloat2("Sigma (fine)", blur_amount_fine, 0, 10); | |
| } | |
| ImGui::Combo("Blur style", &selected_blur_style, blur_style_names, | |
| sizeof(blur_style_names) / sizeof(char*)); | |
| ImGui::Combo("Tile mode", &selected_tile_mode, tile_mode_names, | |
| sizeof(tile_mode_names) / sizeof(char*)); | |
| ImGui::ColorEdit4("Cover color", reinterpret_cast<float*>(&cover_color)); | |
| ImGui::ColorEdit4("Bounds color", | |
| reinterpret_cast<float*>(&bounds_color)); | |
| ImGui::SliderFloat2("Translation", offset, 0, | |
| pass.GetRenderTargetSize().width); | |
| ImGui::SliderFloat("Rotation", &rotation, 0, kPi * 2); | |
| ImGui::SliderFloat2("Scale", scale, 0, 3); | |
| ImGui::SliderFloat2("Skew", skew, -3, 3); | |
| ImGui::SliderFloat4("Path XYWH", path_rect, -1000, 1000); | |
| } | |
| ImGui::End(); | |
| auto blur_sigma_x = Sigma{blur_amount_coarse[0] + blur_amount_fine[0]}; | |
| auto blur_sigma_y = Sigma{blur_amount_coarse[1] + blur_amount_fine[1]}; | |
| std::shared_ptr<Contents> input; | |
| Size input_size; | |
| auto input_rect = | |
| Rect::MakeXYWH(path_rect[0], path_rect[1], path_rect[2], path_rect[3]); | |
| if (selected_input_type == 0) { | |
| auto texture = std::make_shared<TextureContents>(); | |
| texture->SetSourceRect(Rect::MakeSize(boston->GetSize())); | |
| texture->SetDestinationRect(input_rect); | |
| texture->SetTexture(boston); | |
| texture->SetOpacity(input_color.alpha); | |
| input = texture; | |
| input_size = input_rect.GetSize(); | |
| } else { | |
| auto fill = std::make_shared<SolidColorContents>(); | |
| fill->SetColor(input_color); | |
| fill->SetGeometry( | |
| Geometry::MakeFillPath(PathBuilder{}.AddRect(input_rect).TakePath())); | |
| input = fill; | |
| input_size = input_rect.GetSize(); | |
| } | |
| std::shared_ptr<FilterContents> blur; | |
| switch (selected_pass_variation) { | |
| case 0: | |
| blur = std::make_shared<GaussianBlurFilterContents>( | |
| blur_sigma_x.sigma, blur_sigma_y.sigma, | |
| tile_modes[selected_tile_mode], blur_styles[selected_blur_style], | |
| /*geometry=*/nullptr); | |
| blur->SetInputs({FilterInput::Make(input)}); | |
| break; | |
| case 1: | |
| blur = FilterContents::MakeGaussianBlur( | |
| FilterInput::Make(input), blur_sigma_x, blur_sigma_y, | |
| tile_modes[selected_tile_mode], blur_styles[selected_blur_style]); | |
| break; | |
| }; | |
| FML_CHECK(blur); | |
| auto mask_blur = FilterContents::MakeBorderMaskBlur( | |
| FilterInput::Make(input), blur_sigma_x, blur_sigma_y, | |
| blur_styles[selected_blur_style]); | |
| auto ctm = Matrix::MakeScale(GetContentScale()) * | |
| Matrix::MakeTranslation(Vector3(offset[0], offset[1])) * | |
| Matrix::MakeRotationZ(Radians(rotation)) * | |
| Matrix::MakeScale(Vector2(scale[0], scale[1])) * | |
| Matrix::MakeSkew(skew[0], skew[1]) * | |
| Matrix::MakeTranslation(-Point(input_size) / 2); | |
| auto target_contents = selected_blur_type == 0 ? blur : mask_blur; | |
| Entity entity; | |
| entity.SetContents(target_contents); | |
| entity.SetTransform(ctm); | |
| entity.Render(context, pass); | |
| // Renders a red "cover" rectangle that shows the original position of the | |
| // unfiltered input. | |
| Entity cover_entity; | |
| cover_entity.SetContents(SolidColorContents::Make( | |
| PathBuilder{}.AddRect(input_rect).TakePath(), cover_color)); | |
| cover_entity.SetTransform(ctm); | |
| cover_entity.Render(context, pass); | |
| // Renders a green bounding rect of the target filter. | |
| Entity bounds_entity; | |
| std::optional<Rect> target_contents_coverage = | |
| target_contents->GetCoverage(entity); | |
| if (target_contents_coverage.has_value()) { | |
| bounds_entity.SetContents(SolidColorContents::Make( | |
| PathBuilder{} | |
| .AddRect(target_contents->GetCoverage(entity).value()) | |
| .TakePath(), | |
| bounds_color)); | |
| bounds_entity.SetTransform(Matrix()); | |
| bounds_entity.Render(context, pass); | |
| } | |
| return true; | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, MorphologyFilter) { | |
| auto boston = CreateTextureForFixture("boston.jpg"); | |
| ASSERT_TRUE(boston); | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| const char* morphology_type_names[] = {"Dilate", "Erode"}; | |
| const FilterContents::MorphType morphology_types[] = { | |
| FilterContents::MorphType::kDilate, FilterContents::MorphType::kErode}; | |
| static Color input_color = Color::Black(); | |
| // UI state. | |
| static int selected_morphology_type = 0; | |
| static float radius[2] = {20, 20}; | |
| static Color cover_color(1, 0, 0, 0.2); | |
| static Color bounds_color(0, 1, 0, 0.1); | |
| static float offset[2] = {500, 400}; | |
| static float rotation = 0; | |
| static float scale[2] = {0.65, 0.65}; | |
| static float skew[2] = {0, 0}; | |
| static float path_rect[4] = {0, 0, | |
| static_cast<float>(boston->GetSize().width), | |
| static_cast<float>(boston->GetSize().height)}; | |
| static float effect_transform_scale = 1; | |
| ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); | |
| { | |
| ImGui::Combo("Morphology type", &selected_morphology_type, | |
| morphology_type_names, | |
| sizeof(morphology_type_names) / sizeof(char*)); | |
| ImGui::SliderFloat2("Radius", radius, 0, 200); | |
| ImGui::SliderFloat("Input opacity", &input_color.alpha, 0, 1); | |
| ImGui::ColorEdit4("Cover color", reinterpret_cast<float*>(&cover_color)); | |
| ImGui::ColorEdit4("Bounds color", | |
| reinterpret_cast<float*>(&bounds_color)); | |
| ImGui::SliderFloat2("Translation", offset, 0, | |
| pass.GetRenderTargetSize().width); | |
| ImGui::SliderFloat("Rotation", &rotation, 0, kPi * 2); | |
| ImGui::SliderFloat2("Scale", scale, 0, 3); | |
| ImGui::SliderFloat2("Skew", skew, -3, 3); | |
| ImGui::SliderFloat4("Path XYWH", path_rect, -1000, 1000); | |
| ImGui::SliderFloat("Effect transform scale", &effect_transform_scale, 0, | |
| 3); | |
| } | |
| ImGui::End(); | |
| std::shared_ptr<Contents> input; | |
| Size input_size; | |
| auto input_rect = | |
| Rect::MakeXYWH(path_rect[0], path_rect[1], path_rect[2], path_rect[3]); | |
| auto texture = std::make_shared<TextureContents>(); | |
| texture->SetSourceRect(Rect::MakeSize(boston->GetSize())); | |
| texture->SetDestinationRect(input_rect); | |
| texture->SetTexture(boston); | |
| texture->SetOpacity(input_color.alpha); | |
| input = texture; | |
| input_size = input_rect.GetSize(); | |
| auto contents = FilterContents::MakeMorphology( | |
| FilterInput::Make(input), Radius{radius[0]}, Radius{radius[1]}, | |
| morphology_types[selected_morphology_type]); | |
| contents->SetEffectTransform(Matrix::MakeScale( | |
| Vector2{effect_transform_scale, effect_transform_scale})); | |
| auto ctm = Matrix::MakeScale(GetContentScale()) * | |
| Matrix::MakeTranslation(Vector3(offset[0], offset[1])) * | |
| Matrix::MakeRotationZ(Radians(rotation)) * | |
| Matrix::MakeScale(Vector2(scale[0], scale[1])) * | |
| Matrix::MakeSkew(skew[0], skew[1]) * | |
| Matrix::MakeTranslation(-Point(input_size) / 2); | |
| Entity entity; | |
| entity.SetContents(contents); | |
| entity.SetTransform(ctm); | |
| entity.Render(context, pass); | |
| // Renders a red "cover" rectangle that shows the original position of the | |
| // unfiltered input. | |
| Entity cover_entity; | |
| cover_entity.SetContents(SolidColorContents::Make( | |
| PathBuilder{}.AddRect(input_rect).TakePath(), cover_color)); | |
| cover_entity.SetTransform(ctm); | |
| cover_entity.Render(context, pass); | |
| // Renders a green bounding rect of the target filter. | |
| Entity bounds_entity; | |
| bounds_entity.SetContents(SolidColorContents::Make( | |
| PathBuilder{}.AddRect(contents->GetCoverage(entity).value()).TakePath(), | |
| bounds_color)); | |
| bounds_entity.SetTransform(Matrix()); | |
| bounds_entity.Render(context, pass); | |
| return true; | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, SetBlendMode) { | |
| Entity entity; | |
| ASSERT_EQ(entity.GetBlendMode(), BlendMode::kSourceOver); | |
| entity.SetBlendMode(BlendMode::kClear); | |
| ASSERT_EQ(entity.GetBlendMode(), BlendMode::kClear); | |
| } | |
| TEST_P(EntityTest, ContentsGetBoundsForEmptyPathReturnsNullopt) { | |
| Entity entity; | |
| entity.SetContents(std::make_shared<SolidColorContents>()); | |
| ASSERT_FALSE(entity.GetCoverage().has_value()); | |
| } | |
| TEST_P(EntityTest, SolidStrokeCoverageIsCorrect) { | |
| { | |
| auto geometry = Geometry::MakeStrokePath( | |
| PathBuilder{}.AddLine({0, 0}, {10, 10}).TakePath(), 4.0, 4.0, | |
| Cap::kButt, Join::kBevel); | |
| Entity entity; | |
| auto contents = std::make_unique<SolidColorContents>(); | |
| contents->SetGeometry(std::move(geometry)); | |
| contents->SetColor(Color::Black()); | |
| entity.SetContents(std::move(contents)); | |
| auto actual = entity.GetCoverage(); | |
| auto expected = Rect::MakeLTRB(-2, -2, 12, 12); | |
| ASSERT_TRUE(actual.has_value()); | |
| ASSERT_RECT_NEAR(actual.value(), expected); | |
| } | |
| // Cover the Cap::kSquare case. | |
| { | |
| auto geometry = Geometry::MakeStrokePath( | |
| PathBuilder{}.AddLine({0, 0}, {10, 10}).TakePath(), 4.0, 4.0, | |
| Cap::kSquare, Join::kBevel); | |
| Entity entity; | |
| auto contents = std::make_unique<SolidColorContents>(); | |
| contents->SetGeometry(std::move(geometry)); | |
| contents->SetColor(Color::Black()); | |
| entity.SetContents(std::move(contents)); | |
| auto actual = entity.GetCoverage(); | |
| auto expected = | |
| Rect::MakeLTRB(-sqrt(8), -sqrt(8), 10 + sqrt(8), 10 + sqrt(8)); | |
| ASSERT_TRUE(actual.has_value()); | |
| ASSERT_RECT_NEAR(actual.value(), expected); | |
| } | |
| // Cover the Join::kMiter case. | |
| { | |
| auto geometry = Geometry::MakeStrokePath( | |
| PathBuilder{}.AddLine({0, 0}, {10, 10}).TakePath(), 4.0, 2.0, | |
| Cap::kSquare, Join::kMiter); | |
| Entity entity; | |
| auto contents = std::make_unique<SolidColorContents>(); | |
| contents->SetGeometry(std::move(geometry)); | |
| contents->SetColor(Color::Black()); | |
| entity.SetContents(std::move(contents)); | |
| auto actual = entity.GetCoverage(); | |
| auto expected = Rect::MakeLTRB(-4, -4, 14, 14); | |
| ASSERT_TRUE(actual.has_value()); | |
| ASSERT_RECT_NEAR(actual.value(), expected); | |
| } | |
| } | |
| TEST_P(EntityTest, BorderMaskBlurCoverageIsCorrect) { | |
| auto fill = std::make_shared<SolidColorContents>(); | |
| fill->SetGeometry(Geometry::MakeFillPath( | |
| PathBuilder{}.AddRect(Rect::MakeXYWH(0, 0, 300, 400)).TakePath())); | |
| fill->SetColor(Color::CornflowerBlue()); | |
| auto border_mask_blur = FilterContents::MakeBorderMaskBlur( | |
| FilterInput::Make(fill), Radius{3}, Radius{4}); | |
| { | |
| Entity e; | |
| e.SetTransform(Matrix()); | |
| auto actual = border_mask_blur->GetCoverage(e); | |
| auto expected = Rect::MakeXYWH(-3, -4, 306, 408); | |
| ASSERT_TRUE(actual.has_value()); | |
| ASSERT_RECT_NEAR(actual.value(), expected); | |
| } | |
| { | |
| Entity e; | |
| e.SetTransform(Matrix::MakeRotationZ(Radians{kPi / 4})); | |
| auto actual = border_mask_blur->GetCoverage(e); | |
| auto expected = Rect::MakeXYWH(-287.792, -4.94975, 504.874, 504.874); | |
| ASSERT_TRUE(actual.has_value()); | |
| ASSERT_RECT_NEAR(actual.value(), expected); | |
| } | |
| } | |
| TEST_P(EntityTest, DrawAtlasNoColor) { | |
| // Draws the image as four squares stiched together. | |
| auto atlas = CreateTextureForFixture("bay_bridge.jpg"); | |
| auto size = atlas->GetSize(); | |
| // Divide image into four quadrants. | |
| Scalar half_width = size.width / 2; | |
| Scalar half_height = size.height / 2; | |
| std::vector<Rect> texture_coordinates = { | |
| Rect::MakeLTRB(0, 0, half_width, half_height), | |
| Rect::MakeLTRB(half_width, 0, size.width, half_height), | |
| Rect::MakeLTRB(0, half_height, half_width, size.height), | |
| Rect::MakeLTRB(half_width, half_height, size.width, size.height)}; | |
| // Position quadrants adjacent to eachother. | |
| std::vector<Matrix> transforms = { | |
| Matrix::MakeTranslation({0, 0, 0}), | |
| Matrix::MakeTranslation({half_width, 0, 0}), | |
| Matrix::MakeTranslation({0, half_height, 0}), | |
| Matrix::MakeTranslation({half_width, half_height, 0})}; | |
| std::shared_ptr<AtlasContents> contents = std::make_shared<AtlasContents>(); | |
| contents->SetTransforms(std::move(transforms)); | |
| contents->SetTextureCoordinates(std::move(texture_coordinates)); | |
| contents->SetTexture(atlas); | |
| contents->SetBlendMode(BlendMode::kSource); | |
| Entity e; | |
| e.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| e.SetContents(contents); | |
| ASSERT_TRUE(OpenPlaygroundHere(std::move(e))); | |
| } | |
| TEST_P(EntityTest, DrawAtlasWithColorAdvanced) { | |
| // Draws the image as four squares stiched together. | |
| auto atlas = CreateTextureForFixture("bay_bridge.jpg"); | |
| auto size = atlas->GetSize(); | |
| // Divide image into four quadrants. | |
| Scalar half_width = size.width / 2; | |
| Scalar half_height = size.height / 2; | |
| std::vector<Rect> texture_coordinates = { | |
| Rect::MakeLTRB(0, 0, half_width, half_height), | |
| Rect::MakeLTRB(half_width, 0, size.width, half_height), | |
| Rect::MakeLTRB(0, half_height, half_width, size.height), | |
| Rect::MakeLTRB(half_width, half_height, size.width, size.height)}; | |
| // Position quadrants adjacent to eachother. | |
| std::vector<Matrix> transforms = { | |
| Matrix::MakeTranslation({0, 0, 0}), | |
| Matrix::MakeTranslation({half_width, 0, 0}), | |
| Matrix::MakeTranslation({0, half_height, 0}), | |
| Matrix::MakeTranslation({half_width, half_height, 0})}; | |
| std::vector<Color> colors = {Color::Red(), Color::Green(), Color::Blue(), | |
| Color::Yellow()}; | |
| std::shared_ptr<AtlasContents> contents = std::make_shared<AtlasContents>(); | |
| contents->SetTransforms(std::move(transforms)); | |
| contents->SetTextureCoordinates(std::move(texture_coordinates)); | |
| contents->SetTexture(atlas); | |
| contents->SetColors(colors); | |
| contents->SetBlendMode(BlendMode::kModulate); | |
| Entity e; | |
| e.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| e.SetContents(contents); | |
| ASSERT_TRUE(OpenPlaygroundHere(std::move(e))); | |
| } | |
| TEST_P(EntityTest, DrawAtlasWithColorSimple) { | |
| // Draws the image as four squares stiched together. Because blend modes | |
| // aren't implented this ends up as four solid color blocks. | |
| auto atlas = CreateTextureForFixture("bay_bridge.jpg"); | |
| auto size = atlas->GetSize(); | |
| // Divide image into four quadrants. | |
| Scalar half_width = size.width / 2; | |
| Scalar half_height = size.height / 2; | |
| std::vector<Rect> texture_coordinates = { | |
| Rect::MakeLTRB(0, 0, half_width, half_height), | |
| Rect::MakeLTRB(half_width, 0, size.width, half_height), | |
| Rect::MakeLTRB(0, half_height, half_width, size.height), | |
| Rect::MakeLTRB(half_width, half_height, size.width, size.height)}; | |
| // Position quadrants adjacent to eachother. | |
| std::vector<Matrix> transforms = { | |
| Matrix::MakeTranslation({0, 0, 0}), | |
| Matrix::MakeTranslation({half_width, 0, 0}), | |
| Matrix::MakeTranslation({0, half_height, 0}), | |
| Matrix::MakeTranslation({half_width, half_height, 0})}; | |
| std::vector<Color> colors = {Color::Red(), Color::Green(), Color::Blue(), | |
| Color::Yellow()}; | |
| std::shared_ptr<AtlasContents> contents = std::make_shared<AtlasContents>(); | |
| contents->SetTransforms(std::move(transforms)); | |
| contents->SetTextureCoordinates(std::move(texture_coordinates)); | |
| contents->SetTexture(atlas); | |
| contents->SetColors(colors); | |
| contents->SetBlendMode(BlendMode::kSourceATop); | |
| Entity e; | |
| e.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| e.SetContents(contents); | |
| ASSERT_TRUE(OpenPlaygroundHere(std::move(e))); | |
| } | |
| TEST_P(EntityTest, DrawAtlasUsesProvidedCullRectForCoverage) { | |
| auto atlas = CreateTextureForFixture("bay_bridge.jpg"); | |
| auto size = atlas->GetSize(); | |
| Scalar half_width = size.width / 2; | |
| Scalar half_height = size.height / 2; | |
| std::vector<Rect> texture_coordinates = { | |
| Rect::MakeLTRB(0, 0, half_width, half_height), | |
| Rect::MakeLTRB(half_width, 0, size.width, half_height), | |
| Rect::MakeLTRB(0, half_height, half_width, size.height), | |
| Rect::MakeLTRB(half_width, half_height, size.width, size.height)}; | |
| std::vector<Matrix> transforms = { | |
| Matrix::MakeTranslation({0, 0, 0}), | |
| Matrix::MakeTranslation({half_width, 0, 0}), | |
| Matrix::MakeTranslation({0, half_height, 0}), | |
| Matrix::MakeTranslation({half_width, half_height, 0})}; | |
| std::shared_ptr<AtlasContents> contents = std::make_shared<AtlasContents>(); | |
| contents->SetTransforms(std::move(transforms)); | |
| contents->SetTextureCoordinates(std::move(texture_coordinates)); | |
| contents->SetTexture(atlas); | |
| contents->SetBlendMode(BlendMode::kSource); | |
| auto transform = Matrix::MakeScale(GetContentScale()); | |
| Entity e; | |
| e.SetTransform(transform); | |
| e.SetContents(contents); | |
| ASSERT_EQ(contents->GetCoverage(e).value(), | |
| Rect::MakeSize(size).TransformBounds(transform)); | |
| contents->SetCullRect(Rect::MakeLTRB(0, 0, 10, 10)); | |
| ASSERT_EQ(contents->GetCoverage(e).value(), | |
| Rect::MakeLTRB(0, 0, 10, 10).TransformBounds(transform)); | |
| } | |
| TEST_P(EntityTest, DrawAtlasWithOpacity) { | |
| // Draws the image as four squares stiched together slightly | |
| // opaque | |
| auto atlas = CreateTextureForFixture("bay_bridge.jpg"); | |
| auto size = atlas->GetSize(); | |
| // Divide image into four quadrants. | |
| Scalar half_width = size.width / 2; | |
| Scalar half_height = size.height / 2; | |
| std::vector<Rect> texture_coordinates = { | |
| Rect::MakeLTRB(0, 0, half_width, half_height), | |
| Rect::MakeLTRB(half_width, 0, size.width, half_height), | |
| Rect::MakeLTRB(0, half_height, half_width, size.height), | |
| Rect::MakeLTRB(half_width, half_height, size.width, size.height)}; | |
| // Position quadrants adjacent to eachother. | |
| std::vector<Matrix> transforms = { | |
| Matrix::MakeTranslation({0, 0, 0}), | |
| Matrix::MakeTranslation({half_width, 0, 0}), | |
| Matrix::MakeTranslation({0, half_height, 0}), | |
| Matrix::MakeTranslation({half_width, half_height, 0})}; | |
| std::shared_ptr<AtlasContents> contents = std::make_shared<AtlasContents>(); | |
| contents->SetTransforms(std::move(transforms)); | |
| contents->SetTextureCoordinates(std::move(texture_coordinates)); | |
| contents->SetTexture(atlas); | |
| contents->SetBlendMode(BlendMode::kSource); | |
| contents->SetAlpha(0.5); | |
| Entity e; | |
| e.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| e.SetContents(contents); | |
| ASSERT_TRUE(OpenPlaygroundHere(std::move(e))); | |
| } | |
| TEST_P(EntityTest, DrawAtlasNoColorFullSize) { | |
| auto atlas = CreateTextureForFixture("bay_bridge.jpg"); | |
| auto size = atlas->GetSize(); | |
| std::vector<Rect> texture_coordinates = { | |
| Rect::MakeLTRB(0, 0, size.width, size.height)}; | |
| std::vector<Matrix> transforms = {Matrix::MakeTranslation({0, 0, 0})}; | |
| std::shared_ptr<AtlasContents> contents = std::make_shared<AtlasContents>(); | |
| contents->SetTransforms(std::move(transforms)); | |
| contents->SetTextureCoordinates(std::move(texture_coordinates)); | |
| contents->SetTexture(atlas); | |
| contents->SetBlendMode(BlendMode::kSource); | |
| Entity e; | |
| e.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| e.SetContents(contents); | |
| ASSERT_TRUE(OpenPlaygroundHere(std::move(e))); | |
| } | |
| TEST_P(EntityTest, SolidFillCoverageIsCorrect) { | |
| // No transform | |
| { | |
| auto fill = std::make_shared<SolidColorContents>(); | |
| fill->SetColor(Color::CornflowerBlue()); | |
| auto expected = Rect::MakeLTRB(100, 110, 200, 220); | |
| fill->SetGeometry( | |
| Geometry::MakeFillPath(PathBuilder{}.AddRect(expected).TakePath())); | |
| auto coverage = fill->GetCoverage({}); | |
| ASSERT_TRUE(coverage.has_value()); | |
| ASSERT_RECT_NEAR(coverage.value(), expected); | |
| } | |
| // Entity transform | |
| { | |
| auto fill = std::make_shared<SolidColorContents>(); | |
| fill->SetColor(Color::CornflowerBlue()); | |
| fill->SetGeometry(Geometry::MakeFillPath( | |
| PathBuilder{}.AddRect(Rect::MakeLTRB(100, 110, 200, 220)).TakePath())); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeTranslation(Vector2(4, 5))); | |
| entity.SetContents(std::move(fill)); | |
| auto coverage = entity.GetCoverage(); | |
| auto expected = Rect::MakeLTRB(104, 115, 204, 225); | |
| ASSERT_TRUE(coverage.has_value()); | |
| ASSERT_RECT_NEAR(coverage.value(), expected); | |
| } | |
| // No coverage for fully transparent colors | |
| { | |
| auto fill = std::make_shared<SolidColorContents>(); | |
| fill->SetColor(Color::WhiteTransparent()); | |
| fill->SetGeometry(Geometry::MakeFillPath( | |
| PathBuilder{}.AddRect(Rect::MakeLTRB(100, 110, 200, 220)).TakePath())); | |
| auto coverage = fill->GetCoverage({}); | |
| ASSERT_FALSE(coverage.has_value()); | |
| } | |
| } | |
| TEST_P(EntityTest, SolidFillShouldRenderIsCorrect) { | |
| // No path. | |
| { | |
| auto fill = std::make_shared<SolidColorContents>(); | |
| fill->SetColor(Color::CornflowerBlue()); | |
| ASSERT_FALSE(fill->ShouldRender(Entity{}, Rect::MakeSize(Size{100, 100}))); | |
| ASSERT_FALSE( | |
| fill->ShouldRender(Entity{}, Rect::MakeLTRB(-100, -100, -50, -50))); | |
| } | |
| // With path. | |
| { | |
| auto fill = std::make_shared<SolidColorContents>(); | |
| fill->SetColor(Color::CornflowerBlue()); | |
| fill->SetGeometry(Geometry::MakeFillPath( | |
| PathBuilder{}.AddRect(Rect::MakeLTRB(0, 0, 100, 100)).TakePath())); | |
| ASSERT_TRUE(fill->ShouldRender(Entity{}, Rect::MakeSize(Size{100, 100}))); | |
| ASSERT_FALSE( | |
| fill->ShouldRender(Entity{}, Rect::MakeLTRB(-100, -100, -50, -50))); | |
| } | |
| // With paint cover. | |
| { | |
| auto fill = std::make_shared<SolidColorContents>(); | |
| fill->SetColor(Color::CornflowerBlue()); | |
| fill->SetGeometry(Geometry::MakeCover()); | |
| ASSERT_TRUE(fill->ShouldRender(Entity{}, Rect::MakeSize(Size{100, 100}))); | |
| ASSERT_TRUE( | |
| fill->ShouldRender(Entity{}, Rect::MakeLTRB(-100, -100, -50, -50))); | |
| } | |
| } | |
| TEST_P(EntityTest, DoesNotCullEntitiesByDefault) { | |
| auto fill = std::make_shared<SolidColorContents>(); | |
| fill->SetColor(Color::CornflowerBlue()); | |
| fill->SetGeometry( | |
| Geometry::MakeRect(Rect::MakeLTRB(-1000, -1000, -900, -900))); | |
| Entity entity; | |
| entity.SetContents(fill); | |
| // Even though the entity is offscreen, this should still render because we do | |
| // not compute the coverage intersection by default. | |
| EXPECT_TRUE(entity.ShouldRender(Rect::MakeLTRB(0, 0, 100, 100))); | |
| } | |
| TEST_P(EntityTest, ClipContentsShouldRenderIsCorrect) { | |
| // For clip ops, `ShouldRender` should always return true. | |
| // Clip. | |
| { | |
| auto clip = std::make_shared<ClipContents>(); | |
| ASSERT_TRUE(clip->ShouldRender(Entity{}, Rect::MakeSize(Size{100, 100}))); | |
| clip->SetGeometry(Geometry::MakeFillPath( | |
| PathBuilder{}.AddRect(Rect::MakeLTRB(0, 0, 100, 100)).TakePath())); | |
| ASSERT_TRUE(clip->ShouldRender(Entity{}, Rect::MakeSize(Size{100, 100}))); | |
| ASSERT_TRUE( | |
| clip->ShouldRender(Entity{}, Rect::MakeLTRB(-100, -100, -50, -50))); | |
| } | |
| // Clip restore. | |
| { | |
| auto restore = std::make_shared<ClipRestoreContents>(); | |
| ASSERT_TRUE( | |
| restore->ShouldRender(Entity{}, Rect::MakeSize(Size{100, 100}))); | |
| ASSERT_TRUE( | |
| restore->ShouldRender(Entity{}, Rect::MakeLTRB(-100, -100, -50, -50))); | |
| } | |
| } | |
| TEST_P(EntityTest, ClipContentsGetClipCoverageIsCorrect) { | |
| // Intersection: No stencil coverage, no geometry. | |
| { | |
| auto clip = std::make_shared<ClipContents>(); | |
| clip->SetClipOperation(Entity::ClipOperation::kIntersect); | |
| auto result = clip->GetClipCoverage(Entity{}, Rect{}); | |
| ASSERT_FALSE(result.coverage.has_value()); | |
| } | |
| // Intersection: No stencil coverage, with geometry. | |
| { | |
| auto clip = std::make_shared<ClipContents>(); | |
| clip->SetClipOperation(Entity::ClipOperation::kIntersect); | |
| clip->SetGeometry(Geometry::MakeFillPath( | |
| PathBuilder{}.AddRect(Rect::MakeLTRB(0, 0, 100, 100)).TakePath())); | |
| auto result = clip->GetClipCoverage(Entity{}, Rect{}); | |
| ASSERT_FALSE(result.coverage.has_value()); | |
| } | |
| // Intersection: With stencil coverage, no geometry. | |
| { | |
| auto clip = std::make_shared<ClipContents>(); | |
| clip->SetClipOperation(Entity::ClipOperation::kIntersect); | |
| auto result = | |
| clip->GetClipCoverage(Entity{}, Rect::MakeLTRB(0, 0, 100, 100)); | |
| ASSERT_FALSE(result.coverage.has_value()); | |
| } | |
| // Intersection: With stencil coverage, with geometry. | |
| { | |
| auto clip = std::make_shared<ClipContents>(); | |
| clip->SetClipOperation(Entity::ClipOperation::kIntersect); | |
| clip->SetGeometry(Geometry::MakeFillPath( | |
| PathBuilder{}.AddRect(Rect::MakeLTRB(0, 0, 50, 50)).TakePath())); | |
| auto result = | |
| clip->GetClipCoverage(Entity{}, Rect::MakeLTRB(0, 0, 100, 100)); | |
| ASSERT_TRUE(result.coverage.has_value()); | |
| ASSERT_RECT_NEAR(result.coverage.value(), Rect::MakeLTRB(0, 0, 50, 50)); | |
| ASSERT_EQ(result.type, Contents::ClipCoverage::Type::kAppend); | |
| } | |
| // Difference: With stencil coverage, with geometry. | |
| { | |
| auto clip = std::make_shared<ClipContents>(); | |
| clip->SetClipOperation(Entity::ClipOperation::kDifference); | |
| clip->SetGeometry(Geometry::MakeFillPath( | |
| PathBuilder{}.AddRect(Rect::MakeLTRB(0, 0, 50, 50)).TakePath())); | |
| auto result = | |
| clip->GetClipCoverage(Entity{}, Rect::MakeLTRB(0, 0, 100, 100)); | |
| ASSERT_TRUE(result.coverage.has_value()); | |
| ASSERT_RECT_NEAR(result.coverage.value(), Rect::MakeLTRB(0, 0, 100, 100)); | |
| ASSERT_EQ(result.type, Contents::ClipCoverage::Type::kAppend); | |
| } | |
| } | |
| TEST_P(EntityTest, RRectShadowTest) { | |
| auto callback = [&](ContentContext& context, RenderPass& pass) { | |
| static Color color = Color::Red(); | |
| static float corner_radius = 100; | |
| static float blur_radius = 100; | |
| static bool show_coverage = false; | |
| static Color coverage_color = Color::Green().WithAlpha(0.2); | |
| ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize); | |
| ImGui::SliderFloat("Corner radius", &corner_radius, 0, 300); | |
| ImGui::SliderFloat("Blur radius", &blur_radius, 0, 300); | |
| ImGui::ColorEdit4("Color", reinterpret_cast<Scalar*>(&color)); | |
| ImGui::Checkbox("Show coverage", &show_coverage); | |
| if (show_coverage) { | |
| ImGui::ColorEdit4("Coverage color", | |
| reinterpret_cast<Scalar*>(&coverage_color)); | |
| } | |
| ImGui::End(); | |
| static PlaygroundPoint top_left_point(Point(200, 200), 30, Color::White()); | |
| static PlaygroundPoint bottom_right_point(Point(600, 400), 30, | |
| Color::White()); | |
| auto [top_left, bottom_right] = | |
| DrawPlaygroundLine(top_left_point, bottom_right_point); | |
| auto rect = | |
| Rect::MakeLTRB(top_left.x, top_left.y, bottom_right.x, bottom_right.y); | |
| auto contents = std::make_unique<SolidRRectBlurContents>(); | |
| contents->SetRRect(rect, {corner_radius, corner_radius}); | |
| contents->SetColor(color); | |
| contents->SetSigma(Radius(blur_radius)); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| entity.SetContents(std::move(contents)); | |
| entity.Render(context, pass); | |
| auto coverage = entity.GetCoverage(); | |
| if (show_coverage && coverage.has_value()) { | |
| auto bounds_contents = std::make_unique<SolidColorContents>(); | |
| bounds_contents->SetGeometry(Geometry::MakeFillPath( | |
| PathBuilder{}.AddRect(entity.GetCoverage().value()).TakePath())); | |
| bounds_contents->SetColor(coverage_color.Premultiply()); | |
| Entity bounds_entity; | |
| bounds_entity.SetContents(std::move(bounds_contents)); | |
| bounds_entity.Render(context, pass); | |
| } | |
| return true; | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, ColorMatrixFilterCoverageIsCorrect) { | |
| // Set up a simple color background. | |
| auto fill = std::make_shared<SolidColorContents>(); | |
| fill->SetGeometry(Geometry::MakeFillPath( | |
| PathBuilder{}.AddRect(Rect::MakeXYWH(0, 0, 300, 400)).TakePath())); | |
| fill->SetColor(Color::Coral()); | |
| // Set the color matrix filter. | |
| ColorMatrix matrix = { | |
| 1, 1, 1, 1, 1, // | |
| 1, 1, 1, 1, 1, // | |
| 1, 1, 1, 1, 1, // | |
| 1, 1, 1, 1, 1, // | |
| }; | |
| auto filter = | |
| ColorFilterContents::MakeColorMatrix(FilterInput::Make(fill), matrix); | |
| Entity e; | |
| e.SetTransform(Matrix()); | |
| // Confirm that the actual filter coverage matches the expected coverage. | |
| auto actual = filter->GetCoverage(e); | |
| auto expected = Rect::MakeXYWH(0, 0, 300, 400); | |
| ASSERT_TRUE(actual.has_value()); | |
| ASSERT_RECT_NEAR(actual.value(), expected); | |
| } | |
| TEST_P(EntityTest, ColorMatrixFilterEditable) { | |
| auto bay_bridge = CreateTextureForFixture("bay_bridge.jpg"); | |
| ASSERT_TRUE(bay_bridge); | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| // UI state. | |
| static ColorMatrix color_matrix = { | |
| 1, 0, 0, 0, 0, // | |
| 0, 3, 0, 0, 0, // | |
| 0, 0, 1, 0, 0, // | |
| 0, 0, 0, 1, 0, // | |
| }; | |
| static float offset[2] = {500, 400}; | |
| static float rotation = 0; | |
| static float scale[2] = {0.65, 0.65}; | |
| static float skew[2] = {0, 0}; | |
| // Define the ImGui | |
| ImGui::Begin("Color Matrix", nullptr, ImGuiWindowFlags_AlwaysAutoResize); | |
| { | |
| std::string label = "##1"; | |
| for (int i = 0; i < 20; i += 5) { | |
| ImGui::InputScalarN(label.c_str(), ImGuiDataType_Float, | |
| &(color_matrix.array[i]), 5, nullptr, nullptr, | |
| "%.2f", 0); | |
| label[2]++; | |
| } | |
| ImGui::SliderFloat2("Translation", &offset[0], 0, | |
| pass.GetRenderTargetSize().width); | |
| ImGui::SliderFloat("Rotation", &rotation, 0, kPi * 2); | |
| ImGui::SliderFloat2("Scale", &scale[0], 0, 3); | |
| ImGui::SliderFloat2("Skew", &skew[0], -3, 3); | |
| } | |
| ImGui::End(); | |
| // Set the color matrix filter. | |
| auto filter = ColorFilterContents::MakeColorMatrix( | |
| FilterInput::Make(bay_bridge), color_matrix); | |
| // Define the entity with the color matrix filter. | |
| Entity entity; | |
| entity.SetTransform( | |
| Matrix::MakeScale(GetContentScale()) * | |
| Matrix::MakeTranslation(Vector3(offset[0], offset[1])) * | |
| Matrix::MakeRotationZ(Radians(rotation)) * | |
| Matrix::MakeScale(Vector2(scale[0], scale[1])) * | |
| Matrix::MakeSkew(skew[0], skew[1]) * | |
| Matrix::MakeTranslation(-Point(bay_bridge->GetSize()) / 2)); | |
| entity.SetContents(filter); | |
| entity.Render(context, pass); | |
| return true; | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, LinearToSrgbFilterCoverageIsCorrect) { | |
| // Set up a simple color background. | |
| auto fill = std::make_shared<SolidColorContents>(); | |
| fill->SetGeometry(Geometry::MakeFillPath( | |
| PathBuilder{}.AddRect(Rect::MakeXYWH(0, 0, 300, 400)).TakePath())); | |
| fill->SetColor(Color::MintCream()); | |
| auto filter = | |
| ColorFilterContents::MakeLinearToSrgbFilter(FilterInput::Make(fill)); | |
| Entity e; | |
| e.SetTransform(Matrix()); | |
| // Confirm that the actual filter coverage matches the expected coverage. | |
| auto actual = filter->GetCoverage(e); | |
| auto expected = Rect::MakeXYWH(0, 0, 300, 400); | |
| ASSERT_TRUE(actual.has_value()); | |
| ASSERT_RECT_NEAR(actual.value(), expected); | |
| } | |
| TEST_P(EntityTest, LinearToSrgbFilter) { | |
| auto image = CreateTextureForFixture("kalimba.jpg"); | |
| ASSERT_TRUE(image); | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| auto filtered = | |
| ColorFilterContents::MakeLinearToSrgbFilter(FilterInput::Make(image)); | |
| // Define the entity that will serve as the control image as a Gaussian blur | |
| // filter with no filter at all. | |
| Entity entity_left; | |
| entity_left.SetTransform(Matrix::MakeScale(GetContentScale()) * | |
| Matrix::MakeTranslation({100, 300}) * | |
| Matrix::MakeScale(Vector2{0.5, 0.5})); | |
| auto unfiltered = FilterContents::MakeGaussianBlur(FilterInput::Make(image), | |
| Sigma{0}, Sigma{0}); | |
| entity_left.SetContents(unfiltered); | |
| // Define the entity that will be filtered from linear to sRGB. | |
| Entity entity_right; | |
| entity_right.SetTransform(Matrix::MakeScale(GetContentScale()) * | |
| Matrix::MakeTranslation({500, 300}) * | |
| Matrix::MakeScale(Vector2{0.5, 0.5})); | |
| entity_right.SetContents(filtered); | |
| return entity_left.Render(context, pass) && | |
| entity_right.Render(context, pass); | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, SrgbToLinearFilterCoverageIsCorrect) { | |
| // Set up a simple color background. | |
| auto fill = std::make_shared<SolidColorContents>(); | |
| fill->SetGeometry(Geometry::MakeFillPath( | |
| PathBuilder{}.AddRect(Rect::MakeXYWH(0, 0, 300, 400)).TakePath())); | |
| fill->SetColor(Color::DeepPink()); | |
| auto filter = | |
| ColorFilterContents::MakeSrgbToLinearFilter(FilterInput::Make(fill)); | |
| Entity e; | |
| e.SetTransform(Matrix()); | |
| // Confirm that the actual filter coverage matches the expected coverage. | |
| auto actual = filter->GetCoverage(e); | |
| auto expected = Rect::MakeXYWH(0, 0, 300, 400); | |
| ASSERT_TRUE(actual.has_value()); | |
| ASSERT_RECT_NEAR(actual.value(), expected); | |
| } | |
| TEST_P(EntityTest, SrgbToLinearFilter) { | |
| auto image = CreateTextureForFixture("embarcadero.jpg"); | |
| ASSERT_TRUE(image); | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| auto filtered = | |
| ColorFilterContents::MakeSrgbToLinearFilter(FilterInput::Make(image)); | |
| // Define the entity that will serve as the control image as a Gaussian blur | |
| // filter with no filter at all. | |
| Entity entity_left; | |
| entity_left.SetTransform(Matrix::MakeScale(GetContentScale()) * | |
| Matrix::MakeTranslation({100, 300}) * | |
| Matrix::MakeScale(Vector2{0.5, 0.5})); | |
| auto unfiltered = FilterContents::MakeGaussianBlur(FilterInput::Make(image), | |
| Sigma{0}, Sigma{0}); | |
| entity_left.SetContents(unfiltered); | |
| // Define the entity that will be filtered from sRGB to linear. | |
| Entity entity_right; | |
| entity_right.SetTransform(Matrix::MakeScale(GetContentScale()) * | |
| Matrix::MakeTranslation({500, 300}) * | |
| Matrix::MakeScale(Vector2{0.5, 0.5})); | |
| entity_right.SetContents(filtered); | |
| return entity_left.Render(context, pass) && | |
| entity_right.Render(context, pass); | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, AtlasContentsSubAtlas) { | |
| auto boston = CreateTextureForFixture("boston.jpg"); | |
| { | |
| auto contents = std::make_shared<AtlasContents>(); | |
| contents->SetBlendMode(BlendMode::kSourceOver); | |
| contents->SetTexture(boston); | |
| contents->SetColors({ | |
| Color::Red(), | |
| Color::Red(), | |
| Color::Red(), | |
| }); | |
| contents->SetTextureCoordinates({ | |
| Rect::MakeLTRB(0, 0, 10, 10), | |
| Rect::MakeLTRB(0, 0, 10, 10), | |
| Rect::MakeLTRB(0, 0, 10, 10), | |
| }); | |
| contents->SetTransforms({ | |
| Matrix::MakeTranslation(Vector2(0, 0)), | |
| Matrix::MakeTranslation(Vector2(100, 100)), | |
| Matrix::MakeTranslation(Vector2(200, 200)), | |
| }); | |
| // Since all colors and sample rects are the same, there should | |
| // only be a single entry in the sub atlas. | |
| auto subatlas = contents->GenerateSubAtlas(); | |
| ASSERT_EQ(subatlas->sub_texture_coords.size(), 1u); | |
| } | |
| { | |
| auto contents = std::make_shared<AtlasContents>(); | |
| contents->SetBlendMode(BlendMode::kSourceOver); | |
| contents->SetTexture(boston); | |
| contents->SetColors({ | |
| Color::Red(), | |
| Color::Green(), | |
| Color::Blue(), | |
| }); | |
| contents->SetTextureCoordinates({ | |
| Rect::MakeLTRB(0, 0, 10, 10), | |
| Rect::MakeLTRB(0, 0, 10, 10), | |
| Rect::MakeLTRB(0, 0, 10, 10), | |
| }); | |
| contents->SetTransforms({ | |
| Matrix::MakeTranslation(Vector2(0, 0)), | |
| Matrix::MakeTranslation(Vector2(100, 100)), | |
| Matrix::MakeTranslation(Vector2(200, 200)), | |
| }); | |
| // Since all colors are different, there are three entires. | |
| auto subatlas = contents->GenerateSubAtlas(); | |
| ASSERT_EQ(subatlas->sub_texture_coords.size(), 3u); | |
| // The translations are kept but the sample rects point into | |
| // different parts of the sub atlas. | |
| ASSERT_EQ(subatlas->result_texture_coords[0], Rect::MakeXYWH(0, 0, 10, 10)); | |
| ASSERT_EQ(subatlas->result_texture_coords[1], | |
| Rect::MakeXYWH(11, 0, 10, 10)); | |
| ASSERT_EQ(subatlas->result_texture_coords[2], | |
| Rect::MakeXYWH(22, 0, 10, 10)); | |
| } | |
| } | |
| static Vector3 RGBToYUV(Vector3 rgb, YUVColorSpace yuv_color_space) { | |
| Vector3 yuv; | |
| switch (yuv_color_space) { | |
| case YUVColorSpace::kBT601FullRange: | |
| yuv.x = rgb.x * 0.299 + rgb.y * 0.587 + rgb.z * 0.114; | |
| yuv.y = rgb.x * -0.169 + rgb.y * -0.331 + rgb.z * 0.5 + 0.5; | |
| yuv.z = rgb.x * 0.5 + rgb.y * -0.419 + rgb.z * -0.081 + 0.5; | |
| break; | |
| case YUVColorSpace::kBT601LimitedRange: | |
| yuv.x = rgb.x * 0.257 + rgb.y * 0.516 + rgb.z * 0.100 + 0.063; | |
| yuv.y = rgb.x * -0.145 + rgb.y * -0.291 + rgb.z * 0.439 + 0.5; | |
| yuv.z = rgb.x * 0.429 + rgb.y * -0.368 + rgb.z * -0.071 + 0.5; | |
| break; | |
| } | |
| return yuv; | |
| } | |
| static std::vector<std::shared_ptr<Texture>> CreateTestYUVTextures( | |
| Context* context, | |
| YUVColorSpace yuv_color_space) { | |
| Vector3 red = {244.0 / 255.0, 67.0 / 255.0, 54.0 / 255.0}; | |
| Vector3 green = {76.0 / 255.0, 175.0 / 255.0, 80.0 / 255.0}; | |
| Vector3 blue = {33.0 / 255.0, 150.0 / 255.0, 243.0 / 255.0}; | |
| Vector3 white = {1.0, 1.0, 1.0}; | |
| Vector3 red_yuv = RGBToYUV(red, yuv_color_space); | |
| Vector3 green_yuv = RGBToYUV(green, yuv_color_space); | |
| Vector3 blue_yuv = RGBToYUV(blue, yuv_color_space); | |
| Vector3 white_yuv = RGBToYUV(white, yuv_color_space); | |
| std::vector<Vector3> yuvs{red_yuv, green_yuv, blue_yuv, white_yuv}; | |
| std::vector<uint8_t> y_data; | |
| std::vector<uint8_t> uv_data; | |
| for (int i = 0; i < 4; i++) { | |
| auto yuv = yuvs[i]; | |
| uint8_t y = std::round(yuv.x * 255.0); | |
| uint8_t u = std::round(yuv.y * 255.0); | |
| uint8_t v = std::round(yuv.z * 255.0); | |
| for (int j = 0; j < 16; j++) { | |
| y_data.push_back(y); | |
| } | |
| for (int j = 0; j < 8; j++) { | |
| uv_data.push_back(j % 2 == 0 ? u : v); | |
| } | |
| } | |
| impeller::TextureDescriptor y_texture_descriptor; | |
| y_texture_descriptor.storage_mode = impeller::StorageMode::kHostVisible; | |
| y_texture_descriptor.format = PixelFormat::kR8UNormInt; | |
| y_texture_descriptor.size = {8, 8}; | |
| auto y_texture = | |
| context->GetResourceAllocator()->CreateTexture(y_texture_descriptor); | |
| auto y_mapping = std::make_shared<fml::DataMapping>(y_data); | |
| if (!y_texture->SetContents(y_mapping)) { | |
| FML_DLOG(ERROR) << "Could not copy contents into Y texture."; | |
| } | |
| impeller::TextureDescriptor uv_texture_descriptor; | |
| uv_texture_descriptor.storage_mode = impeller::StorageMode::kHostVisible; | |
| uv_texture_descriptor.format = PixelFormat::kR8G8UNormInt; | |
| uv_texture_descriptor.size = {4, 4}; | |
| auto uv_texture = | |
| context->GetResourceAllocator()->CreateTexture(uv_texture_descriptor); | |
| auto uv_mapping = std::make_shared<fml::DataMapping>(uv_data); | |
| if (!uv_texture->SetContents(uv_mapping)) { | |
| FML_DLOG(ERROR) << "Could not copy contents into UV texture."; | |
| } | |
| return {y_texture, uv_texture}; | |
| } | |
| TEST_P(EntityTest, YUVToRGBFilter) { | |
| if (GetParam() == PlaygroundBackend::kOpenGLES) { | |
| // TODO(114588) : Support YUV to RGB filter on OpenGLES backend. | |
| GTEST_SKIP_("YUV to RGB filter is not supported on OpenGLES backend yet."); | |
| } | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| YUVColorSpace yuv_color_space_array[2]{YUVColorSpace::kBT601FullRange, | |
| YUVColorSpace::kBT601LimitedRange}; | |
| for (int i = 0; i < 2; i++) { | |
| auto yuv_color_space = yuv_color_space_array[i]; | |
| auto textures = | |
| CreateTestYUVTextures(GetContext().get(), yuv_color_space); | |
| auto filter_contents = FilterContents::MakeYUVToRGBFilter( | |
| textures[0], textures[1], yuv_color_space); | |
| Entity filter_entity; | |
| filter_entity.SetContents(filter_contents); | |
| auto snapshot = filter_contents->RenderToSnapshot(context, filter_entity); | |
| Entity entity; | |
| auto contents = TextureContents::MakeRect(Rect::MakeLTRB(0, 0, 256, 256)); | |
| contents->SetTexture(snapshot->texture); | |
| contents->SetSourceRect(Rect::MakeSize(snapshot->texture->GetSize())); | |
| entity.SetContents(contents); | |
| entity.SetTransform( | |
| Matrix::MakeTranslation({static_cast<Scalar>(100 + 400 * i), 300})); | |
| entity.Render(context, pass); | |
| } | |
| return true; | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, RuntimeEffect) { | |
| auto runtime_stages = | |
| OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr"); | |
| auto runtime_stage = | |
| runtime_stages[PlaygroundBackendToRuntimeStageBackend(GetBackend())]; | |
| ASSERT_TRUE(runtime_stage); | |
| ASSERT_TRUE(runtime_stage->IsDirty()); | |
| bool expect_dirty = true; | |
| Pipeline<PipelineDescriptor>* first_pipeline; | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| EXPECT_EQ(runtime_stage->IsDirty(), expect_dirty); | |
| auto contents = std::make_shared<RuntimeEffectContents>(); | |
| contents->SetGeometry(Geometry::MakeCover()); | |
| contents->SetRuntimeStage(runtime_stage); | |
| struct FragUniforms { | |
| Vector2 iResolution; | |
| Scalar iTime; | |
| } frag_uniforms = { | |
| .iResolution = Vector2(GetWindowSize().width, GetWindowSize().height), | |
| .iTime = static_cast<Scalar>(GetSecondsElapsed()), | |
| }; | |
| auto uniform_data = std::make_shared<std::vector<uint8_t>>(); | |
| uniform_data->resize(sizeof(FragUniforms)); | |
| memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms)); | |
| contents->SetUniformData(uniform_data); | |
| Entity entity; | |
| entity.SetContents(contents); | |
| bool result = contents->Render(context, entity, pass); | |
| if (expect_dirty) { | |
| EXPECT_NE(first_pipeline, pass.GetCommands().back().pipeline.get()); | |
| first_pipeline = pass.GetCommands().back().pipeline.get(); | |
| } else { | |
| EXPECT_EQ(pass.GetCommands().back().pipeline.get(), first_pipeline); | |
| } | |
| expect_dirty = false; | |
| return result; | |
| }; | |
| // Simulate some renders and hot reloading of the shader. | |
| auto content_context = GetContentContext(); | |
| { | |
| RenderTarget target = | |
| content_context->GetRenderTargetCache()->CreateOffscreen( | |
| *content_context->GetContext(), {1, 1}, 1u); | |
| testing::MockRenderPass mock_pass(GetContext(), target); | |
| callback(*content_context, mock_pass); | |
| callback(*content_context, mock_pass); | |
| // Dirty the runtime stage. | |
| runtime_stages = OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr"); | |
| runtime_stage = | |
| runtime_stages[PlaygroundBackendToRuntimeStageBackend(GetBackend())]; | |
| ASSERT_TRUE(runtime_stage->IsDirty()); | |
| expect_dirty = true; | |
| callback(*content_context, mock_pass); | |
| } | |
| } | |
| TEST_P(EntityTest, RuntimeEffectCanSuccessfullyRender) { | |
| auto runtime_stages = | |
| OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr"); | |
| auto runtime_stage = | |
| runtime_stages[PlaygroundBackendToRuntimeStageBackend(GetBackend())]; | |
| ASSERT_TRUE(runtime_stage); | |
| ASSERT_TRUE(runtime_stage->IsDirty()); | |
| auto contents = std::make_shared<RuntimeEffectContents>(); | |
| contents->SetGeometry(Geometry::MakeCover()); | |
| contents->SetRuntimeStage(runtime_stage); | |
| struct FragUniforms { | |
| Vector2 iResolution; | |
| Scalar iTime; | |
| } frag_uniforms = { | |
| .iResolution = Vector2(GetWindowSize().width, GetWindowSize().height), | |
| .iTime = static_cast<Scalar>(GetSecondsElapsed()), | |
| }; | |
| auto uniform_data = std::make_shared<std::vector<uint8_t>>(); | |
| uniform_data->resize(sizeof(FragUniforms)); | |
| memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms)); | |
| contents->SetUniformData(uniform_data); | |
| Entity entity; | |
| entity.SetContents(contents); | |
| // Create a render target with a depth-stencil, similar to how EntityPass | |
| // does. | |
| RenderTarget target = | |
| GetContentContext()->GetRenderTargetCache()->CreateOffscreenMSAA( | |
| *GetContext(), {GetWindowSize().width, GetWindowSize().height}, 1, | |
| "RuntimeEffect Texture"); | |
| testing::MockRenderPass pass(GetContext(), target); | |
| ASSERT_TRUE(contents->Render(*GetContentContext(), entity, pass)); | |
| ASSERT_EQ(pass.GetCommands().size(), 1u); | |
| const auto& command = pass.GetCommands()[0]; | |
| ASSERT_TRUE(command.pipeline->GetDescriptor() | |
| .GetDepthStencilAttachmentDescriptor() | |
| .has_value()); | |
| ASSERT_TRUE(command.pipeline->GetDescriptor() | |
| .GetFrontStencilAttachmentDescriptor() | |
| .has_value()); | |
| } | |
| TEST_P(EntityTest, RuntimeEffectSetsRightSizeWhenUniformIsStruct) { | |
| if (GetBackend() != PlaygroundBackend::kVulkan) { | |
| GTEST_SKIP() << "Test only applies to Vulkan"; | |
| } | |
| auto runtime_stages = | |
| OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr"); | |
| auto runtime_stage = | |
| runtime_stages[PlaygroundBackendToRuntimeStageBackend(GetBackend())]; | |
| ASSERT_TRUE(runtime_stage); | |
| ASSERT_TRUE(runtime_stage->IsDirty()); | |
| auto contents = std::make_shared<RuntimeEffectContents>(); | |
| contents->SetGeometry(Geometry::MakeCover()); | |
| contents->SetRuntimeStage(runtime_stage); | |
| struct FragUniforms { | |
| Vector2 iResolution; | |
| Scalar iTime; | |
| } frag_uniforms = { | |
| .iResolution = Vector2(GetWindowSize().width, GetWindowSize().height), | |
| .iTime = static_cast<Scalar>(GetSecondsElapsed()), | |
| }; | |
| auto uniform_data = std::make_shared<std::vector<uint8_t>>(); | |
| uniform_data->resize(sizeof(FragUniforms)); | |
| memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms)); | |
| contents->SetUniformData(uniform_data); | |
| Entity entity; | |
| entity.SetContents(contents); | |
| auto context = GetContentContext(); | |
| RenderTarget target = context->GetRenderTargetCache()->CreateOffscreen( | |
| *context->GetContext(), {1, 1}, 1u); | |
| testing::MockRenderPass pass(GetContext(), target); | |
| ASSERT_TRUE(contents->Render(*context, entity, pass)); | |
| ASSERT_EQ(pass.GetCommands().size(), 1u); | |
| const auto& command = pass.GetCommands()[0]; | |
| ASSERT_EQ(command.fragment_bindings.buffers.size(), 1u); | |
| // 16 bytes: | |
| // 8 bytes for iResolution | |
| // 4 bytes for iTime | |
| // 4 bytes padding | |
| EXPECT_EQ(command.fragment_bindings.buffers[0].view.resource.range.length, | |
| 16u); | |
| } | |
| TEST_P(EntityTest, InheritOpacityTest) { | |
| Entity entity; | |
| // Texture contents can always accept opacity. | |
| auto texture_contents = std::make_shared<TextureContents>(); | |
| texture_contents->SetOpacity(0.5); | |
| ASSERT_TRUE(texture_contents->CanInheritOpacity(entity)); | |
| texture_contents->SetInheritedOpacity(0.5); | |
| ASSERT_EQ(texture_contents->GetOpacity(), 0.25); | |
| texture_contents->SetInheritedOpacity(0.5); | |
| ASSERT_EQ(texture_contents->GetOpacity(), 0.25); | |
| // Solid color contents can accept opacity if their geometry | |
| // doesn't overlap. | |
| auto solid_color = std::make_shared<SolidColorContents>(); | |
| solid_color->SetGeometry( | |
| Geometry::MakeRect(Rect::MakeLTRB(100, 100, 200, 200))); | |
| solid_color->SetColor(Color::Blue().WithAlpha(0.5)); | |
| ASSERT_TRUE(solid_color->CanInheritOpacity(entity)); | |
| solid_color->SetInheritedOpacity(0.5); | |
| ASSERT_EQ(solid_color->GetColor().alpha, 0.25); | |
| solid_color->SetInheritedOpacity(0.5); | |
| ASSERT_EQ(solid_color->GetColor().alpha, 0.25); | |
| // Color source contents can accept opacity if their geometry | |
| // doesn't overlap. | |
| auto tiled_texture = std::make_shared<TiledTextureContents>(); | |
| tiled_texture->SetGeometry( | |
| Geometry::MakeRect(Rect::MakeLTRB(100, 100, 200, 200))); | |
| tiled_texture->SetOpacityFactor(0.5); | |
| ASSERT_TRUE(tiled_texture->CanInheritOpacity(entity)); | |
| tiled_texture->SetInheritedOpacity(0.5); | |
| ASSERT_EQ(tiled_texture->GetOpacityFactor(), 0.25); | |
| tiled_texture->SetInheritedOpacity(0.5); | |
| ASSERT_EQ(tiled_texture->GetOpacityFactor(), 0.25); | |
| // Text contents can accept opacity if the text frames do not | |
| // overlap | |
| SkFont font = flutter::testing::CreateTestFontOfSize(30); | |
| auto blob = SkTextBlob::MakeFromString("A", font); | |
| auto frame = MakeTextFrameFromTextBlobSkia(blob); | |
| auto lazy_glyph_atlas = | |
| std::make_shared<LazyGlyphAtlas>(TypographerContextSkia::Make()); | |
| lazy_glyph_atlas->AddTextFrame(*frame, 1.0f); | |
| auto text_contents = std::make_shared<TextContents>(); | |
| text_contents->SetTextFrame(frame); | |
| text_contents->SetColor(Color::Blue().WithAlpha(0.5)); | |
| ASSERT_TRUE(text_contents->CanInheritOpacity(entity)); | |
| text_contents->SetInheritedOpacity(0.5); | |
| ASSERT_EQ(text_contents->GetColor().alpha, 0.25); | |
| text_contents->SetInheritedOpacity(0.5); | |
| ASSERT_EQ(text_contents->GetColor().alpha, 0.25); | |
| // Clips and restores trivially accept opacity. | |
| ASSERT_TRUE(ClipContents().CanInheritOpacity(entity)); | |
| ASSERT_TRUE(ClipRestoreContents().CanInheritOpacity(entity)); | |
| // Runtime effect contents can't accept opacity. | |
| auto runtime_effect = std::make_shared<RuntimeEffectContents>(); | |
| ASSERT_FALSE(runtime_effect->CanInheritOpacity(entity)); | |
| } | |
| TEST_P(EntityTest, ColorFilterWithForegroundColorAdvancedBlend) { | |
| auto image = CreateTextureForFixture("boston.jpg"); | |
| auto filter = ColorFilterContents::MakeBlend( | |
| BlendMode::kColorBurn, FilterInput::Make({image}), Color::Red()); | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale()) * | |
| Matrix::MakeTranslation({500, 300}) * | |
| Matrix::MakeScale(Vector2{0.5, 0.5})); | |
| entity.SetContents(filter); | |
| return entity.Render(context, pass); | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, ColorFilterWithForegroundColorClearBlend) { | |
| auto image = CreateTextureForFixture("boston.jpg"); | |
| auto filter = ColorFilterContents::MakeBlend( | |
| BlendMode::kClear, FilterInput::Make({image}), Color::Red()); | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale()) * | |
| Matrix::MakeTranslation({500, 300}) * | |
| Matrix::MakeScale(Vector2{0.5, 0.5})); | |
| entity.SetContents(filter); | |
| return entity.Render(context, pass); | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, ColorFilterWithForegroundColorSrcBlend) { | |
| auto image = CreateTextureForFixture("boston.jpg"); | |
| auto filter = ColorFilterContents::MakeBlend( | |
| BlendMode::kSource, FilterInput::Make({image}), Color::Red()); | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale()) * | |
| Matrix::MakeTranslation({500, 300}) * | |
| Matrix::MakeScale(Vector2{0.5, 0.5})); | |
| entity.SetContents(filter); | |
| return entity.Render(context, pass); | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, ColorFilterWithForegroundColorDstBlend) { | |
| auto image = CreateTextureForFixture("boston.jpg"); | |
| auto filter = ColorFilterContents::MakeBlend( | |
| BlendMode::kDestination, FilterInput::Make({image}), Color::Red()); | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale()) * | |
| Matrix::MakeTranslation({500, 300}) * | |
| Matrix::MakeScale(Vector2{0.5, 0.5})); | |
| entity.SetContents(filter); | |
| return entity.Render(context, pass); | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, ColorFilterWithForegroundColorSrcInBlend) { | |
| auto image = CreateTextureForFixture("boston.jpg"); | |
| auto filter = ColorFilterContents::MakeBlend( | |
| BlendMode::kSourceIn, FilterInput::Make({image}), Color::Red()); | |
| auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale()) * | |
| Matrix::MakeTranslation({500, 300}) * | |
| Matrix::MakeScale(Vector2{0.5, 0.5})); | |
| entity.SetContents(filter); | |
| return entity.Render(context, pass); | |
| }; | |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); | |
| } | |
| TEST_P(EntityTest, CoverageForStrokePathWithNegativeValuesInTransform) { | |
| auto arrow_head = PathBuilder{} | |
| .MoveTo({50, 120}) | |
| .LineTo({120, 190}) | |
| .LineTo({190, 120}) | |
| .TakePath(); | |
| auto geometry = Geometry::MakeStrokePath(arrow_head, 15.0, 4.0, Cap::kRound, | |
| Join::kRound); | |
| auto transform = Matrix::MakeTranslation({300, 300}) * | |
| Matrix::MakeRotationZ(Radians(kPiOver2)); | |
| // Note that e[0][0] used to be tested here, but it was -epsilon solely | |
| // due to floating point inaccuracy in the transcendental trig functions. | |
| // e[1][0] is the intended negative value that we care about (-1.0) as it | |
| // comes from the rotation of pi/2. | |
| EXPECT_LT(transform.e[1][0], 0.0f); | |
| auto coverage = geometry->GetCoverage(transform); | |
| ASSERT_RECT_NEAR(coverage.value(), Rect::MakeXYWH(102.5, 342.5, 85, 155)); | |
| } | |
| TEST_P(EntityTest, SolidColorContentsIsOpaque) { | |
| SolidColorContents contents; | |
| contents.SetColor(Color::CornflowerBlue()); | |
| ASSERT_TRUE(contents.IsOpaque()); | |
| contents.SetColor(Color::CornflowerBlue().WithAlpha(0.5)); | |
| ASSERT_FALSE(contents.IsOpaque()); | |
| } | |
| TEST_P(EntityTest, ConicalGradientContentsIsOpaque) { | |
| ConicalGradientContents contents; | |
| contents.SetColors({Color::CornflowerBlue()}); | |
| ASSERT_FALSE(contents.IsOpaque()); | |
| contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)}); | |
| ASSERT_FALSE(contents.IsOpaque()); | |
| } | |
| TEST_P(EntityTest, LinearGradientContentsIsOpaque) { | |
| LinearGradientContents contents; | |
| contents.SetColors({Color::CornflowerBlue()}); | |
| ASSERT_TRUE(contents.IsOpaque()); | |
| contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)}); | |
| ASSERT_FALSE(contents.IsOpaque()); | |
| contents.SetColors({Color::CornflowerBlue()}); | |
| contents.SetTileMode(Entity::TileMode::kDecal); | |
| ASSERT_FALSE(contents.IsOpaque()); | |
| } | |
| TEST_P(EntityTest, RadialGradientContentsIsOpaque) { | |
| RadialGradientContents contents; | |
| contents.SetColors({Color::CornflowerBlue()}); | |
| ASSERT_TRUE(contents.IsOpaque()); | |
| contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)}); | |
| ASSERT_FALSE(contents.IsOpaque()); | |
| contents.SetColors({Color::CornflowerBlue()}); | |
| contents.SetTileMode(Entity::TileMode::kDecal); | |
| ASSERT_FALSE(contents.IsOpaque()); | |
| } | |
| TEST_P(EntityTest, SweepGradientContentsIsOpaque) { | |
| RadialGradientContents contents; | |
| contents.SetColors({Color::CornflowerBlue()}); | |
| ASSERT_TRUE(contents.IsOpaque()); | |
| contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)}); | |
| ASSERT_FALSE(contents.IsOpaque()); | |
| contents.SetColors({Color::CornflowerBlue()}); | |
| contents.SetTileMode(Entity::TileMode::kDecal); | |
| ASSERT_FALSE(contents.IsOpaque()); | |
| } | |
| TEST_P(EntityTest, TiledTextureContentsIsOpaque) { | |
| auto bay_bridge = CreateTextureForFixture("bay_bridge.jpg"); | |
| TiledTextureContents contents; | |
| contents.SetTexture(bay_bridge); | |
| // This is a placeholder test. Images currently never decompress as opaque | |
| // (whether in Flutter or the playground), and so this should currently always | |
| // return false in practice. | |
| ASSERT_FALSE(contents.IsOpaque()); | |
| } | |
| TEST_P(EntityTest, PointFieldGeometryDivisions) { | |
| // Square always gives 4 divisions. | |
| ASSERT_EQ(PointFieldGeometry::ComputeCircleDivisions(24.0, false), 4u); | |
| ASSERT_EQ(PointFieldGeometry::ComputeCircleDivisions(2.0, false), 4u); | |
| ASSERT_EQ(PointFieldGeometry::ComputeCircleDivisions(200.0, false), 4u); | |
| ASSERT_EQ(PointFieldGeometry::ComputeCircleDivisions(0.5, true), 4u); | |
| ASSERT_EQ(PointFieldGeometry::ComputeCircleDivisions(1.5, true), 8u); | |
| ASSERT_EQ(PointFieldGeometry::ComputeCircleDivisions(5.5, true), 24u); | |
| ASSERT_EQ(PointFieldGeometry::ComputeCircleDivisions(12.5, true), 34u); | |
| ASSERT_EQ(PointFieldGeometry::ComputeCircleDivisions(22.3, true), 22u); | |
| ASSERT_EQ(PointFieldGeometry::ComputeCircleDivisions(40.5, true), 40u); | |
| ASSERT_EQ(PointFieldGeometry::ComputeCircleDivisions(100.0, true), 100u); | |
| // Caps at 140. | |
| ASSERT_EQ(PointFieldGeometry::ComputeCircleDivisions(1000.0, true), 140u); | |
| ASSERT_EQ(PointFieldGeometry::ComputeCircleDivisions(20000.0, true), 140u); | |
| } | |
| TEST_P(EntityTest, PointFieldGeometryCoverage) { | |
| std::vector<Point> points = {{10, 20}, {100, 200}}; | |
| auto geometry = Geometry::MakePointField(points, 5.0, false); | |
| ASSERT_EQ(*geometry->GetCoverage(Matrix()), Rect::MakeLTRB(5, 15, 105, 205)); | |
| ASSERT_EQ(*geometry->GetCoverage(Matrix::MakeTranslation({30, 0, 0})), | |
| Rect::MakeLTRB(35, 15, 135, 205)); | |
| } | |
| TEST_P(EntityTest, ColorFilterContentsWithLargeGeometry) { | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| auto src_contents = std::make_shared<SolidColorContents>(); | |
| src_contents->SetGeometry( | |
| Geometry::MakeRect(Rect::MakeLTRB(-300, -500, 30000, 50000))); | |
| src_contents->SetColor(Color::Red()); | |
| auto dst_contents = std::make_shared<SolidColorContents>(); | |
| dst_contents->SetGeometry( | |
| Geometry::MakeRect(Rect::MakeLTRB(300, 500, 20000, 30000))); | |
| dst_contents->SetColor(Color::Blue()); | |
| auto contents = ColorFilterContents::MakeBlend( | |
| BlendMode::kSourceOver, {FilterInput::Make(dst_contents, false), | |
| FilterInput::Make(src_contents, false)}); | |
| entity.SetContents(std::move(contents)); | |
| ASSERT_TRUE(OpenPlaygroundHere(std::move(entity))); | |
| } | |
| TEST_P(EntityTest, TextContentsCeilsGlyphScaleToDecimal) { | |
| ASSERT_EQ(TextFrame::RoundScaledFontSize(0.4321111f, 12), 0.43f); | |
| ASSERT_EQ(TextFrame::RoundScaledFontSize(0.5321111f, 12), 0.53f); | |
| ASSERT_EQ(TextFrame::RoundScaledFontSize(2.1f, 12), 2.1f); | |
| ASSERT_EQ(TextFrame::RoundScaledFontSize(0.0f, 12), 0.0f); | |
| } | |
| TEST_P(EntityTest, AdvancedBlendCoverageHintIsNotResetByEntityPass) { | |
| if (GetContext()->GetCapabilities()->SupportsFramebufferFetch()) { | |
| GTEST_SKIP() << "Backends that support framebuffer fetch dont use coverage " | |
| "for advanced blends."; | |
| } | |
| auto contents = std::make_shared<SolidColorContents>(); | |
| contents->SetGeometry(Geometry::MakeRect(Rect::MakeXYWH(100, 100, 100, 100))); | |
| contents->SetColor(Color::Red()); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(Vector3(2, 2, 1))); | |
| entity.SetBlendMode(BlendMode::kColorBurn); | |
| entity.SetContents(contents); | |
| auto coverage = entity.GetCoverage(); | |
| EXPECT_TRUE(coverage.has_value()); | |
| auto pass = std::make_unique<EntityPass>(); | |
| std::shared_ptr<RenderTargetCache> render_target_allocator = | |
| std::make_shared<RenderTargetCache>(GetContext()->GetResourceAllocator()); | |
| auto stencil_config = RenderTarget::AttachmentConfig{ | |
| .storage_mode = StorageMode::kDevicePrivate, | |
| .load_action = LoadAction::kClear, | |
| .store_action = StoreAction::kDontCare, | |
| .clear_color = Color::BlackTransparent()}; | |
| auto rt = render_target_allocator->CreateOffscreen( | |
| *GetContext(), ISize::MakeWH(1000, 1000), | |
| /*mip_count=*/1, "Offscreen", RenderTarget::kDefaultColorAttachmentConfig, | |
| stencil_config); | |
| auto content_context = ContentContext( | |
| GetContext(), TypographerContextSkia::Make(), render_target_allocator); | |
| pass->AddEntity(std::move(entity)); | |
| EXPECT_TRUE(pass->Render(content_context, rt)); | |
| auto contains_size = [&render_target_allocator](ISize size) -> bool { | |
| return std::find_if(render_target_allocator->GetRenderTargetDataBegin(), | |
| render_target_allocator->GetRenderTargetDataEnd(), | |
| [&size](const auto& data) { | |
| return data.config.size == size; | |
| }) != render_target_allocator->GetRenderTargetDataEnd(); | |
| }; | |
| EXPECT_TRUE(contains_size(ISize(1000, 1000))) | |
| << "The root texture wasn't allocated"; | |
| EXPECT_TRUE(contains_size(ISize(200, 200))) | |
| << "The ColorBurned texture wasn't allocated (100x100 scales up 2x)"; | |
| } | |
| TEST_P(EntityTest, SpecializationConstantsAreAppliedToVariants) { | |
| auto content_context = GetContentContext(); | |
| auto default_color_burn = content_context->GetBlendColorBurnPipeline({ | |
| .color_attachment_pixel_format = PixelFormat::kR8G8B8A8UNormInt, | |
| .has_depth_stencil_attachments = false, | |
| }); | |
| auto alt_color_burn = content_context->GetBlendColorBurnPipeline( | |
| {.color_attachment_pixel_format = PixelFormat::kR8G8B8A8UNormInt, | |
| .has_depth_stencil_attachments = true}); | |
| ASSERT_NE(default_color_burn, alt_color_burn); | |
| ASSERT_EQ(default_color_burn->GetDescriptor().GetSpecializationConstants(), | |
| alt_color_burn->GetDescriptor().GetSpecializationConstants()); | |
| auto decal_supported = static_cast<Scalar>( | |
| GetContext()->GetCapabilities()->SupportsDecalSamplerAddressMode()); | |
| std::vector<Scalar> expected_constants = {5, decal_supported}; | |
| ASSERT_EQ(default_color_burn->GetDescriptor().GetSpecializationConstants(), | |
| expected_constants); | |
| } | |
| TEST_P(EntityTest, DecalSpecializationAppliedToMorphologyFilter) { | |
| auto content_context = GetContentContext(); | |
| auto default_color_burn = content_context->GetMorphologyFilterPipeline({ | |
| .color_attachment_pixel_format = PixelFormat::kR8G8B8A8UNormInt, | |
| }); | |
| auto decal_supported = static_cast<Scalar>( | |
| GetContext()->GetCapabilities()->SupportsDecalSamplerAddressMode()); | |
| std::vector<Scalar> expected_constants = {decal_supported}; | |
| ASSERT_EQ(default_color_burn->GetDescriptor().GetSpecializationConstants(), | |
| expected_constants); | |
| } | |
| // This doesn't really tell you if the hashes will have frequent | |
| // collisions, but since this type is only used to hash a bounded | |
| // set of options, we can just compare benchmarks. | |
| TEST_P(EntityTest, ContentContextOptionsHasReasonableHashFunctions) { | |
| ContentContextOptions opts; | |
| auto hash_a = ContentContextOptions::Hash{}(opts); | |
| opts.blend_mode = BlendMode::kColorBurn; | |
| auto hash_b = ContentContextOptions::Hash{}(opts); | |
| opts.has_depth_stencil_attachments = false; | |
| auto hash_c = ContentContextOptions::Hash{}(opts); | |
| opts.primitive_type = PrimitiveType::kPoint; | |
| auto hash_d = ContentContextOptions::Hash{}(opts); | |
| EXPECT_NE(hash_a, hash_b); | |
| EXPECT_NE(hash_b, hash_c); | |
| EXPECT_NE(hash_c, hash_d); | |
| } | |
| #ifdef FML_OS_LINUX | |
| TEST_P(EntityTest, FramebufferFetchVulkanBindingOffsetIsTheSame) { | |
| // Using framebuffer fetch on Vulkan requires that we maintain a subpass input | |
| // binding that we don't have a good route for configuring with the current | |
| // metadata approach. This test verifies that the binding value doesn't change | |
| // from the expected constant. | |
| // See also: | |
| // * impeller/renderer/backend/vulkan/binding_helpers_vk.cc | |
| // * impeller/entity/shaders/blending/framebuffer_blend.frag | |
| // This test only works on Linux because macOS hosts incorrectly populate the | |
| // Vulkan descriptor sets based on the MSL compiler settings. | |
| bool expected_layout = false; | |
| for (const DescriptorSetLayout& layout : FramebufferBlendColorBurnPipeline:: | |
| FragmentShader::kDescriptorSetLayouts) { | |
| if (layout.binding == 64 && | |
| layout.descriptor_type == DescriptorType::kInputAttachment) { | |
| expected_layout = true; | |
| } | |
| } | |
| EXPECT_TRUE(expected_layout); | |
| } | |
| #endif | |
| TEST_P(EntityTest, FillPathGeometryGetPositionBufferReturnsExpectedMode) { | |
| RenderTarget target; | |
| testing::MockRenderPass mock_pass(GetContext(), target); | |
| auto get_result = [this, &mock_pass](const Path& path) { | |
| auto geometry = Geometry::MakeFillPath( | |
| path, /* inner rect */ Rect::MakeLTRB(0, 0, 100, 100)); | |
| return geometry->GetPositionBuffer(*GetContentContext(), {}, mock_pass); | |
| }; | |
| // Convex path | |
| { | |
| GeometryResult result = | |
| get_result(PathBuilder{} | |
| .AddRect(Rect::MakeLTRB(0, 0, 100, 100)) | |
| .SetConvexity(Convexity::kConvex) | |
| .TakePath()); | |
| EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal); | |
| } | |
| // Concave path | |
| { | |
| Path path = PathBuilder{} | |
| .MoveTo({0, 0}) | |
| .LineTo({100, 0}) | |
| .LineTo({100, 100}) | |
| .LineTo({50, 50}) | |
| .Close() | |
| .TakePath(); | |
| GeometryResult result = get_result(path); | |
| EXPECT_EQ(result.mode, GeometryResult::Mode::kNonZero); | |
| } | |
| } | |
| TEST_P(EntityTest, FailOnValidationError) { | |
| if (GetParam() != PlaygroundBackend::kVulkan) { | |
| GTEST_SKIP() << "Validation is only fatal on Vulkan backend."; | |
| } | |
| EXPECT_DEATH( | |
| // The easiest way to trigger a validation error is to try to compile | |
| // a shader with an unsupported pixel format. | |
| GetContentContext()->GetBlendColorBurnPipeline({ | |
| .color_attachment_pixel_format = PixelFormat::kUnknown, | |
| .has_depth_stencil_attachments = false, | |
| }), | |
| ""); | |
| } | |
| TEST_P(EntityTest, CanComputeGeometryForEmptyPathsWithoutCrashing) { | |
| PathBuilder builder = {}; | |
| builder.AddRect(Rect::MakeLTRB(0, 0, 0, 0)); | |
| Path path = builder.TakePath(); | |
| EXPECT_TRUE(path.GetBoundingBox()->IsEmpty()); | |
| auto geom = Geometry::MakeFillPath(path); | |
| Entity entity; | |
| RenderTarget target = | |
| GetContentContext()->GetRenderTargetCache()->CreateOffscreen( | |
| *GetContext(), {1, 1}, 1u); | |
| testing::MockRenderPass render_pass(GetContext(), target); | |
| auto position_result = | |
| geom->GetPositionBuffer(*GetContentContext(), entity, render_pass); | |
| auto uv_result = | |
| geom->GetPositionUVBuffer(Rect::MakeLTRB(0, 0, 100, 100), Matrix(), | |
| *GetContentContext(), entity, render_pass); | |
| EXPECT_EQ(position_result.vertex_buffer.vertex_count, 0u); | |
| EXPECT_EQ(uv_result.vertex_buffer.vertex_count, 0u); | |
| EXPECT_EQ(geom->GetResultMode(), GeometryResult::Mode::kNormal); | |
| } | |
| TEST_P(EntityTest, CanRenderEmptyPathsWithoutCrashing) { | |
| PathBuilder builder = {}; | |
| builder.AddRect(Rect::MakeLTRB(0, 0, 0, 0)); | |
| Path path = builder.TakePath(); | |
| EXPECT_TRUE(path.GetBoundingBox()->IsEmpty()); | |
| auto contents = std::make_shared<SolidColorContents>(); | |
| contents->SetGeometry(Geometry::MakeFillPath(path)); | |
| contents->SetColor(Color::Red()); | |
| Entity entity; | |
| entity.SetTransform(Matrix::MakeScale(GetContentScale())); | |
| entity.SetContents(contents); | |
| ASSERT_TRUE(OpenPlaygroundHere(std::move(entity))); | |
| } | |
| } // namespace testing | |
| } // namespace impeller | |
| // NOLINTEND(bugprone-unchecked-optional-access) |