[Engine] Support asymmetrical rounded superellipses (#161409)

This PR allows rounded superellipses to have asymmetrical and uneven
radii, effectively supporting `BorderRadius` instead of mere `double` as
corner radius. Fixes https://github.com/flutter/flutter/issues/161207.


https://github.com/user-attachments/assets/6293028c-d14b-4ffb-b93e-d0602f5ca0ee

These features exist in SwiftUI: The `RoundedRectangle` class provides
an initializer with a `cornerSize` parameter, allowing different radii
for the horizontal and vertical directions, while the
`UnevenRoundedRectangle` class allows each corner to have a unique
radius. However, SwiftUI does not allow the corners to be asymmetrical
_and_ uneven at the same time, which is supported by this PR.

Additionally, this change allows rounded superellipses to use the same
API as rounded rectangles. This allows RSEs to be added as a style of
`RRect` (just like in SwiftUI), which will use much fewer changes than
adding it as a new shape (>1500 LOC according to my prototype).

This PR also improves performance by removing an intermediate cache for
flipping. Now the point list is only copied once for the triangle strip
rearrangement.

`RoundingRadii` is moved to a separate file, and the code to scale it
based on bounds now its method.

## Pre-launch Checklist


- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Tong Mu 2025-01-14 13:15:13 -08:00 committed by GitHub
parent fa04f4a8d2
commit 1b7cd83b57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 631 additions and 327 deletions

View File

@ -42511,6 +42511,8 @@ ORIGIN: ../../../flutter/impeller/geometry/rect.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/rect.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/round_rect.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/round_rect.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/rounding_radii.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/rounding_radii.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/saturated_math.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/scalar.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/impeller/geometry/separated_vector.cc + ../../../flutter/LICENSE
@ -45452,6 +45454,8 @@ FILE: ../../../flutter/impeller/geometry/rect.cc
FILE: ../../../flutter/impeller/geometry/rect.h
FILE: ../../../flutter/impeller/geometry/round_rect.cc
FILE: ../../../flutter/impeller/geometry/round_rect.h
FILE: ../../../flutter/impeller/geometry/rounding_radii.cc
FILE: ../../../flutter/impeller/geometry/rounding_radii.h
FILE: ../../../flutter/impeller/geometry/saturated_math.h
FILE: ../../../flutter/impeller/geometry/scalar.h
FILE: ../../../flutter/impeller/geometry/separated_vector.cc

View File

@ -2325,27 +2325,89 @@ TEST_P(EntityTest, DrawSuperEllipse) {
TEST_P(EntityTest, DrawRoundSuperEllipse) {
auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
// UI state.
static float center_x = 100;
static float center_y = 100;
static float width = 900;
static float height = 900;
static float corner_radius = 300;
static Color color = Color::Red();
static float center[2] = {830, 830};
static float size[2] = {600, 600};
static bool horizontal_symmetry = true;
static bool vertical_symmetry = true;
static bool corner_symmetry = true;
// Initially radius_tl[0] will be mirrored to all 8 values since all 3
// symmetries are enabled.
static std::array<float, 2> radius_tl = {200};
static std::array<float, 2> radius_tr;
static std::array<float, 2> radius_bl;
static std::array<float, 2> radius_br;
auto AddRadiusControl = [](std::array<float, 2>& radii, const char* tb_name,
const char* lr_name) {
std::string name = "Radius";
if (!horizontal_symmetry || !vertical_symmetry) {
name += ":";
}
if (!vertical_symmetry) {
name = name + " " + tb_name;
}
if (!horizontal_symmetry) {
name = name + " " + lr_name;
}
if (corner_symmetry) {
ImGui::SliderFloat(name.c_str(), radii.data(), 0, 1000);
} else {
ImGui::SliderFloat2(name.c_str(), radii.data(), 0, 1000);
}
};
if (corner_symmetry) {
radius_tl[1] = radius_tl[0];
radius_tr[1] = radius_tr[0];
radius_bl[1] = radius_bl[0];
radius_br[1] = radius_br[0];
}
ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
ImGui::SliderFloat("Center X", &center_x, 0, 1000);
ImGui::SliderFloat("Center Y", &center_y, 0, 1000);
ImGui::SliderFloat("Width", &width, 0, 1000);
ImGui::SliderFloat("Height", &height, 0, 1000);
ImGui::SliderFloat("Corner radius", &corner_radius, 0, 500);
{
ImGui::SliderFloat2("Center", center, 0, 1000);
ImGui::SliderFloat2("Size", size, 0, 1000);
ImGui::Checkbox("Symmetry: Horizontal", &horizontal_symmetry);
ImGui::Checkbox("Symmetry: Vertical", &vertical_symmetry);
ImGui::Checkbox("Symmetry: Corners", &corner_symmetry);
AddRadiusControl(radius_tl, "Top", "Left");
if (!horizontal_symmetry) {
AddRadiusControl(radius_tr, "Top", "Right");
} else {
radius_tr = radius_tl;
}
if (!vertical_symmetry) {
AddRadiusControl(radius_bl, "Bottom", "Left");
} else {
radius_bl = radius_tl;
}
if (!horizontal_symmetry && !vertical_symmetry) {
AddRadiusControl(radius_br, "Bottom", "Right");
} else {
if (horizontal_symmetry) {
radius_br = radius_bl;
} else {
radius_br = radius_tr;
}
}
}
ImGui::End();
RoundingRadii radii{
.top_left = {radius_tl[0], radius_tl[1]},
.top_right = {radius_tr[0], radius_tr[1]},
.bottom_left = {radius_bl[0], radius_bl[1]},
.bottom_right = {radius_br[0], radius_br[1]},
};
auto contents = std::make_shared<SolidColorContents>();
std::unique_ptr<RoundSuperellipseGeometry> geom =
std::make_unique<RoundSuperellipseGeometry>(
Rect::MakeOriginSize({center_x, center_y}, {width, height}),
corner_radius);
contents->SetColor(color);
Rect::MakeOriginSize({center[0], center[1]}, {size[0], size[1]}),
radii);
contents->SetColor(Color::Red());
contents->SetGeometry(geom.get());
Entity entity;

View File

@ -3,6 +3,7 @@
// found in the LICENSE file.
#include <cmath>
#include <variant>
#include "flutter/impeller/entity/geometry/round_superellipse_geometry.h"
@ -11,6 +12,157 @@
namespace impeller {
namespace {
// An interface for classes that arranges a point list that forms a convex
// contour into a triangle strip.
class ConvexRearranger {
public:
ConvexRearranger() {}
virtual ~ConvexRearranger() {}
virtual size_t ContourLength() const = 0;
virtual Point GetPoint(size_t i) const = 0;
void RearrangeIntoTriangleStrip(Point* output) {
size_t index_count = 0;
output[index_count++] = GetPoint(0);
size_t a = 1;
size_t contour_length = ContourLength();
size_t b = contour_length - 1;
while (a < b) {
output[index_count++] = GetPoint(a);
output[index_count++] = GetPoint(b);
a++;
b--;
}
if (a == b) {
output[index_count++] = GetPoint(b);
}
}
private:
ConvexRearranger(const ConvexRearranger&) = delete;
ConvexRearranger& operator=(const ConvexRearranger&) = delete;
};
// A convex rearranger whose contour is concatenated from 4 quadrant segments.
//
// The input quadrant curves must travel from the Y axis to the X axis, and
// include both ends. This means that the points on the axes are duplicate
// between segments, and will be omitted by this class.
class UnevenQuadrantsRearranger : public ConvexRearranger {
public:
UnevenQuadrantsRearranger(Point* cache, size_t segment_capacity)
: cache_(cache), segment_capacity_(segment_capacity) {}
Point* QuadCache(size_t i) { return cache_ + segment_capacity_ * i; }
const Point* QuadCache(size_t i) const {
return cache_ + segment_capacity_ * i;
}
size_t& QuadSize(size_t i) { return lengths_[i]; }
size_t ContourLength() const override {
return lengths_[0] + lengths_[1] + lengths_[2] + lengths_[3] - 4;
}
Point GetPoint(size_t i) const override {
// output from index
// 0 ... l0-2 quads[0] 0 ... l0-2
// next 0 ... l1-2 quads[1] l1-1 ... 1
// next 0 ... l2-2 quads[2] 0 ... l2-2
// next 0 ... l3-2 quads[3] l3-1 ... 1
size_t high = lengths_[0] - 1;
if (i < high) {
return QuadCache(0)[i];
}
high += lengths_[1] - 1;
if (i < high) {
return QuadCache(1)[high - i];
}
size_t low = high;
high += lengths_[2] - 1;
if (i < high) {
return QuadCache(2)[i - low];
}
high += lengths_[3] - 1;
if (i < high) {
return QuadCache(3)[high - i];
} else {
// Unreachable
return Point();
}
}
private:
Point* cache_;
size_t segment_capacity_;
size_t lengths_[4];
};
// A convex rearranger whose contour is concatenated from 4 identical quadrant
// segments.
//
// The input curve must travel from the Y axis to the X axis and include both
// ends. This means that the points on the axes are duplicate between segments,
// and will be omitted by this class.
class MirroredQuadrantRearranger : public ConvexRearranger {
public:
MirroredQuadrantRearranger(Point center, Point* cache)
: center_(center), cache_(cache) {}
size_t& QuadSize() { return l_; }
size_t ContourLength() const override { return l_ * 4 - 4; }
Point GetPoint(size_t i) const override {
// output from index
// 0 ... l-2 quad 0 ... l-2
// next 0 ... l-2 quad l-1 ... 1
// next 0 ... l-2 quad 0 ... l-2
// next 0 ... l-2 quad l-1 ... 1
size_t high = l_ - 1;
if (i < high) {
return cache_[i] + center_;
}
high += l_ - 1;
if (i < high) {
return cache_[high - i] * Point{1, -1} + center_;
}
size_t low = high;
high += l_ - 1;
if (i < high) {
return cache_[i - low] * Point{-1, -1} + center_;
}
high += l_ - 1;
if (i < high) {
return cache_[high - i] * Point{-1, 1} + center_;
} else {
// Unreachable
return Point();
}
}
private:
Point center_;
Point* cache_;
size_t l_ = 0;
};
// A matrix that swaps the coordinates of a point.
// clang-format off
constexpr Matrix kFlip = Matrix(
0.0f, 1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f);
// clang-format on
// A look up table with precomputed variables.
//
// The columns represent the following variabls respectively:
@ -64,15 +216,6 @@ Scalar LerpPrecomputedVariable(size_t column, Scalar ratio) {
frac * kPrecomputedVariables[left + 1][column];
}
// Return the shortest of `corner_radius`, height/2, and width/2.
//
// Corner radii longer than 1/2 of the side length does not make sense, and will
// be limited to the longest possible.
Scalar LimitRadius(Scalar corner_radius, const Rect& bounds) {
return std::min(corner_radius,
std::min(bounds.GetWidth() / 2, bounds.GetHeight() / 2));
}
// The max angular step that the algorithm will traverse a quadrant of the
// curve.
//
@ -102,23 +245,47 @@ Scalar CalculateStep(Scalar minDimension, Scalar fullAngle) {
return std::min(kMinAngleStep, angleByDimension);
}
// The distance from point M (the 45deg point) to either side of the closer
// bounding box is defined as `CalculateGap`.
constexpr Scalar CalculateGap(Scalar corner_radius) {
// Heuristic formula derived from experimentation.
return 0.2924066406 * corner_radius;
// A factor used to calculate the "gap", defined as the distance from the
// midpoint of the curved corners to the nearest sides of the bounding box.
//
// When the corner radius is symmetrical on both dimensions, the midpoint of the
// corner is where the circular arc intersects its quadrant bisector. When the
// corner radius is asymmetrical, since the corner can be considered "elongated"
// from a symmetrical corner, the midpoint is transformed in the same way.
//
// Experiments indicate that the gap is linear with respect to the corner
// radius on that dimension.
//
// The formula should be kept in sync with a few files, as documented in
// `CalculateGap` in round_superellipse_geometry.cc.
constexpr Scalar kGapFactor = 0.2924066406;
// Return the value that splits the range from `left` to `right` into two
// portions whose ratio equals to `ratio_left` : `ratio_right`.
static Scalar Split(Scalar left,
Scalar right,
Scalar ratio_left,
Scalar ratio_right) {
return (left * ratio_right + right * ratio_left) / (ratio_left + ratio_right);
}
// Draw a circular arc from `start` to `end` with a radius of `r`.
//
// It is assumed that `start` is north-west to `end`, and the center
// of the circle is south-west to both points.
// It is assumed that `start` is north-west to `end`, and the center of the
// circle is south-west to both points. If `reverse` is true, then the curve
// goes from `end` to `start` instead.
//
// The resulting points are appended to `output` and include the starting point
// but exclude the ending point.
// The resulting points, after applying `transform`, are appended to `output`
// and include the effective starting point but exclude the effective ending
// point.
//
// Returns the number of the
size_t DrawCircularArc(Point* output, Point start, Point end, Scalar r) {
// Returns the number of generated points.
size_t DrawCircularArc(Point* output,
Point start,
Point end,
Scalar r,
bool reverse,
const Matrix& transform) {
/* Denote the middle point of S and E as M. The key is to find the center of
* the circle.
* S --__
@ -139,35 +306,72 @@ size_t DrawCircularArc(Point* output, Point start, Point end, Scalar r) {
Point c = m - distance_cm * c_to_m.Normalize();
Scalar angle_sce = asinf(distance_sm / r) * 2;
Point c_to_s = start - c;
Scalar step = CalculateStep(std::abs(s_to_e.y), angle_sce);
Matrix full_transform = transform * Matrix::MakeTranslation(c);
Point* next = output;
Scalar angle = 0;
while (angle < angle_sce) {
*(next++) = c_to_s.Rotate(Radians(-angle)) + c;
Scalar angle = reverse ? angle_sce : 0.0f;
Scalar step =
(reverse ? -1 : 1) * CalculateStep(std::abs(s_to_e.y), angle_sce);
Scalar end_angle = reverse ? 0.0f : angle_sce;
while ((angle < end_angle) != reverse) {
*(next++) = full_transform * c_to_s.Rotate(Radians(-angle));
angle += step;
}
return next - output;
}
// Draw a superellipsoid arc.
//
// The superellipse is centered at the origin and has degree `n` and both
// semi-axes equal to `a`. The arc starts from positive Y axis and spans from 0
// to `max_theta` radiance clockwise if `reverse` is false, or from `max_theta`
// to 0 otherwise.
//
// The resulting points, after applying `transform`, are appended to `output`
// and include the starting point but exclude the ending point.
//
// Returns the number of generated points.
size_t DrawSuperellipsoidArc(Point* output,
Scalar a,
Scalar n,
Scalar max_theta,
bool reverse,
const Matrix& transform) {
Point* next = output;
Scalar angle = reverse ? max_theta : 0.0f;
Scalar step =
(reverse ? -1 : 1) *
CalculateStep(a - a * pow(abs(cosf(max_theta)), 2 / n), max_theta);
Scalar end = reverse ? 0.0f : max_theta;
while ((angle < end) != reverse) {
Scalar x = a * pow(abs(sinf(angle)), 2 / n);
Scalar y = a * pow(abs(cosf(angle)), 2 / n);
*(next++) = transform * Point(x, y);
angle += step;
}
return next - output;
}
// Draws an arc representing the top 1/8 segment of a square-like rounded
// superellipse.
// superellipse centered at the origin.
//
// The resulting arc centers at the origin, spanning from 0 to pi/4, moving
// clockwise starting from the positive Y-axis, and includes the starting point
// (the middle of the top flat side) while excluding the ending point (the x=y
// point).
// The square-like rounded superellipse that this arc belongs to has a width and
// height specified by `size` and features rounded corners determined by
// `corner_radius`. The `corner_radius` corresponds to the `cornerRadius`
// parameter in SwiftUI, rather than the literal radius of corner circles.
//
// The full square-like rounded superellipse has a width and height specified by
// `size` and features rounded corners determined by `corner_radius`. The
// `corner_radius` corresponds to the `cornerRadius` parameter in SwiftUI,
// rather than the literal radius of corner circles.
// If `reverse` is false, the resulting arc spans from 0 (inclusive) to pi/4
// (exclusive), moving clockwise starting from the positive Y-axis. If `reverse`
// is true, the curve spans from pi/4 (inclusive) to 0 (inclusive)
// counterclockwise instead.
//
// Returns the number of points generated.
size_t DrawOctantSquareLikeSquircle(Point* output,
Scalar size,
Scalar corner_radius) {
Scalar corner_radius,
bool reverse,
const Matrix& transform) {
/* The following figure shows the first quadrant of a square-like rounded
* superellipse. The target arc consists of the "stretch" (AB), a
* superellipsoid arc (BJ), and a circular arc (JM).
@ -182,10 +386,10 @@ size_t DrawOctantSquareLikeSquircle(Point* output,
* | | / |
* | | D |
* | | / |
* +----+ |
* +----+ S |
* s | | |
* +----+---------------| A'
* O S
* O
* s
* ------ size/2 ------
*
@ -203,7 +407,7 @@ size_t DrawOctantSquareLikeSquircle(Point* output,
Scalar ratio = {std::min(size / corner_radius, kMaxRatio)};
Scalar a = ratio * corner_radius / 2;
Scalar s = size / 2 - a;
Scalar g = CalculateGap(corner_radius);
Scalar g = kGapFactor * corner_radius;
Scalar n = LerpPrecomputedVariable(1, ratio);
Scalar d = LerpPrecomputedVariable(2, ratio) * a;
@ -211,118 +415,95 @@ size_t DrawOctantSquareLikeSquircle(Point* output,
Scalar R = (a - d - g) * sqrt(2);
Point pointM(size / 2 - g, size / 2 - g);
Scalar xJ = a * pow(abs(sinf(thetaJ)), 2 / n);
Scalar yJ = a * pow(abs(cosf(thetaJ)), 2 / n);
Point pointA{0, size / 2};
Point pointM{size / 2 - g, size / 2 - g};
Point pointS{s, s};
Point pointJ =
Point{pow(abs(sinf(thetaJ)), 2 / n), pow(abs(cosf(thetaJ)), 2 / n)} * a +
pointS;
Matrix translationS = Matrix::MakeTranslation(pointS);
Point* next = output;
// A
*(next++) = Point(0, size / 2);
// Superellipsoid arc BJ (B inclusive, J exclusive)
{
Scalar step = CalculateStep(a - yJ, thetaJ);
Scalar angle = 0;
while (angle < thetaJ) {
Scalar x = a * pow(abs(sinf(angle)), 2 / n);
Scalar y = a * pow(abs(cosf(angle)), 2 / n);
*(next++) = Point(x + s, y + s);
angle += step;
}
if (!reverse) {
// Point A
*(next++) = transform * pointA;
// Arc [B, J)
next += DrawSuperellipsoidArc(next, a, n, thetaJ, reverse,
transform * translationS);
// Arc [J, M)
next += DrawCircularArc(next, pointJ, pointM, R, reverse, transform);
} else {
// Arc [M, J)
next += DrawCircularArc(next, pointJ, pointM, R, reverse, transform);
// Arc [J, B)
next += DrawSuperellipsoidArc(next, a, n, thetaJ, reverse,
transform * translationS);
// Point B
*(next++) = transform * Point{s, size / 2};
// Point A
*(next++) = transform * pointA;
}
// Circular arc JM (B inclusive, M exclusive)
next += DrawCircularArc(next, {xJ + s, yJ + s}, pointM, R);
return next - output;
}
// Optionally `flip` the input points before offsetting it by `center`, and
// append the result to `output`.
// Draw a quadrant curve, both ends included.
//
// If `flip` is true, then the entire input list is reversed, and the x and y
// coordinate of each point is swapped as well. This effectively mirrors the
// input point list by the y=x line.
size_t FlipAndOffset(Point* output,
const Point* input,
size_t input_length,
bool flip,
const Point& center) {
if (!flip) {
for (size_t i = 0; i < input_length; i++) {
output[i] = input[i] + center;
}
} else {
for (size_t i = 0; i < input_length; i++) {
const Point& point = input[input_length - i - 1];
output[i] = Point(point.y + center.x, point.x + center.y);
}
}
return input_length;
}
constexpr Point kReflection[4] = {{1, 1}, {1, -1}, {-1, -1}, {-1, 1}};
// Mirror the point list `quad` into other quadrants and output as a triangle
// strip.
// Returns the number of points.
//
// The input arc `quad` should reside in the first quadrant, starting at
// positive Y axis and ending at positive X axis (both ends inclusive), for a
// total of `quad_length` points. This function mirrors the arc into 4
// quadrants, offset the result by `center`, and rearrange it as a triangle
// strip, which is appended to `output`.
//
// A total of (quad_length - 1) * 4 points will be appended, and `output` must
// have sufficient memory allocated before this call.
void MirrorIntoTriangleStrip(const Point* quad,
size_t quad_length,
const Point& center,
Point* output) {
// The length of 1/4 arc including the starting point but excluding the
// ending point.
const size_t arc_length = quad_length - 1;
auto GetPoint = [quad, arc_length](size_t i) -> Point {
if (i < arc_length) {
return quad[i];
}
i = i - arc_length;
if (i < arc_length) {
return quad[arc_length - i] * kReflection[1];
}
i = i - arc_length;
if (i < arc_length) {
return quad[i] * kReflection[2];
}
i = i - arc_length;
if (i < arc_length) {
return quad[arc_length - i] * kReflection[3];
} else {
// Unreachable
return Point();
}
};
size_t index_count = 0;
output[index_count++] = GetPoint(0) + center;
size_t a = 1;
size_t b = arc_length * 4 - 1;
while (a < b) {
output[index_count++] = GetPoint(a) + center;
output[index_count++] = GetPoint(b) + center;
a++;
b--;
}
if (a == b) {
output[index_count++] = GetPoint(b) + center;
// The eact quadrant is specified by the direction of `outer` relative to
// `center`. The curve goes from the X axis to the Y axis.
static size_t DrawQuadrant(Point* output,
Point center,
Point outer,
Size radii) {
if (radii.width == 0 || radii.height == 0) {
// Degrade to rectangle. (A zero radius causes error below.)
output[0] = {center.x, outer.y};
output[1] = outer;
output[2] = {outer.x, center.y};
return 3;
}
// Normalize sizes and radii into symmetrical radius by scaling the longer of
// `radii` to the shorter. For example, to draw a RSE with size (200, 300)
// and radii (20, 10), this function draws one with size (100, 300) and radii
// (10, 10) and then scales it by (2x, 1x).
Scalar norm_radius = radii.MinDimension();
Size radius_scale = radii / norm_radius;
Point signed_size = (outer - center) * 2;
Point norm_size = signed_size.Abs() / radius_scale;
Point signed_scale = signed_size / norm_size;
// Each quadrant curve is composed of two octant curves, each of which belongs
// to a square-like rounded rectangle. When `norm_size`'s width != height, the
// centers of such square-like rounded rectangles are offset from the origin
// by a distance denoted as `c`.
Scalar c = (norm_size.x - norm_size.y) / 2;
Point* next = output;
next += DrawOctantSquareLikeSquircle(
next, norm_size.x, norm_radius, /*reverse=*/false,
Matrix::MakeTranslateScale(signed_scale, center) *
Matrix::MakeTranslation(Size{0, -c}));
next += DrawOctantSquareLikeSquircle(
next, norm_size.y, norm_radius, /*reverse=*/true,
Matrix::MakeTranslateScale(signed_scale, center) *
Matrix::MakeTranslation(Size{c, 0}) * kFlip);
return next - output;
}
} // namespace
RoundSuperellipseGeometry::RoundSuperellipseGeometry(const Rect& bounds,
Scalar corner_radius)
: bounds_(bounds), corner_radius_(LimitRadius(corner_radius, bounds)) {}
const RoundingRadii& radii)
: bounds_(bounds.GetPositive()), radii_(radii.Scaled(bounds_)) {}
RoundSuperellipseGeometry::RoundSuperellipseGeometry(const Rect& bounds,
float corner_radius)
: RoundSuperellipseGeometry(bounds,
RoundingRadii::MakeRadius(corner_radius)) {}
RoundSuperellipseGeometry::~RoundSuperellipseGeometry() {}
@ -330,66 +511,66 @@ GeometryResult RoundSuperellipseGeometry::GetPositionBuffer(
const ContentContext& renderer,
const Entity& entity,
RenderPass& pass) const {
const Size size = bounds_.GetSize();
const Point center = bounds_.GetCenter();
// The full shape is divided into 4 segments: the top and bottom edges come
// from two square-like rounded superellipses (called "width-aligned"), while
// the left and right squircles come from another two ("height-aligned").
//
// Denote the distance from the center of the square-like squircles to the
// origin as `c`. The width-aligned square-like squircle and the
// height-aligned one have the same offset in different directions.
const Scalar c = (size.width - size.height) / 2;
// The cache is allocated as follows:
//
// * The first chunk stores the quadrant arc.
// * The second chunk stores an octant arc before flipping and translation.
Point* cache = renderer.GetTessellator().GetStrokePointCache().data();
// The memory size (in units of Points) allocated to store the first chunk.
constexpr size_t kMaxQuadrantLength = kPointArenaSize / 4;
// The memory size (in units of Points) allocated to store each quadrants.
constexpr size_t kMaxQuadSize = kPointArenaSize / 4;
// Since the curve is traversed in steps bounded by kMaxQuadrantSteps, the
// curving part will have fewer points than kMaxQuadrantSteps. Multiply it by
// 2 for storing other sporatic points (an extremely conservative estimate).
static_assert(kMaxQuadrantLength > 2 * kMaxQuadrantSteps);
static_assert(kMaxQuadSize > 2 * kMaxQuadrantSteps);
// Draw the first quadrant of the shape and store in `quadrant`, including
// both ends. It will be mirrored to other quadrants later.
Point* quadrant = cache;
size_t quadrant_length;
{
Point* next = quadrant;
ConvexRearranger* rearranger;
std::variant<std::monostate, MirroredQuadrantRearranger,
UnevenQuadrantsRearranger>
rearranger_holder;
Point* octant_cache = cache + kMaxQuadrantLength;
size_t octant_length;
if (radii_.AreAllCornersSame()) {
rearranger_holder.emplace<MirroredQuadrantRearranger>(bounds_.GetCenter(),
cache);
auto& t = std::get<MirroredQuadrantRearranger>(rearranger_holder);
rearranger = &t;
octant_length =
DrawOctantSquareLikeSquircle(octant_cache, size.width, corner_radius_);
next += FlipAndOffset(next, octant_cache, octant_length, /*flip=*/false,
Point(0, -c));
// The quadrant must be drawn at the origin so that it can be rotated later.
t.QuadSize() = DrawQuadrant(cache, Point(),
bounds_.GetRightTop() - bounds_.GetCenter(),
radii_.top_right);
} else {
rearranger_holder.emplace<UnevenQuadrantsRearranger>(cache, kMaxQuadSize);
auto& t = std::get<UnevenQuadrantsRearranger>(rearranger_holder);
rearranger = &t;
*(next++) = Point(size / 2) - CalculateGap(corner_radius_); // Point M
Scalar top_split = Split(bounds_.GetLeft(), bounds_.GetRight(),
radii_.top_left.width, radii_.top_right.width);
Scalar right_split =
Split(bounds_.GetTop(), bounds_.GetBottom(), radii_.top_right.height,
radii_.bottom_right.height);
Scalar bottom_split =
Split(bounds_.GetLeft(), bounds_.GetRight(), radii_.bottom_left.width,
radii_.bottom_right.width);
Scalar left_split =
Split(bounds_.GetTop(), bounds_.GetBottom(), radii_.top_left.height,
radii_.bottom_left.height);
octant_length =
DrawOctantSquareLikeSquircle(octant_cache, size.height, corner_radius_);
next += FlipAndOffset(next, octant_cache, octant_length, /*flip=*/true,
Point(c, 0));
quadrant_length = next - quadrant;
t.QuadSize(0) = DrawQuadrant(t.QuadCache(0), Point{top_split, right_split},
bounds_.GetRightTop(), radii_.top_right);
t.QuadSize(1) =
DrawQuadrant(t.QuadCache(1), Point{bottom_split, right_split},
bounds_.GetRightBottom(), radii_.bottom_right);
t.QuadSize(2) =
DrawQuadrant(t.QuadCache(2), Point{bottom_split, left_split},
bounds_.GetLeftBottom(), radii_.bottom_left);
t.QuadSize(3) = DrawQuadrant(t.QuadCache(3), Point{top_split, left_split},
bounds_.GetLeftTop(), radii_.top_left);
}
// The `contour_point_count` include all points on the border. The "-1" comes
// from duplicate ends from the mirrored arcs.
size_t contour_length = 4 * (quadrant_length - 1);
size_t contour_length = rearranger->ContourLength();
BufferView vertex_buffer = renderer.GetTransientsBuffer().Emplace(
nullptr, sizeof(Point) * contour_length, alignof(Point));
Point* vertex_data =
reinterpret_cast<Point*>(vertex_buffer.GetBuffer()->OnGetContents() +
vertex_buffer.GetRange().offset);
MirrorIntoTriangleStrip(quadrant, quadrant_length, center, vertex_data);
rearranger->RearrangeIntoTriangleStrip(vertex_data);
return GeometryResult{
.type = PrimitiveType::kTriangleStrip,
@ -413,14 +594,18 @@ bool RoundSuperellipseGeometry::CoversArea(const Matrix& transform,
if (!transform.IsTranslationScaleOnly()) {
return false;
}
// Use the rectangle formed by the four 45deg points (point M) as a
// conservative estimate of the inner rectangle.
Scalar g = CalculateGap(corner_radius_);
Scalar left_inset = std::max(radii_.top_left.width, radii_.bottom_left.width);
Scalar right_inset =
std::max(radii_.top_right.width, radii_.bottom_right.width);
Scalar top_inset = std::max(radii_.top_left.height, radii_.top_right.height);
Scalar bottom_inset =
std::max(radii_.bottom_left.height, radii_.bottom_right.height);
Rect coverage =
Rect::MakeLTRB(bounds_.GetLeft() + g, bounds_.GetTop() + g,
bounds_.GetRight() - g, bounds_.GetBottom() - g)
.TransformBounds(transform);
return coverage.Contains(rect);
Rect::MakeLTRB(bounds_.GetLeft() + left_inset * kGapFactor,
bounds_.GetTop() + top_inset * kGapFactor,
bounds_.GetRight() - right_inset * kGapFactor,
bounds_.GetBottom() - bottom_inset * kGapFactor);
return coverage.TransformBounds(transform).Contains(rect);
}
bool RoundSuperellipseGeometry::IsAxisAlignedRect() const {

View File

@ -6,6 +6,7 @@
#define FLUTTER_IMPELLER_ENTITY_GEOMETRY_ROUND_SUPERELLIPSE_GEOMETRY_H_
#include "impeller/entity/geometry/geometry.h"
#include "impeller/geometry/rounding_radii.h"
namespace impeller {
@ -25,7 +26,8 @@ namespace impeller {
/// not exactly equals to, the radius of the corner circles.
class RoundSuperellipseGeometry final : public Geometry {
public:
explicit RoundSuperellipseGeometry(const Rect& bounds, Scalar corner_radius);
RoundSuperellipseGeometry(const Rect& bounds, const RoundingRadii& radii);
RoundSuperellipseGeometry(const Rect& bounds, float corner_radius);
~RoundSuperellipseGeometry() override;
@ -45,7 +47,7 @@ class RoundSuperellipseGeometry final : public Geometry {
std::optional<Rect> GetCoverage(const Matrix& transform) const override;
const Rect bounds_;
double corner_radius_;
const RoundingRadii radii_;
RoundSuperellipseGeometry(const RoundSuperellipseGeometry&) = delete;

View File

@ -31,6 +31,8 @@ impeller_component("geometry") {
"rect.h",
"round_rect.cc",
"round_rect.h",
"rounding_radii.cc",
"rounding_radii.h",
"saturated_math.h",
"scalar.h",
"separated_vector.cc",

View File

@ -6,68 +6,17 @@
namespace impeller {
static inline void NormalizeEmptyToZero(Size& radii) {
if (radii.IsEmpty()) {
radii = Size();
}
}
static inline void AdjustScale(Scalar& radius1,
Scalar& radius2,
Scalar dimension,
Scalar& scale) {
FML_DCHECK(radius1 >= 0.0f && radius2 >= 0.0f);
FML_DCHECK(dimension > 0.0f);
if (radius1 + radius2 > dimension) {
scale = std::min(scale, dimension / (radius1 + radius2));
}
}
RoundRect RoundRect::MakeRectRadii(const Rect& in_bounds,
const RoundingRadii& in_radii) {
if (!in_bounds.IsFinite()) {
return {};
}
Rect bounds = in_bounds.GetPositive();
if (bounds.IsEmpty() || //
in_radii.AreAllCornersEmpty() || !in_radii.IsFinite()) {
// pass along the bounds even if empty as it would still have a valid
// location and/or 1-dimensional size which might appear when stroked
return RoundRect(bounds, RoundingRadii());
}
// Copy the incoming radii so that we can work on normalizing them to the
// particular rectangle they are paired with without disturbing the caller.
RoundingRadii radii = in_radii;
// If any corner is flat or has a negative value, normalize it to zeros
// We do this first so that the unnecessary non-flat part of that radius
// does not contribute to the global scaling below.
NormalizeEmptyToZero(radii.top_left);
NormalizeEmptyToZero(radii.top_right);
NormalizeEmptyToZero(radii.bottom_left);
NormalizeEmptyToZero(radii.bottom_right);
// Now determine a global scale to apply to all of the radii to ensure
// that none of the adjacent pairs of radius values sum to larger than
// the corresponding dimension of the rectangle.
Size size = bounds.GetSize();
Scalar scale = 1.0f;
// clang-format off
AdjustScale(radii.top_left.width, radii.top_right.width, size.width,
scale);
AdjustScale(radii.bottom_left.width, radii.bottom_right.width, size.width,
scale);
AdjustScale(radii.top_left.height, radii.bottom_left.height, size.height,
scale);
AdjustScale(radii.top_right.height, radii.bottom_right.height, size.height,
scale);
// clang-format on
if (scale < 1.0f) {
radii = radii * scale;
}
return RoundRect(bounds, radii);
// RoundingRadii::Scaled might return an empty radii if bounds or in_radii is
// empty, which is expected. Pass along the bounds even if the radii is empty
// as it would still have a valid location and/or 1-dimensional size which
// might appear when stroked
return RoundRect(bounds, in_radii.Scaled(bounds));
}
// Determine if p is inside the elliptical corner curve defined by the

View File

@ -7,68 +7,11 @@
#include "flutter/impeller/geometry/point.h"
#include "flutter/impeller/geometry/rect.h"
#include "flutter/impeller/geometry/rounding_radii.h"
#include "flutter/impeller/geometry/size.h"
namespace impeller {
struct RoundingRadii {
Size top_left;
Size top_right;
Size bottom_left;
Size bottom_right;
constexpr static RoundingRadii MakeRadius(Scalar radius) {
return {Size(radius), Size(radius), Size(radius), Size(radius)};
}
constexpr static RoundingRadii MakeRadii(Size radii) {
return {radii, radii, radii, radii};
}
constexpr bool IsFinite() const {
return top_left.IsFinite() && //
top_right.IsFinite() && //
bottom_left.IsFinite() && //
bottom_right.IsFinite();
}
constexpr bool AreAllCornersEmpty() const {
return top_left.IsEmpty() && //
top_right.IsEmpty() && //
bottom_left.IsEmpty() && //
bottom_right.IsEmpty();
}
constexpr bool AreAllCornersSame(Scalar tolerance = kEhCloseEnough) const {
return ScalarNearlyEqual(top_left.width, top_right.width, tolerance) &&
ScalarNearlyEqual(top_left.width, bottom_right.width, tolerance) &&
ScalarNearlyEqual(top_left.width, bottom_left.width, tolerance) &&
ScalarNearlyEqual(top_left.height, top_right.height, tolerance) &&
ScalarNearlyEqual(top_left.height, bottom_right.height, tolerance) &&
ScalarNearlyEqual(top_left.height, bottom_left.height, tolerance);
}
constexpr inline RoundingRadii operator*(Scalar scale) {
return {
.top_left = top_left * scale,
.top_right = top_right * scale,
.bottom_left = bottom_left * scale,
.bottom_right = bottom_right * scale,
};
}
[[nodiscard]] constexpr bool operator==(const RoundingRadii& rr) const {
return top_left == rr.top_left && //
top_right == rr.top_right && //
bottom_left == rr.bottom_left && //
bottom_right == rr.bottom_right;
}
[[nodiscard]] constexpr bool operator!=(const RoundingRadii& rr) const {
return !(*this == rr);
}
};
struct RoundRect {
RoundRect() = default;
@ -190,17 +133,6 @@ struct RoundRect {
namespace std {
inline std::ostream& operator<<(std::ostream& out,
const impeller::RoundingRadii& rr) {
out << "(" //
<< "ul: " << rr.top_left << ", " //
<< "ur: " << rr.top_right << ", " //
<< "ll: " << rr.bottom_left << ", " //
<< "lr: " << rr.bottom_right //
<< ")";
return out;
}
inline std::ostream& operator<<(std::ostream& out,
const impeller::RoundRect& rr) {
out << "(" //

View File

@ -0,0 +1,68 @@
// 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 "flutter/impeller/geometry/rounding_radii.h"
namespace impeller {
static inline void NormalizeEmptyToZero(Size& radii) {
if (radii.IsEmpty()) {
radii = Size();
}
}
static inline void AdjustScale(Scalar& radius1,
Scalar& radius2,
Scalar dimension,
Scalar& scale) {
FML_DCHECK(radius1 >= 0.0f && radius2 >= 0.0f);
FML_DCHECK(dimension > 0.0f);
if (radius1 + radius2 > dimension) {
scale = std::min(scale, dimension / (radius1 + radius2));
}
}
RoundingRadii RoundingRadii::Scaled(const Rect& in_bounds) const {
Rect bounds = in_bounds.GetPositive();
if (bounds.IsEmpty() || //
AreAllCornersEmpty() || !IsFinite()) {
// Normalize empty radii.
return RoundingRadii();
}
// Copy the incoming radii so that we can work on normalizing them to the
// particular rectangle they are paired with without disturbing the caller.
RoundingRadii radii = *this;
// If any corner is flat or has a negative value, normalize it to zeros
// We do this first so that the unnecessary non-flat part of that radius
// does not contribute to the global scaling below.
NormalizeEmptyToZero(radii.top_left);
NormalizeEmptyToZero(radii.top_right);
NormalizeEmptyToZero(radii.bottom_left);
NormalizeEmptyToZero(radii.bottom_right);
// Now determine a global scale to apply to all of the radii to ensure
// that none of the adjacent pairs of radius values sum to larger than
// the corresponding dimension of the rectangle.
Size size = bounds.GetSize();
Scalar scale = 1.0f;
// clang-format off
AdjustScale(radii.top_left.width, radii.top_right.width, size.width,
scale);
AdjustScale(radii.bottom_left.width, radii.bottom_right.width, size.width,
scale);
AdjustScale(radii.top_left.height, radii.bottom_left.height, size.height,
scale);
AdjustScale(radii.top_right.height, radii.bottom_right.height, size.height,
scale);
// clang-format on
if (scale < 1.0f) {
radii = radii * scale;
}
return radii;
}
} // namespace impeller

View File

@ -0,0 +1,98 @@
// 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.
#ifndef FLUTTER_IMPELLER_GEOMETRY_ROUNDING_RADII_H_
#define FLUTTER_IMPELLER_GEOMETRY_ROUNDING_RADII_H_
#include "flutter/impeller/geometry/point.h"
#include "flutter/impeller/geometry/rect.h"
#include "flutter/impeller/geometry/size.h"
namespace impeller {
struct RoundingRadii {
Size top_left;
Size top_right;
Size bottom_left;
Size bottom_right;
constexpr static RoundingRadii MakeRadius(Scalar radius) {
return {Size(radius), Size(radius), Size(radius), Size(radius)};
}
constexpr static RoundingRadii MakeRadii(Size radii) {
return {radii, radii, radii, radii};
}
constexpr bool IsFinite() const {
return top_left.IsFinite() && //
top_right.IsFinite() && //
bottom_left.IsFinite() && //
bottom_right.IsFinite();
}
constexpr bool AreAllCornersEmpty() const {
return top_left.IsEmpty() && //
top_right.IsEmpty() && //
bottom_left.IsEmpty() && //
bottom_right.IsEmpty();
}
constexpr bool AreAllCornersSame(Scalar tolerance = kEhCloseEnough) const {
return ScalarNearlyEqual(top_left.width, top_right.width, tolerance) &&
ScalarNearlyEqual(top_left.width, bottom_right.width, tolerance) &&
ScalarNearlyEqual(top_left.width, bottom_left.width, tolerance) &&
ScalarNearlyEqual(top_left.height, top_right.height, tolerance) &&
ScalarNearlyEqual(top_left.height, bottom_right.height, tolerance) &&
ScalarNearlyEqual(top_left.height, bottom_left.height, tolerance);
}
/// @brief Returns a scaled copy of this object, ensuring that the sum of the
/// corner radii on each side does not exceed the width or height of
/// the given bounds.
///
/// See the [Skia scaling
/// implementation](https://github.com/google/skia/blob/main/src/core/SkRRect.cpp)
/// for more details.
RoundingRadii Scaled(const Rect& bounds) const;
constexpr inline RoundingRadii operator*(Scalar scale) {
return {
.top_left = top_left * scale,
.top_right = top_right * scale,
.bottom_left = bottom_left * scale,
.bottom_right = bottom_right * scale,
};
}
[[nodiscard]] constexpr bool operator==(const RoundingRadii& rr) const {
return top_left == rr.top_left && //
top_right == rr.top_right && //
bottom_left == rr.bottom_left && //
bottom_right == rr.bottom_right;
}
[[nodiscard]] constexpr bool operator!=(const RoundingRadii& rr) const {
return !(*this == rr);
}
};
} // namespace impeller
namespace std {
inline std::ostream& operator<<(std::ostream& out,
const impeller::RoundingRadii& rr) {
out << "(" //
<< "ul: " << rr.top_left << ", " //
<< "ur: " << rr.top_right << ", " //
<< "ll: " << rr.bottom_left << ", " //
<< "lr: " << rr.bottom_right //
<< ")";
return out;
}
} // namespace std
#endif // FLUTTER_IMPELLER_GEOMETRY_ROUNDING_RADII_H_

View File

@ -101,6 +101,8 @@ struct TSize {
};
}
constexpr Type MinDimension() const { return std::min(width, height); }
constexpr Type MaxDimension() const { return std::max(width, height); }
constexpr TSize Abs() const { return {std::fabs(width), std::fabs(height)}; }