Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-shapes] Allow optional rounding parameter for polygon() #9843

Open
LeaVerou opened this issue Jan 24, 2024 · 32 comments
Open

[css-shapes] Allow optional rounding parameter for polygon() #9843

LeaVerou opened this issue Jan 24, 2024 · 32 comments

Comments

@LeaVerou
Copy link
Member

LeaVerou commented Jan 24, 2024

Motivation

There is a wide variety of use cases where authors want to clip or mask an element based on a shape that is basically a simple polygon plus rounding. Right now this is exceedingly complicated to do by hand (since you'd need to use path()), and practically impossible to do with the same flexibility of units that polygon() supports.

Tons of SO questions about this:

It also reminded me of my W3Conf 2013 site design:
image

The proposal

A good chunk of use cases only requires a single radius for all corners, and I have yet to see a use case that requires different horizontal and vertical radii. Therefore, even something as simple as the following would go a long way.

Change polygon() grammar from:

<polygon()> = polygon(
  <'fill-rule'>? ,
  [<length-percentage> <length-percentage>]#
)

to

<polygon()> = polygon(
  <'fill-rule'>? [round <length>]?,
  [<length-percentage> <length-percentage>]#
)

To cover even more use cases we could also explore allowing [round <length>]? on a per-point basis, to customize rounding for a specific point. This can ship later.

Rendering

There is always exactly one circle of a given radius that is tangential to each side adjacent to a corner < 180deg.

image

For corners >= 180deg, the other side would be used:

image
@LeaVerou
Copy link
Member Author

LeaVerou commented Jan 24, 2024

ping @astearns @atanassov (editors of CSS Shapes)

@astearns
Copy link
Member

The use case is valid, but I worry a little bit about diverging from SVG polygons (and the regular dictionary definition of “polygon”) for this. Could adding more unit flexibility in path() be an alternative solution?

@Crissov
Copy link
Contributor

Crissov commented Jan 24, 2024

When looking at this through an SVG lens, wouldn’t it be much like an equivalent to stroke-linejoin, so e.g. shape-edgejoin?
(I’m not really saying this would be a good fit for general CSS.)

@LeaVerou
Copy link
Member Author

The use case is valid, but I worry a little bit about diverging from SVG polygons (and the regular dictionary definition of “polygon”) for this. Could adding more unit flexibility in path() be an alternative solution?

Given the reluctance of browsers to work on SVG, and the fact that there is no SVG WG anymore to evolve SVG further, I think this in practice will end up holding the web platform back 😕
We have also not been tied to SVG capabilities in other places, e.g. we added conic gradients even though SVG does not support them and our gradient syntax is very different (and generally far more usable) than SVG's.
We can however add this to CSS and then try to get it added to SVG as well.

That said, adding more unit flexibility in path() is sorely needed as well! But it's a separate issue. I can try to spin off something.

@fantasai
Copy link
Collaborator

fantasai commented Jan 24, 2024

I think this makes a lot of sense. Syntactically, I would flip the order of the <length> and its identifying keyword (radius) so that the keyword comes first, and also switch to using a keyword compatible with the appropriate value of the corner-shape property rather than radius, in case at some point we want to allow other values.

@Loirooriol
Copy link
Contributor

What happens if the radius is big enough or the segments of the polygon small enough?

Browsers already tend to break with border-radius when the circles are centered at slightly weird places: https://bugzil.la/1823646, https://crbug.com/1262583

These situations will be much easier to happen with arbitrary polygons.

@LeaVerou
Copy link
Member Author

LeaVerou commented Jan 25, 2024

What happens if the radius is big enough or the segments of the polygon small enough?

Just like with border-radius there’s a limit of how large you can make the radius and anything beyond that is scaled down, we can do the same here. Hopefully there’s a non-iterative way to calculate that, I can reach out to some geometry experts about the specifics once we have consensus to pursue the functionality.

For an MVP, I wonder if we could come up with a much lower bound, since the vast majority of use cases only need a little rounding.

Browsers already tend to break with border-radius when the circles are centered at slightly weird places: bugzil.la/1823646, crbug.com/1262583

These have a lot to do with borders, which is not a consideration here.

@tabatkins
Copy link
Member

This sounds good!

I presume that the circle radiuses would be clamped to the smaller of half the neighboring side widths, so two adjacent corners can't run into each other and you get well defined arcs+lines.

Wrt differing from SVG polygon, the SVG polygon was intentionally made useless in the first place; it's literally just a polyline with a closing segment. I do not think we should take its (extremely harsh) limitations as any sort of limit on what we do with shapes.

@LeaVerou
Copy link
Member Author

Agenda+ to resolve on consensus to work on this.

@tabatkins
Copy link
Member

I would flip the order of the and its identifying keyword (radius) so that the keyword comes first, and also switch to using a keyword compatible with the appropriate value of the corner-shape property rather than radius, in case at some point we want to allow other values.

Agreed, plus this would match better with what the rectangular functions already allow (round ...).

@LeaVerou
Copy link
Member Author

I would flip the order of the and its identifying keyword (radius) so that the keyword comes first, and also switch to using a keyword compatible with the appropriate value of the corner-shape property rather than radius, in case at some point we want to allow other values.

Agreed, plus this would match better with what the rectangular functions already allow (round ...).

Hadn't realized there’s such precedent. Updated the proposal to reflect that.

@Loirooriol
Copy link
Contributor

Loirooriol commented Jan 28, 2024

I presume that the circle radiuses would be clamped to the smaller of half the neighboring side widths, so two adjacent corners can't run into each other and you get well defined arcs+lines.

In the following example the neighboring sides are √101 (slightly longer than 10), so your condition would allow a radius of 5, but anything bigger than √1.01 is problematic, even without taking into account the rounding of the other corners.

@tabatkins
Copy link
Member

Sorry, you're right, it's not the radius of the corner circle that's clamped to half the side width, but rather than affected segment itself. So in this example, the A corner can only cut in to x=5 at most (radius of about .5), while the C and B corners can only cut in to y=0 at most (also radius of about .5).

Now I'm doing some dang geometry to try and make a demonstration, and it's hard ;_;

@Loirooriol
Copy link
Contributor

It may be better to think in terms of clamping the position of the center of the circle. The center must always lie on the angle bisector of the corner, typically at a distance d = r / sin(α/2) from the corner, where r is the desired radius and α the angle of the corner.

However, we would clamp this distance by both midpoints of the sides, projected (perpendicularly to the sides) towards the angle bisector.

So if the sides have lengths s₁ and s₂, the distance between the corner and the center of the circle should be d' = min(d, s₁ / (2 cos(α/2)), s₂ / (2 cos(α/2)))

And then the clamped radius is r' = d' sin(α/2).

I'm not sure if that's the best approach (it might be too strict), but it should work.

@LeaVerou
Copy link
Member Author

LeaVerou commented Jan 31, 2024

@Loirooriol Would it prevent this problem if we clamp to sqrt(min(side1, side2))? Or what you gave. Too strict is fine, since most use cases need a rather small radius, and we can always expand the upper bound later.

One question to decide is whether we clamp all circles to the smallest of the radii (this is what border-radius does) or if each clamping operation is independent. Presumably author intent is to end up with the same rounding for the entire polygon, but this means that one small side somewhere can destroy the result for the whole polygon so I’m leaning towards doing it for each corner separately.

@tabatkins
Copy link
Member

It may be better to think in terms of clamping the position of the center of the circle.

I'm not certain how that would help. The position of the circle can vary a decent bit, relative to where it touches the sides, depending on the angle of the corner.

The thing we want to avoid is one corner's circle running into another; every circle cap should have a (possibly zero-length) straight segment between it and the next. Thus my "ensure it can't go past the halfway point of its adjacent sides" rule; this ensures that two adjacent corners will, at worst, meet at the center of their mutual side.

It might be that the trig you worked thru ends up producing that result, but I'm not sure without doing some diagrams by hand. ^_^

@tabatkins
Copy link
Member

One question to decide is whether we clamp all circles to the smallest of the radii (this is what border-radius does) or if each clamping operation is independent. Presumably author intent is to end up with the same rounding for the entire polygon, but this means that one small side somewhere can destroy the result for the whole polygon so I’m leaning towards doing it for each corner separately.

I think this would be best answered by staring at some examples and seeing what looks good. In all your example images the corners were rounded fairly small compared to the shape, so it's not clear what would look right in more extreme cases.

@LeaVerou
Copy link
Member Author

LeaVerou commented Feb 1, 2024

I think this would be best answered by staring at some examples and seeing what looks good. In all your example images the corners were rounded fairly small compared to the shape, so it's not clear what would look right in more extreme cases.

That’s not a coincidence, the vast majority of cases I’ve found need fairly small rounding. However looking at them again, eg in the speech bubble, the rounding of the corners should not really be affected by the size of the pointer. So I think per-corner makes the most sense. Also in that case you’d need to be able to override it there anyway.

@Loirooriol
Copy link
Contributor

Would it prevent this problem if we clamp to sqrt(min(side1, side2))?

I don't see the reasoning of that formula. In particular sqrt makes values between 0 and 1 larger, making it easier to be problematic.

One question to decide is whether we clamp all circles to the smallest of the radii (this is what border-radius does) or if each clamping operation is independent.

I'm leaning towards clamping independently

I'm not certain how that [clamping the position of the center of the circle] would help

When I wrote the comment it seemed simpler to think in those terms to "ensure it can't go past the halfway point of its adjacent sides". But thinking again, using tan() to express the tangent point limits in terms of the radius is more straightforward.

Basically, the provided radius needs to be clamped to not exceed tan(α/2) s₁ / 2 nor tan(α/2) s₂ / 2.

@LeaVerou
Copy link
Member Author

LeaVerou commented Feb 1, 2024

Basically, the provided radius needs to be clamped to not exceed tan(α/2) s₁ / 2 nor tan(α/2) s₂ / 2.

So basically the upper bound per angle would be min(tan(α/2) s₁ / 2, tan(α/2) s₂ / 2), right? SGTM. Like I said, most uses involve such a small radius, that I think any reasonable lower bound would be fine.

A couple other things to explore:

  • if we ship with a very strict upper bound, can we relax it later, or would that involve web compat issues?
  • It seems that the functionality of overriding the radius per angle may need to be part of the MVP — how much does that increase implementation effort?

@Loirooriol
Copy link
Contributor

Yes, these maximum radii ensure that the tangential points won't go past the halfpoints of the sides. This diagram may make it clearer visually:

@Loirooriol
Copy link
Contributor

Loirooriol commented Feb 4, 2024

It's worth noting that this clamping (done independently per corner) doesn't guarantee that a simple polygon will stay as a non-intersecting shape:

@Loirooriol
Copy link
Contributor

Another idea could be to allow the circles to be tangential to non-adjacent sides, basically skipping corners which are too close:

But this may be order-dependent and still need clamping at some point when running out of corners.

@tabatkins
Copy link
Member

tabatkins commented Feb 5, 2024

The polygon may no longer be simple, but it's still well-defined. I think the correct answer for the author is just "don't round that shape, then". We shouldn't guess too hard at what people want in such odd situations.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-shapes] Allow optional rounding parameter for `polygon()` , and agreed to the following:

  • RESOLVED: Work on adding corner rounding to polygon()
The full IRC log of that discussion <emilio> lea: this is about resolving whether we should work on this
<emilio> ... noticed that even though we have polygon() and clip-path, author still resort to images because they don't cover their use cases, the use cases include rounding
<emilio> ... many cases the same as the polygon
<emilio> ... I posted many
<emilio> ... proposal was to add a round parameter to the polygon, and then to also add an optional on each individual coordinate to customize rounding to that corner
<emilio> ... the way it'd be painted would be "a circle of that radius that is tangential to both vertices"
<emilio> ... if it's >180deg it'd draw outside
<emilio> ... we did some research on the max values to not end up with the right shape
<TabAtkins> q+
<astearns> q+
<emilio> ... question is whether there are blockers or should we work on this
<fantasai> +1 to the proposal
<astearns> ack TabAtkins
<emilio> TabAtkins: I agree with this. There are some details about limits
<emilio> ... but they don't need to be worked out here. Suggested functionality and syntax sounds good
<emilio> q+
<astearns> ack astearns
<emilio> ... so very supportive
<emilio> astearns: two questions
<emilio> ... perfectly fine with the proposal
<emilio> ... but I'm a bit concerned about rounding polygon corners because it's no longer a polygon
<emilio> ... so I wonder if it'd be better to fix path() to make this syntax simpler
<emilio> ... but if we do this I don't mind
<emilio> TabAtkins: we already round rectangles
<emilio> astearns: the other question is that it'd be nice if the rounding matches the rounding for shape-margin for polygons
<emilio> ... there's some text in the shapes spec that says how we do that
<emilio> TabAtkins: for expanding shapes it works the same way
<emilio> ... when it goes in I'm less sure
<astearns> ack emilio
<lea> re: is it a polygon any more, e.g. look at the W3Conf example, would you describe these as anything other than hexagons?
<dholbert> emilio (IRC): same line as astearns (IRC) ... Do we have anything to make this sort of path easily already right now?
<dholbert> TabAtkins (IRC): We do in the canvas specs. It has an operation to do this sort of rounding, corner-by-corner
<bkardell_> let's add it to svg :)
<dholbert> TabAtkins (IRC): It doesn't exist in SVG, but in the other thread I suggested pulling in some of the canvas operations as well
<fantasai> bkardell_, you volunteering to edit SVG? :)
<dholbert> emilio (IRC): seems like this use case should be/become covered by path, since it is a path
<lea> bkardell_: we'd need an SVG WG first and buy-in from implementers (who are *very* unwilling to implement any SVG thing)...
<astearns> ack fantasai
<Zakim> fantasai, you wanted to respond to that
<dholbert> emilio (IRC): it seems reasonable to add it to polygon, but we should make sure you can do this for a path too
<emilio> fantasai: could you do this in path? Yeah, but lots more work
<TabAtkins> the canvas `arcTo()` is literally "define a corner + a radius, and add the rounded corner to your path"
<emilio> ... by making the assumption that everything is a line it makes it a lot easier in polygon()
<TabAtkins> bkardell_, I don't think we need to add it to SVG *per se*, but adding it to shape() is fine
<emilio> ... telling authors that they should use path() for this is fine
<lea> +1 to fantasai's point, we even have a TAG principle to avoid cliffs where a small amount of use case complexity results in a large increase in complexity
<bkardell_> igalia is working on svg. I admit it is not 'new' svg, but if there are people (or organizations) interested in svg they should definitely maybe talk to me :)
<emilio> astearns: agree but we should make it easy to move from one to another
<lea> s/1 to fantasai's point, we even have a TAG principle to avoid cliffs where a small amount of use case complexity results in a large increase in complexity/1 to fantasai's point, we even have a TAG principle to avoid cliffs where a small amount of use case complexity results in a large increase in UI complexity/
<emilio> PROPOSAL: Work on adding corner rounding to polygon()
<emilio> RESOLVED: Work on adding corner rounding to polygon()
<fantasai> \^_^/
<emilio> emilio: should we resolve on rounded lines for path()?
<emilio> fantasai: there's another issue for that, a bit more complicated

