Skip to content

Conversation

@jslavitz
Copy link
Contributor

@jslavitz jslavitz commented Feb 5, 2019

Adds Cupertino "super ellipse" rect. Fixes #13914, reverts #26295.

@goderbauer goderbauer added framework flutter/packages/flutter repository. See also f: labels. f: cupertino flutter/packages/flutter/cupertino repository labels Feb 5, 2019
@xster
Copy link
Member

xster commented Feb 5, 2019

Please check the CI failure

file:///tmp/flutter%20sdk/packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart:415:22: Error: Can't declare 'maxMultiplier' because it was already used in this scope.
        final double maxMultiplier = ui.lerpDouble(
                     ^^^^^^^^^^^^^

@Hixie
Copy link
Contributor

Hixie commented Feb 5, 2019

We shouldn't say this is Cupertino-related. It's just a shape, it's not platform-specific.

Please check all the documentation for mentions of Superellipse, we want to make sure they're all updated (there's at least one that I know of).

@Hixie
Copy link
Contributor

Hixie commented Feb 5, 2019

@Salby suggested ContinuousCornerBorder, which I think is a better name.

@Hixie
Copy link
Contributor

Hixie commented Feb 7, 2019

I don't think we should be adding a feature and renaming the class in the same PR, that's a bit confusing (makes it very hard to review). I suggest we let #27586 take care of the rename.

It's not entirely clear to me what the new feature here does? Are there before/after pictures we can see?

@jslavitz
Copy link
Contributor Author

@Hixie Ok that's fair. I think the name 'ContinuousCornerRectangleBorder' makes more sense tho? Given that We have 'RoundedRectangleBorder' and not 'RoundedBorder' ?

@Hixie
Copy link
Contributor

Hixie commented Feb 14, 2019

The term "rounded rectangle" is the name of the shape that "RoundedRectangleBorder" creates; search Google for [Rounded Rectangle] and you get millions of hits and all the pictures clearly describe what we're doing; see also RRect and the corresponding methods of the canvas.

On the other hand the phrase "Continuous Corner Rectangle" doesn't have any hits on Google at all. The phrase "Continuous Corner" does have some hits, especially if you search for [ios continuous corner]; indeed there are a number of terms (e.g. continuous curvature, squircle) that are used. None of them, as far as I can tell, have the word "rectangle" in them, though.

// The multiplier of the radius in comparison to the smallest edge length
// used to describe the minimum radius for this shape.
//
// This is multiplier used in the case of an extreme aspect ratio and a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what extreme aspect ratio is concretely and why are we doing something special in that case and what happens if we just followed the 3 instead of 2.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if we used 3 instead of 2 in this "extreme" case, then the shape would still look noticeably flat on the edges instead of rounded like with the search bar on native iOS.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too. Don't use subjective words. Quantify and explain why those numbers are important.

// This is multiplier used in the case of an extreme aspect ratio and a
// small extent value. It can be less than 'maxMultiplier' because there
// are not enough pixels to render the clipping of the shape at this size so
// it appears to still be concave (whereas mathematically it's convex).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still confused about these various usages of concave vs convex. Isn't the shape always convex? When does it become concave? Or are you describing the path drawing glitch caused by this specific implementation of the bezier curve when it clips?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now realize that a lozenge isn't actually convex. But yah a lozenge doesn't correctly describe the shape that the curves make when they clip as the shape in reality is more like |X| with a lozenge in middle. Which is a convex shape.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're mixing a bunch of things.

If we're talking about these shapes assuming we produce protruding rendering artifacts when the shapes clip, then:

  • If it doesn't clip, it's a simple non-intersecting polygon that is convex
  • If it does clip, it is a non-simple self-intersecting polygon (which by definition is neither convex nor concave).
    We shouldn't do this since the fact that we produce rendering artifacts is an implementation detail that we use bezier curves and has nothing to do with superellipses themselves.

If we're talking about these shapes assuming we don't render protruding artifacts (e.g. we end up producing a lozenge), then:

  • If it doesn't clip (n>2), it's a simple non-intersecting polygon that is convex.
  • If it does clip (n<2), then it is still a simple non-intersecting polygon that is convex

