Skip to content

Commit

Permalink
Add round caps and joins (flutter#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdero authored and dnfield committed Apr 27, 2022
1 parent d9e7927 commit 9058956
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 36 deletions.
104 changes: 87 additions & 17 deletions impeller/entity/contents.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "impeller/entity/content_context.h"
#include "impeller/entity/entity.h"
#include "impeller/geometry/path_builder.h"
#include "impeller/geometry/path_component.h"
#include "impeller/geometry/scalar.h"
#include "impeller/geometry/vector.h"
#include "impeller/renderer/render_pass.h"
Expand Down Expand Up @@ -303,7 +304,8 @@ static VertexBuffer CreateSolidStrokeVertices(
HostBuffer& buffer,
const SolidStrokeContents::CapProc& cap_proc,
const SolidStrokeContents::JoinProc& join_proc,
Scalar miter_limit) {
Scalar miter_limit,
const SmoothingApproximation& smoothing) {
using VS = SolidStrokeVertexShader;

VertexBufferBuilder<VS::PerVertexData> vtx_builder;
Expand Down Expand Up @@ -356,7 +358,8 @@ static VertexBuffer CreateSolidStrokeVertices(

// Generate start cap.
if (!polyline.contours[contour_i].is_closed) {
cap_proc(vtx_builder, polyline.points[contour_start_point_i], -normal);
cap_proc(vtx_builder, polyline.points[contour_start_point_i], -normal,
smoothing);
}

// Generate contour geometry.
Expand All @@ -381,17 +384,18 @@ static VertexBuffer CreateSolidStrokeVertices(

// Generate join from the current line to the next line.
join_proc(vtx_builder, polyline.points[point_i], previous_normal,
normal, miter_limit);
normal, miter_limit, smoothing);
}
}
}

// Generate end cap or join.
if (!polyline.contours[contour_i].is_closed) {
cap_proc(vtx_builder, polyline.points[contour_end_point_i - 1], normal);
cap_proc(vtx_builder, polyline.points[contour_end_point_i - 1], normal,
smoothing);
} else {
join_proc(vtx_builder, polyline.points[contour_start_point_i], normal,
contour_first_normal, miter_limit);
contour_first_normal, miter_limit, smoothing);
}
}