@Afif13
Copy link

Afif13 commented Feb 14, 2024

I like this feature, I made a try using the Paint API: https://css-tricks.com/exploring-the-css-paint-api-rounding-shapes/ where I also considered the case of one radius per corner. It works pretty well and I was able to get some cool examples.

@LeaVerou
Copy link
Member Author

I like this feature, I made a try using the Paint API: css-tricks.com/exploring-the-css-paint-api-rounding-shapes where I also considered the case of one radius per corner. It works pretty well and I was able to get some cool examples.

Great stuff! Not sure if this is something you have the cycles for, but it would be very useful to extract the code that does the rounding into code that we can use to experiment with the algorithm for polygon().

@Afif13
Copy link

Afif13 commented Feb 16, 2024

Great stuff! Not sure if this is something you have the cycles for, but it would be very useful to extract the code that does the rounding into code that we can use to experiment with the algorithm for polygon().

This demo should contain the final code of the API with a lot of examples: https://codepen.io/t_afif/pen/GREaoMJ

There are a lot of code to extract the points and their radii from the variables (and also compute calc()). Then this is the part that will draw the shape:

   /* we start the path */
   ctx.beginPath();
    ctx.moveTo(Cpoints[0][0],Cpoints[0][1]); 
   /**/

    var i;
    var rr;
    for (i = 0; i < (Cpoints.length - 1); i++) { /* loop all the points */
      /* I start with a complex calculation to avoid the cases illustrated by @Loirooriol and find a maximum radius  */
      var angle = Math.atan2(Cpoints[i+1][1] - Ppoints[i][1], 
                             Cpoints[i+1][0] - Ppoints[i][0]) -
                  Math.atan2(Cpoints[i][1] - Ppoints[i][1], 
                             Cpoints[i][0] - Ppoints[i][0]);
      if (angle < 0) {
        angle += (2*Math.PI)
      }
      if (angle > Math.PI) {
        angle = 2*Math.PI - angle
      }
      var distance = Math.min(Math.sqrt((Cpoints[i+1][1] - Ppoints[i][1]) ** 2 + 
                                        (Cpoints[i+1][0] - Ppoints[i][0]) ** 2),
                              Math.sqrt((Cpoints[i][1] - Ppoints[i][1]) ** 2 + 
                                        (Cpoints[i][0] - Ppoints[i][0]) ** 2));
     /**/
      rr = Math.min(distance * Math.tan(angle/2),Radius[i]); /* I use a min function to get either the specified radius or the maximum allowed */
      ctx.arcTo(Ppoints[i][0], Ppoints[i][1], Cpoints[i+1][0],Cpoints[i+1][1], rr); /* I draw the curve here */
    }
  /* an extra curve to get back to the initial point*/
  var angle = Math.atan2(Cpoints[0][1] - Ppoints[i][1], 
                             Cpoints[0][0] - Ppoints[i][0]) -
                  Math.atan2(Cpoints[i][1] - Ppoints[i][1], 
                             Cpoints[i][0] - Ppoints[i][0]);
   if (angle < 0) {
      angle += (2*Math.PI)
   }
   if (angle > Math.PI) {
      angle = 2*Math.PI - angle
   }
   var distance = Math.min(Math.sqrt((Cpoints[0][1] - Ppoints[i][1]) ** 2 + 
                                        (Cpoints[0][0] - Ppoints[i][0]) ** 2),
                              Math.sqrt((Cpoints[i][1] - Ppoints[i][1]) ** 2 + 
                                        (Cpoints[i][0] - Ppoints[i][0]) ** 2));
    rr = Math.min(distance * Math.tan(angle/2),Radius[i]);
    ctx.arcTo(Ppoints[i][0], Ppoints[i][1], Cpoints[0][0],Cpoints[0][1], rr);
   /* end of the path*/
    ctx.closePath();