// If the smallest edge length is less than this value, the dynamic radius
// value can be made smaller than the 'maxMultiplier' while the rendered
// shape still does not visually clip.
const double minRadiusEdgeLength = 200.0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this number derived?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By visual approximation and comparison with the native Cupertino search bar rounded rect.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it should be in Cupertino.

// As the edge length approaches 200, the limitedRadius approaches ~3 –- the
// multiplier of the radius value where the resulting shape is concave (ie.
// does not visually clip) at any dimension.
final double multiplier = ui.lerpDouble(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? Do we think apple is doing this?

Copy link
Contributor Author

@jslavitz jslavitz Feb 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to make a more rounded, stadium-esque shape at a small width or height. And yah, I believe apple does this for their search bar.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, then this in-between-superellipse-and-stadium-ness should be in cupertino.

@tvolkert
Copy link
Contributor

tvolkert commented Mar 1, 2019

@chinmaygarde @liyuqian can you review this with an eye towards performance?

return Path()
..moveTo(leftX(1.52866483), topY(0.0))
..lineTo(rightX(1.52866471), topY(0.0))
..cubicTo(rightX(1.08849323), topY(0.0),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skia team had a discussion about this "squircles" about 1.5 years ago. It seems that Skia wanted to add something like bool SkPath::asSuperellipse(SkRect*, int* exp); but it's not there yet.

From my own CPU path rasterization experience, cubics are more expensive than quadratics. Although it's unlikely to be a bottleneck for the whole app, it might be safer to just use quadratics as I heard that you can either use a single cubic or 2 quadratics to approximate the squircle.

CC Skia's GPU guru @bsalomon @csmartdalton here for comments about cubics vs. quadratics performance on GPU.

Of course, it would be better if Skia just added bool SkPath::asSuperellipse(SkRect*, int* exp); API so it can freely change the most performant implementation without Flutter modifying the source code. Not sure if the benefit is worth the maintenance cost (CC @reed-at-google ).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shapes are not yet linked as this PR needs to land before the shape images can be generated. Tho you should be able to see the shapes at this link: https://github.com/flutter/assets-for-api-docs/pull/55/files

return Path()
..moveTo(leftX(2.00593972), topY(0.0))
..lineTo(originX + width - 1.52866483 * radius, originY)
..cubicTo(rightX(1.63527834), topY(0.0),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. If quadratics can be used, I think they're usually more preferred than cubics from Skia's perspective. My anecdotal experience also told me that cubics are more prone to bugs and rendering artifacts than quadratics since Skia has to handle many more edge cases for cubics than quadratics. Skia's major clients such as Chrome and Anroid probably use much more quadratics than cubics which makes quadratics better tested. @reed-at-google : please correct me if my impression were wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic paths are definitely present in our framework. If they're prone to bugs or rendering artifacts, we should make a concerted effort to remove them from our codebase and remove the API until ready.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, cubics definitely passes Skia's quality control which means that it's fast and correct probably 99.99% of the time. It's just that quadratics are even faster and more reliable. So if it's easy to use quadratics instead cubics, we'll prefer quadratics.

@jslavitz jslavitz requested a review from xster March 6, 2019 01:22
@liyuqian
Copy link
Contributor

liyuqian commented Mar 6, 2019

Just checked perf.skia.org for a comparison between quads and cubics. The results are surprising to me on the GPU side. Maybe need some experts @bsalomon and @csmartdalton to verify the results.

First I checked path_hairline_big_AA on Moto G4. On the CPU side (config = 8888), quad is twice as fast as cubic (which is expected). But GPU speed looks the same between cubic and quad, and they're all many times slower than CPU. I thought that Skia would just use CPU path renderer to rasterize the path if the GPU renderer is too slow. Maybe this test is just generating non-sense data for GPU?

Then I checked path_hairline_big_AA on Pixel2XL which unfortunately doesn't have CPU (8888) performance data. But the GPU data there is really astonishing. The cubic is about 4x as fast as the quad? I don't understand how that could happen. One possibility is that the GPU falls back to CPU on the cubic, but it uses some very slow GPU renderer on the quad similar to those used on MotoG4?

@xster
Copy link
Member

xster commented Mar 6, 2019

@liyuqian what is the actionable item here for this PR?

@liyuqian
Copy link
Contributor

liyuqian commented Mar 6, 2019

Ok, forget about the perf.skia.org data that I put earlier. It's not very helpful to track GPU performance according to the Skia team.

Our actionable item, if there's time (there's no hurry to land this), should be to add a Flutter driver performance test ourselves to check the performance difference between quadratics and cubics implementation of this widget. That driver test will also be helpful in the future to catch any unexpected performance regression.

@xster
Copy link
Member

xster commented Mar 6, 2019

Thanks. @jslavitz, let's add a microbenchmark test to check the performance of a CupertinoButton when you start using this shape in widgets in a next PR.

See something like complex_layout_scroll_perf__timeline_summary for example.

Copy link
Member

@xster xster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're getting closer. Still some comments on the documentation and the dynamic ratio on the continuous rect.

/// In an attempt to keep the shape of the rectangle the same regardless of its
/// dimension (and to avoid clipping of the shape), the radius will
/// automatically be lessened if its width or height is less than ~3x the
/// declared radius. The new resulting radius will always be maximal in respect
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already implied in the previous sentence. I would just remove this.

/// to the dimensions of the given rectangle.
///
/// To achieve the corner smoothness with this shape, ~50% more pixels in each
/// dimensions are used to render each corner. As such, the ~3 represents twice
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still reads like too much math in the sentence. Just "Therefore, with 2 corners per side, the height or width needs to be minimally ~1.5 * 2 = ~3 times the length of the radius to accommodate the smooth corner transitions."

/// width of pixels that are used to construct it. For example, if a
/// rectangle had and a corner radius of 25, in reality ~38 pixels in each
/// dimension would be used to construct a corner and so ~76px x ~76px would be
/// the minimal size needed to render this rectangle.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"... needed to render this rectangle before reducing the radius."

Otherwise, it might sound like we can't render this rectangle at all.

/// corner and so ~76px x ~38px would be used to render both corners on a given
/// side.
///
/// This shape will always have 4 linear edges and 4 90º curves. However, for
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm asking what's the motivation for doing something special for smaller sizes and not have one single set of rules. The users will be wondering too after reading this paragraph.

final BorderRadiusGeometry borderRadius;
/// The radius will be clamped to 0 if a value less than 0 is entered as the
/// radius. By default the radius is 0. This value must not be null.
///
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each field's doc must be able to stand on its own. Mention here too the fact that the value you put in isn't always what actually gets drawn.

// If the smallest edge length is less than this value, the dynamic radius
// value can be made smaller than the 'maxMultiplier' while the rendered
// shape still does not visually clip.
const double minRadiusEdgeLength = 200.0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it should be in Cupertino.

// As the edge length approaches 200, the limitedRadius approaches ~3 –- the
// multiplier of the radius value where the resulting shape is concave (ie.
// does not visually clip) at any dimension.
final double multiplier = ui.lerpDouble(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, then this in-between-superellipse-and-stadium-ness should be in cupertino.

/// is approximately a gaussian curve instead of a step function as with a
/// traditional half circle round.
///
/// The ~3 represents twice the ratio (ie. ~3/2) of a corner's declared radius
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was copy pasted but doesn't fit in the flow of the paragraphs here. Make sure this doc reads right.

// shape has a side width as the stroke is drawn centered on the border of
// the shape instead of inside as with the rounded rect and stadium.
if (sideWidth > 0.0)
rect = rect.deflate(actualSideWidth / 2.0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't change the value of incoming function parameters. Make new variables.

@xster
Copy link
Member

xster commented Apr 2, 2019

Same here. We'll pick it up from your branch in a new PR.

@xster xster closed this Apr 2, 2019
@lukepighetti
Copy link
Contributor

@xster and which PR is that?

@lukepighetti
Copy link
Contributor

lukepighetti commented Mar 10, 2020

I just pulled down the ContinuousRectangleBorder and ContinuousStadiumBorder classes from this PR and they are fantastic. Nothing else I've been able to find comes close.

Screen Shot 2020-03-10 at 7 47 03 PM

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 1, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

f: cupertino flutter/packages/flutter/cupertino repository framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Buttons on iOS do not use squircle paths

10 participants