Flutter Impeller
aiks_dl_primitive_shape_unittests.cc
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "display_list/display_list.h"
7 
8 #include "flutter/display_list/dl_builder.h"
9 #include "flutter/display_list/dl_color.h"
10 #include "flutter/display_list/dl_paint.h"
13 #include "flutter/testing/display_list_testing.h"
14 #include "flutter/testing/testing.h"
15 #include "imgui.h"
17 
18 // The tests in this file check the primitive rendering operations to make
19 // sure that they produce correct output, particularly for strokes and
20 // especially for hairline strokes, under a variety of transforms.
21 
22 namespace {
23 
24 using flutter::DisplayList;
25 using flutter::DisplayListBuilder;
26 using flutter::DlBlendMode;
27 using flutter::DlColor;
28 using flutter::DlDrawStyle;
29 using flutter::DlPaint;
30 using flutter::DlPoint;
31 using flutter::DlRect;
32 using flutter::DlScalar;
33 
34 enum class RenderType {
35  kSquare,
36  kRectangle,
37  kCircle,
38  kOval,
39  kLine,
40 
41  // kValidCount casts to the number of valid enum values.
42  kValidCount,
43  // kInvalid is used as a default initializer to force construction override.
44  kInvalid,
45 };
46 
47 struct RenderParameters {
48  RenderType render_type =
49  RenderType::kInvalid; // Will cause errors unless explicitly replaced.
50  DlPoint center;
51  DlScalar stroke_width = 0.0f;
52  DlScalar scale_x = 1.0f;
53  DlScalar scale_y = 1.0f;
54  DlScalar skew_x = 0.0f;
55  DlScalar skew_y = 0.0f;
56  DlScalar degrees = 0.0f;
57 };
58 
59 void RenderPrimitiveWithStroke(DisplayListBuilder& builder,
60  const RenderParameters& params) {
61  builder.Save();
62 
63  builder.Translate(params.center.x, params.center.y);
64  builder.Rotate(params.degrees);
65  builder.Scale(params.scale_x, params.scale_y);
66  builder.Skew(params.skew_x, params.skew_y);
67  builder.Translate(-params.center.x, -params.center.y);
68 
69  // We describe the base size of the shape to draw in terms of the radius
70  // of a circle that would fill the space.
71  const DlScalar base_radius = 100.0f;
72  const DlScalar fill_radius = base_radius + params.stroke_width;
73  const DlScalar stroke_radius = base_radius - 10.0f;
74 
75  auto draw_shape = [&](DlScalar radius, DlColor color,
76  DlScalar stroke_width = -1) -> void {
77  DlPaint paint;
78  paint.setColor(color);
79  if (stroke_width < 0) {
80  paint.setDrawStyle(DlDrawStyle::kFill);
81  } else {
82  paint.setDrawStyle(DlDrawStyle::kStroke);
83  paint.setStrokeWidth(stroke_width);
84  }
85 
86  // Square bounds to match the overall size of the circle described.
87  DlRect square_bounds = DlRect::MakeCircleBounds(params.center, radius);
88  // Rectangular bounds slightly elongated from the square bounds.
89  DlRect rect_bounds = square_bounds.Expand(20.0f, 0.0f);
90 
91  switch (params.render_type) {
92  case RenderType::kSquare:
93  builder.DrawRect(square_bounds, paint);
94  break;
95  case RenderType::kRectangle:
96  builder.DrawRect(rect_bounds, paint);
97  break;
98  case RenderType::kCircle:
99  builder.DrawCircle(params.center, radius, paint);
100  break;
101  case RenderType::kOval:
102  builder.DrawOval(rect_bounds, paint);
103  break;
104  case RenderType::kLine: {
105  // Drawline can't "fill" the outer shape since it is just a line and
106  // has no fillable interior. So, instead we reinterpret the radius as
107  // a larger line width, but only accepting a radius outside of the
108  // typical stroke_radius. The length of the line always extends across
109  // the diameter of what would have been a circle of that radius.
110  if (stroke_width < 0) {
111  if (radius <= stroke_radius) {
112  // This must be an interior fill, but the line has no interior.
113  break;
114  }
115  paint.setDrawStyle(DlDrawStyle::kStroke);
116  paint.setStrokeWidth((radius - stroke_radius) * 2.0f);
117  }
118  // Extend the line by the radius amount in both directions.
119  DlPoint fill_offset(radius, 0.0f);
120  builder.DrawLine(params.center - fill_offset, //
121  params.center + fill_offset, //
122  paint);
123  break;
124  }
125  case RenderType::kValidCount:
126  case RenderType::kInvalid:
127  FML_UNREACHABLE();
128  }
129  };
130 
131  draw_shape(fill_radius, DlColor::kBlue());
132  draw_shape(stroke_radius, DlColor::ARGB(1.0f, 0.0f, 0.0f, 0.5f));
133  draw_shape(stroke_radius, DlColor::kWhite(), params.stroke_width);
134 
135  builder.Restore();
136 }
137 
138 } // namespace
139 
140 namespace impeller {
141 namespace testing {
142 
143 // This playground tests the effects of scaling, rotation and skew transforms
144 // on the consistency of a stroke (particularly hairlines). The math used to
145 // estimate the pixel size can get complicated in a shader that is performing
146 // some of its operations on local space values and some in device space.
147 TEST_P(AiksTest, PrimitiveShapePlayground) {
148  if (IsGoldenTest()) {
149  GTEST_SKIP() << "PrimitiveShapePlayground does not produce a golden image";
150  }
151 
152  RenderParameters params{
153  .render_type = RenderType::kRectangle,
154  .center = GetWindowBounds().GetCenter(),
155  };
156  // The ImGui controls need a pointer to an int and casting a pointer to
157  // an enum field to an int pointer is frowned upon, so we instead create
158  // an int variable and then fill in the params enum from it later.
159  int render_type_index = static_cast<int>(RenderType::kRectangle);
160 
161  auto callback = [&]() -> sk_sp<DisplayList> {
162  if (AiksTest::ImGuiBegin("Controls", nullptr,
163  ImGuiWindowFlags_AlwaysAutoResize)) {
164  ImGui::SliderFloat("Stroke", &params.stroke_width, 0.0f, 30.0f);
165  ImGui::SliderFloat("X Scale", &params.scale_x, 1.0f, 3.0f);
166  ImGui::SliderFloat("Y Scale", &params.scale_y, 1.0f, 3.0f);
167  ImGui::SliderFloat("X Skew", &params.skew_x, 0.0f, 1.0f);
168  ImGui::SliderFloat("Y Skew", &params.skew_y, 0.0f, 1.0f);
169  ImGui::SliderFloat("Rotation", &params.degrees, 0.0f, 360.0f);
170  ImGui::ListBox(
171  "Shape Type", &render_type_index,
172  [](void* data, int index) {
173  switch (static_cast<RenderType>(index)) {
174  case RenderType::kSquare:
175  return "Square";
176  case RenderType::kRectangle:
177  return "Rectangle";
178  case RenderType::kCircle:
179  return "Circle";
180  case RenderType::kOval:
181  return "Oval";
182  case RenderType::kLine:
183  return "Line";
184  case RenderType::kValidCount:
185  case RenderType::kInvalid:
186  FML_UNREACHABLE();
187  }
188  },
189  nullptr, static_cast<int>(RenderType::kValidCount), -1);
190  ImGui::End();
191  }
192 
193  // Translate our "Gui int variable" to the appropriate enum field value.
194  params.render_type = static_cast<RenderType>(render_type_index);
195 
196  DisplayListBuilder builder;
197  builder.Scale(GetContentScale().x, GetContentScale().y);
198  builder.DrawColor(DlColor::kBlack(), DlBlendMode::kSrc);
199  RenderPrimitiveWithStroke(builder, params);
200  return builder.Build();
201  };
202 
203  ASSERT_TRUE(OpenPlaygroundHere(callback));
204 }
205 
206 TEST_P(AiksTest, CanRenderSkewedCircleHairline) {
207  DisplayListBuilder builder;
208  builder.Scale(GetContentScale().x, GetContentScale().y);
209  builder.DrawColor(DlColor::kBlack(), DlBlendMode::kSrc);
210 
211  RenderParameters params{
212  .render_type = RenderType::kCircle,
213  .center = GetWindowBounds().GetCenter(),
214  .skew_x = 0.75f,
215  .skew_y = 0.75f,
216  };
217  RenderPrimitiveWithStroke(builder, params);
218 
219  ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
220 }
221 
222 } // namespace testing
223 } // namespace impeller
static bool ImGuiBegin(const char *name, bool *p_open, ImGuiWindowFlags flags)
int32_t x
TEST_P(AiksTest, DrawAtlasNoColor)
flutter::DlRect DlRect
Definition: dl_dispatcher.h:30
flutter::DlPoint DlPoint
Definition: dl_dispatcher.h:29
flutter::DlScalar DlScalar
Definition: dl_dispatcher.h:28