cc @Loirooriol

@thebabydino
Copy link

This is something I've found myself needing often, both when it comes to the SVG <polygon>/ <polyline> and when it comes to the CSS polygon().

Back in 2016, I even wrote a polyfill that did this for SVG <polygon>/ <polyline>. I intended to release it with an article explaining usage, how it works, but then both the article and the polyfill ended up growing and growing to cover every possible use case and... after 8 months of working on them, I just dropped the whole idea because those were 8 months of zero income and I decided to do something that would be worth it.

This is a demo illustrating rounding regular polygons https://codepen.io/thebabydino/pen/MbxQmJ

round regular polygon

The same idea was applied for rounding any <polygon>/ <polyline> https://codepen.io/thebabydino/pen/oWjVvd/bf097ea4fe45bfb6a8603484fa544692

When it comes to CSS, I've used a bunch of tactics.

A very common one has been simply approximating the corner rounding with extra polygon() points, like this:

screen of interactive demo showing the polygon() points

Here is a Sass mixin doing that (not sure if there isn't a newer one) https://codepen.io/thebabydino/pen/dymLJGo/a343189aa330aee3df89883a3215a91d

This is an example of a demo using something of the kind https://codepen.io/thebabydino/pen/wvmRXeM/c5f191a40fae4feb587d9e3245d88889

screenshot of demo linked above

In other instances, I've used a filter https://codepen.io/thebabydino/pen/wvQaOpz

screenshot of previously linked demo

In some particular cases, I've used CSS gradient masks for the effect https://codepen.io/thebabydino/pen/NWXbYwO - this is obviously very limited.

@astearns
Copy link
Member

I’ve added some spec text in ce3f2ac

Please take a look and see if it is correct and sufficient. I still need to add examples (@Loirooriol could I use your diagram in #9843 (comment)?)

@Loirooriol
Copy link
Contributor

@astearns Sure!

@yisibl
Copy link
Contributor

yisibl commented Jan 13, 2025

There is support in Figma for setting rounded corners directly on polygons, and there will be an algorithm to limit the maximum value.

output.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Wednesday morning
Development

No branches or pull requests

10 participants