blob: 60debd2841f12cefc2041c7d64c3330ef7c477be [file] [log] [blame] [edit]
// 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,
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);
return subpass;
}
TEST_P(EntityTest, EntityPassRespectsSubpassBoundsLimit) {
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, 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), 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", &current_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 = nullptr;
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;
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);
callback(*content_context, mock_pass);
}
ASSERT_TRUE(OpenPlaygroundHere(callback));
}
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;
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));
EXPECT_LT(transform.e[0][0], 0.f);
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 =
ContentContext(GetContext(), TypographerContextSkia::Make());
auto default_color_burn = content_context.GetBlendColorBurnPipeline(
{.has_depth_stencil_attachments = false});
auto alt_color_burn = content_context.GetBlendColorBurnPipeline(
{.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 =
ContentContext(GetContext(), TypographerContextSkia::Make());
auto default_color_burn = content_context.GetMorphologyFilterPipeline({});
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);
if constexpr (ContentContext::kEnableStencilThenCover) {
EXPECT_EQ(result.mode, GeometryResult::Mode::kNonZero);
} else {
EXPECT_EQ(result.mode, GeometryResult::Mode::kNormal);
}
}
}
} // namespace testing
} // namespace impeller
// NOLINTEND(bugprone-unchecked-optional-access)