Expand Down Expand Up @@ -420,9 +424,9 @@ bool SolidStrokeContents::Render(const ContentContext& renderer,
cmd.label = "SolidStroke";
cmd.pipeline = renderer.GetSolidStrokePipeline(OptionsFromPass(pass));
cmd.stencil_reference = entity.GetStencilDepth();
cmd.BindVertices(
CreateSolidStrokeVertices(entity.GetPath(), pass.GetTransientsBuffer(),
cap_proc_, join_proc_, miter_limit_));
cmd.BindVertices(CreateSolidStrokeVertices(
entity.GetPath(), pass.GetTransientsBuffer(), cap_proc_, join_proc_,
miter_limit_, arc_smoothing_approximation_));
VS::BindFrameInfo(cmd, pass.GetTransientsBuffer().EmplaceUniform(frame_info));
VS::BindStrokeInfo(cmd,
pass.GetTransientsBuffer().EmplaceUniform(stroke_info));
Expand All @@ -434,6 +438,7 @@ bool SolidStrokeContents::Render(const ContentContext& renderer,

void SolidStrokeContents::SetStrokeSize(Scalar size) {
stroke_size_ = size;
arc_smoothing_approximation_ = SmoothingApproximation(5.0 / size, 0.0, 0.0);
}

Scalar SolidStrokeContents::GetStrokeSize() const {
Expand All @@ -458,14 +463,41 @@ void SolidStrokeContents::SetStrokeCap(Cap cap) {
switch (cap) {
case Cap::kButt:
cap_proc_ = [](VertexBufferBuilder<VS::PerVertexData>& vtx_builder,
const Point& position, const Point& normal) {};
const Point& position, const Point& normal,
const SmoothingApproximation& smoothing) {};
break;
case Cap::kRound:
FML_DLOG(ERROR) << "Unimplemented.";
cap_proc_ = [](VertexBufferBuilder<VS::PerVertexData>& vtx_builder,
const Point& position, const Point& normal,
const SmoothingApproximation& smoothing) {
SolidStrokeVertexShader::PerVertexData vtx;
vtx.vertex_position = position;
vtx.pen_down = 1.0;

Point forward(normal.y, -normal.x);

auto arc_points =
CubicPathComponent(
normal, normal + forward * PathBuilder::kArcApproximationMagic,
forward + normal * PathBuilder::kArcApproximationMagic, forward)
.CreatePolyline(smoothing);

vtx.vertex_normal = normal;
vtx_builder.AppendVertex(vtx);
vtx.vertex_normal = -normal;
vtx_builder.AppendVertex(vtx);
for (const auto& point : arc_points) {
vtx.vertex_normal = point;
vtx_builder.AppendVertex(vtx);
vtx.vertex_normal = (-point).Reflect(forward);
vtx_builder.AppendVertex(vtx);
}
};
break;
case Cap::kSquare:
cap_proc_ = [](VertexBufferBuilder<VS::PerVertexData>& vtx_builder,
const Point& position, const Point& normal) {
const Point& position, const Point& normal,
const SmoothingApproximation& smoothing) {
SolidStrokeVertexShader::PerVertexData vtx;
vtx.vertex_position = position;
vtx.pen_down = 1.0;
Expand Down Expand Up @@ -517,22 +549,25 @@ void SolidStrokeContents::SetStrokeJoin(Join join) {
case Join::kBevel:
join_proc_ = [](VertexBufferBuilder<VS::PerVertexData>& vtx_builder,
const Point& position, const Point& start_normal,
const Point& end_normal, Scalar miter_limit) {
CreateBevelAndGetDirection(vtx_builder, position, start_normal, end_normal);
const Point& end_normal, Scalar miter_limit,
const SmoothingApproximation& smoothing) {
CreateBevelAndGetDirection(vtx_builder, position, start_normal,
end_normal);
};
break;
case Join::kMiter:
join_proc_ = [](VertexBufferBuilder<VS::PerVertexData>& vtx_builder,
const Point& position, const Point& start_normal,
const Point& end_normal, Scalar miter_limit) {
const Point& end_normal, Scalar miter_limit,
const SmoothingApproximation& smoothing) {
// 1 for no joint (straight line), 0 for max joint (180 degrees).
Scalar alignment = (start_normal.Dot(end_normal) + 1) / 2;
if (ScalarNearlyEqual(alignment, 1)) {
return;
}

Scalar dir =
CreateBevelAndGetDirection(vtx_builder, position, start_normal, end_normal);
Scalar dir = CreateBevelAndGetDirection(vtx_builder, position,
start_normal, end_normal);

Point miter_point = (start_normal + end_normal) / 2 / alignment;
if (miter_point.GetDistanceSquared({0, 0}) >
Expand All @@ -549,7 +584,42 @@ void SolidStrokeContents::SetStrokeJoin(Join join) {
};
break;
case Join::kRound:
FML_DLOG(ERROR) << "Unimplemented.";
join_proc_ = [](VertexBufferBuilder<VS::PerVertexData>& vtx_builder,
const Point& position, const Point& start_normal,
const Point& end_normal, Scalar miter_limit,
const SmoothingApproximation& smoothing) {
// 0 for no joint (straight line), 1 for max joint (180 degrees).
Scalar alignment = 1 - (start_normal.Dot(end_normal) + 1) / 2;
if (ScalarNearlyEqual(alignment, 0)) {
return;
}

Scalar dir =
CreateBevel(vtx_builder, position, start_normal, end_normal);

Point middle = (start_normal + end_normal).Normalize();
Point middle_handle = middle + Point(-middle.y, middle.x) *
PathBuilder::kArcApproximationMagic *
alignment * dir;
Point start_handle =
start_normal + Point(start_normal.y, -start_normal.x) *
PathBuilder::kArcApproximationMagic * alignment *
dir;

auto arc_points = CubicPathComponent(start_normal, start_handle,
middle_handle, middle)
.CreatePolyline(smoothing);

SolidStrokeVertexShader::PerVertexData vtx;
vtx.vertex_position = position;
vtx.pen_down = 1.0;
for (const auto& point : arc_points) {
vtx.vertex_normal = point * dir;
vtx_builder.AppendVertex(vtx);
vtx.vertex_normal = (-point * dir).Reflect(middle);
vtx_builder.AppendVertex(vtx);
}
};
break;
}
}
Expand Down
9 changes: 7 additions & 2 deletions impeller/entity/contents.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "flutter/fml/macros.h"
#include "impeller/entity/solid_stroke.vert.h"
#include "impeller/geometry/color.h"
#include "impeller/geometry/path_component.h"
#include "impeller/geometry/point.h"
#include "impeller/geometry/rect.h"
#include "impeller/renderer/texture.h"
Expand Down Expand Up @@ -132,13 +133,15 @@ class SolidStrokeContents final : public Contents {
using CapProc = std::function<void(
VertexBufferBuilder<SolidStrokeVertexShader::PerVertexData>& vtx_builder,
const Point& position,
const Point& normal)>;
const Point& normal,
const SmoothingApproximation& smoothing)>;
using JoinProc = std::function<void(
VertexBufferBuilder<SolidStrokeVertexShader::PerVertexData>& vtx_builder,
const Point& position,
const Point& start_normal,
const Point& end_normal,
Scalar miter_limit)>;
Scalar miter_limit,
const SmoothingApproximation& smoothing)>;

SolidStrokeContents();

Expand Down Expand Up @@ -170,6 +173,8 @@ class SolidStrokeContents final : public Contents {
RenderPass& pass) const override;

private:
SmoothingApproximation arc_smoothing_approximation_;

Color color_;
Scalar stroke_size_ = 0.0;
Scalar miter_limit_ = 4.0;
Expand Down
70 changes: 55 additions & 15 deletions impeller/entity/entity_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

#include "entity/contents.h"
#include "flutter/testing/testing.h"
#include "imgui.h"
#include "impeller/entity/entity.h"
#include "impeller/entity/entity_playground.h"
#include "impeller/geometry/path_builder.h"
#include "impeller/playground/playground.h"
#include "impeller/playground/widgets.h"
#include "third_party/imgui/imgui.h"

namespace impeller {
namespace testing {
Expand Down Expand Up @@ -81,37 +81,50 @@ TEST_F(EntityTest, TriangleInsideASquare) {
}

TEST_F(EntityTest, StrokeCapAndJoinTest) {
auto callback = [&](ContentContext& context, RenderPass& pass) {
Entity entity;
const Point padding(300, 250);
const Point margin(140, 180);

ImGui::SetNextWindowSize({300, 60});
ImGui::SetNextWindowPos({100, 300});
bool first_frame = true;
auto callback = [&](ContentContext& context, RenderPass& pass) {
if (first_frame) {
first_frame = false;
ImGui::SetNextWindowSize({300, 100});
ImGui::SetNextWindowPos(
{0 * padding.x + margin.x, 1.7f * padding.y + margin.y});
}
ImGui::Begin("Controls");
// 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::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 create_contents = [](SolidStrokeContents::Cap cap,
SolidStrokeContents::Join join) {
auto create_contents = [width = width](SolidStrokeContents::Cap cap,
SolidStrokeContents::Join join) {
auto contents = std::make_unique<SolidStrokeContents>();
contents->SetColor(Color::Red());
contents->SetStrokeSize(20.0);
contents->SetStrokeSize(width);
contents->SetStrokeCap(cap);
contents->SetStrokeJoin(join);
contents->SetStrokeMiter(miter_limit);
return contents;
};

const Point a_def(100, 100), b_def(100, 150), c_def(200, 100),
d_def(200, 50), e_def(150, 150);
const Scalar r = 10;
Entity entity;

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(0, 0);
Point off = Point(0, 0) * padding + margin;
Point a, b, c, d;
std::tie(a, b) = IMPELLER_PLAYGROUND_LINE(off + a_def, off + b_def, r,
Color::Black(), Color::White());
Expand All @@ -125,7 +138,7 @@ TEST_F(EntityTest, StrokeCapAndJoinTest) {

// Cap::kSquare demo.
{
Point off(0, 100);
Point off = Point(1, 0) * padding + margin;
Point a, b, c, d;
std::tie(a, b) = IMPELLER_PLAYGROUND_LINE(off + a_def, off + b_def, r,
Color::Black(), Color::White());
Expand All @@ -137,9 +150,23 @@ TEST_F(EntityTest, StrokeCapAndJoinTest) {
entity.Render(context, pass);
}

// Cap::kRound demo.
{
Point off = Point(2, 0) * padding + margin;
Point a, b, c, d;
std::tie(a, b) = IMPELLER_PLAYGROUND_LINE(off + a_def, off + b_def, r,
Color::Black(), Color::White());
std::tie(c, d) = IMPELLER_PLAYGROUND_LINE(off + c_def, off + d_def, r,
Color::Black(), Color::White());
entity.SetPath(PathBuilder{}.AddCubicCurve(a, b, d, c).TakePath());
entity.SetContents(create_contents(SolidStrokeContents::Cap::kRound,
SolidStrokeContents::Join::kBevel));
entity.Render(context, pass);
}

// Join::kBevel demo.
{
Point off(200, 0);
Point off = Point(0, 1) * padding + margin;
Point a = IMPELLER_PLAYGROUND_POINT(off + a_def, r, Color::White());
Point b = IMPELLER_PLAYGROUND_POINT(off + e_def, r, Color::White());
Point c = IMPELLER_PLAYGROUND_POINT(off + c_def, r, Color::White());
Expand All @@ -152,7 +179,7 @@ TEST_F(EntityTest, StrokeCapAndJoinTest) {

// Join::kMiter demo.
{
Point off(200, 100);
Point off = Point(1, 1) * padding + margin;
Point a = IMPELLER_PLAYGROUND_POINT(off + a_def, r, Color::White());
Point b = IMPELLER_PLAYGROUND_POINT(off + e_def, r, Color::White());
Point c = IMPELLER_PLAYGROUND_POINT(off + c_def, r, Color::White());
Expand All @@ -163,6 +190,19 @@ TEST_F(EntityTest, StrokeCapAndJoinTest) {
entity.Render(context, pass);
}

// Join::kRound demo.
{
Point off = Point(2, 1) * padding + margin;
Point a = IMPELLER_PLAYGROUND_POINT(off + a_def, r, Color::White());
Point b = IMPELLER_PLAYGROUND_POINT(off + e_def, r, Color::White());
Point c = IMPELLER_PLAYGROUND_POINT(off + c_def, r, Color::White());
entity.SetPath(
PathBuilder{}.MoveTo(a).LineTo(b).LineTo(c).Close().TakePath());
entity.SetContents(create_contents(SolidStrokeContents::Cap::kButt,
SolidStrokeContents::Join::kRound));
entity.Render(context, pass);
}

return true;
};
ASSERT_TRUE(OpenPlaygroundHere(callback));
Expand Down
2 changes: 0 additions & 2 deletions impeller/geometry/path_builder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

namespace impeller {

static const Scalar kArcApproximationMagic = 0.551915024494;

PathBuilder::PathBuilder() = default;

PathBuilder::~PathBuilder() = default;
Expand Down
9 changes: 9 additions & 0 deletions impeller/geometry/path_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ namespace impeller {

class PathBuilder {
public:
/// Used for approximating quarter circle arcs with cubic curves. This is the
/// control point distance which results in the smallest possible unit circle
/// integration for a right angle arc. It can be used to approximate arcs less
/// than 90 degrees to great effect by simply reducing it proportionally to
/// the angle. However, accuracy rapidly diminishes if magnified for obtuse
/// angle arcs, and so multiple cubic curves should be used when approximating
/// arcs greater than 90 degrees.
constexpr static const Scalar kArcApproximationMagic = 0.551915024494;

PathBuilder();

~PathBuilder();
Expand Down

0 comments on commit 9058956

Please sign in to comment.