w3c / csswg-drafts

CSS Working Group Editor Drafts

Home Page:https://drafts.csswg.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[css-backgrounds] The shape of box-shadow should be a circle for a box with border-radius:50% and big spread

wangxianzhu opened this issue · comments

https://www.w3.org/TR/css-backgrounds-3/#shadow-shape

For the following case:
data:text/html,<div style="width: 40px; height: 40px; border-radius: 20px; box-shadow: black 0 0 0 80px; margin: 100px">
should we expect that the outer edge of the shadow is a circle (instead of a rounded rect with the 1+(r-1)^3 adjustment)?

Perhaps we should apply the adjustment only if the radius is smaller than e.g. 1/4 of the box size?

Hm, I was about to say that browsers currently produce circles (testing Firefox and Chrome), but turns out that's because they don't implement the cubic reduction at all; if your example is changed to a 1px border-radius it still produces a huge rounded rect rather than a nearly-sharp square.

It does seem like it should maintain roundness when it's entirely round, I agree. How to achieve that smoothly and reasonably is an interesting question. /cc @fantasai @bradkemper

Chrome has changed to conform to the current spec. Since the change, people have filed bugs (e.g. https://crbug.com/1322942) about the case.

I believe that anchoring the geometric adjustment of the shadow shape to the absolute value of corner radii regardless the proportions of the shape was probably not the best idea, too. The result when the corners of the shadow become relatively sharper than the corners of the original shape is far from intuitive and often undesired.

Maybe the shape of the shadow should simply be geomtrically similar to the original shape, i.e. roundings should take the same part of the side as they do on the original shape? At first glance, this would produce the smooth transition between sharp corners and complete roundness in the most natural way.
Screenshot 2022-05-19 at 15 55 28

I tend to agree that geometric similarity can sound good.

However, this would imply that the shadow wouldn't spread by the same amount in both axes, e.g.

<div style="width: 500px; height: 100px; background: cyan; border-radius: 250px / 50px; box-shadow: 0 0 0 125px blue"></div>

The border area is cyan, it's 500x100 with a border-radius of 250px / 50px, i.e. 50%. If the shadow spreads 125px horizontally, then border radius of the shadow should have an X component of 250+125 = 375, and the Y component should preserve the ratio in order to get a similar shape: 50 * 375/250 = 75. So the border radius of the shadow should be 375px / 75px.

However, while the shadow is 750px wide, so 375px is the 50%, the shadow should also spread 125px vertically, becoming 35px tall. This breaks the similarity: it should be 150px tall, so that 75px is the 50%. This difference is represented in red in the image.

So similarity would require changing the spread distance to apply in the biggest dimension, and resolving the other one proportionally. For continuity, this would need to happen regardless of whether there is a border-radius. But this would worsen usecases where you don't care about similarity, and instead just want a shadow of the same thickness in both axes, e.g. in order to have a secondary border.

I guess what could work is:

  1. Calculate the radius of the border corners in percentages of the border box size
  2. Set the radius of the shadow corners to be the same percentages, but now resolved with the shadow size.

Or in other words,

  1. Resolve the radius of the border corners as a pair of lengths x / y.
  2. Let the sizes of the border box be b_x and b_y
  3. Let the sizes of the shadow box be s_x and s_y
  4. Set the radius of the corresponding corner of the shadow box to calc(x * s_x / b_x) / calc(y * s_y / b_y), or to 0px to avoid dividing by zero.

So if you have the initial border-radius: 0px (i.e. 0%), then the shadow will keep being sharp with 0px.

And if you have border-radius: 50% then the shadow will keep being a full circle/ellipsis with 50% of its size.

I’m seeing a lot of tweets about this breaking change in calculation like: https://twitter.com/alvaro_montoro/status/1532869371248394241?s=21&t=vhQ6aoBDItk9fALwfe5WDg

Isn’t it fair to say that a “wrong” calculation, but one done for years consistently across browsers and a calculation people rely on for illustrations is a “Don’t Break The Web” case?

IMO radical changes in CSS rendering should be avoided.

I completely missed this. Was there a spec change? Doesn't seem like there should be breaking changes this late in the game.

I would expect a circle shape to have a circle-shape box-shadow, no matter how big the spread.

I don't think it's a spec change, just implementations catching up with the spec.

We had implemented the cubic reduction algorithm for box-shadow. I've now just tried patching in the proposed algorithm from @Loirooriol above for the three tests described in the comments above:

  • <div style="width: 40px; height: 40px; border-radius: 2px; box-shadow: black 0 0 0 80px">
  • <div style="width: 40px; height: 40px; border-radius: 20px; box-shadow: black 0 0 0 80px">
  • <div style="width: 500px; height: 100px; background: cyan; border-radius: 250px / 50px; box-shadow: 0 0 0 125px blue">

Current specification rendering on the left, revised algorithm on the right. Overall it looks like an improvement.

Untitled-1

@fantasai @tabatkins what do you think? Wouldn't @Loirooriol's algorithm be a better solution for the zero/nonzero radius discontinuity problem than cubic (or likely any other) function of the absolute value of the radius that disregards its relative size (hence perceived shape)?

Agenda+ to accept @Loirooriol's algorithm

I guess the potential surprising/undesirable outcome of my proposal is that is you set border-radius to a length, then you get circular corners. But if the width and height are different, the corners of the shadow will be elliptical instead of circular.

This e.g. makes the corners appear thicker when using a thin shadow spreading 1px:

demo

Another approach could be just keeping

To preserve the box’s shape when spread is applied, the corner radii of the shadow are also increased (decreased, for inner shadows) from the border-box (padding-box) radii by adding (subtracting) the spread distance (and flooring at zero).

with no further adjustment. This would imply that shadows would have rounded corners even with border-radius: 0px, but the trick would be that the initial value would be changed to border-radius: none. none would behave like 0px, but keeping shadows sharp.

Though this may have compat problems since probably there are various sites setting border-radius: 0px expecting a sharp shadow.

Ideally someone (code for "not me" 😄) should update https://drafts.csswg.org/css-backgrounds-3/spread-radius to add the new proposed algorithm(s) and - most importantly - the ability to resize the box.

Clearly a lot of thought has gone into which algorithm is best, I think the only thing that was missed was how it performs on zero-size boxes.

A box-shadow with no offsets or blur, and a large spread, should be indistinguishable from a border of the same width as the spread (aside from affecting layout, etc.). The shape should match, and the effect of having rounded corners should match. I guess I'm missing what the issue is that's causing it to be so different. A border of even width around a circle is still circular.

I guess I'm missing what the issue is that's causing it to be so different

The difference is that border-radius affects the border area, so no need to do anything for the border other than:

The padding edge (inner border) radius is the outer border radius minus the corresponding border thickness. In the case where this results in a negative value, the inner radius is zero.

But a spreading shadow goes beyond the border area, so its radius needs to be increased. And here there is a problem, because a sharp 0px should also produce a sharp shadow, but we should also have continuity.

I think the outer edge of a rounded outline should follow the same rule as box-shadow spread. Filed #7378.

The CSS Working Group just discussed box-shadow roundness/sharpness.

The full IRC log of that discussion <TabAtkins> Topic: box-shadow roundness/sharpness
<TabAtkins> github: https://github.com//issues/7103
<TabAtkins> [looking for Oriol on the call]
<TabAtkins> fantasai: Let's push to next week

Yet another alternative is to use Oriol's algorithm for one the larger of the corner dimensions and then use the initial border radius aspect ratio to compute the other smaller dimension. Since the percent increase of the shadow in different dimensions is not the same, it would at least preserve the corner shape.

It works nice for "typical" rounded corners of otherwise rectangular boxes, without changing the shape of the corner, and it works well for a circle.

However, it adds a straight edge to the side that has a larger percent increase in length. This isn't noticeable in rectangular boxes but a bad case I found is that it tends to not look too nice on ellipses
ellipses

It sounds like maybe we need some kind of threshold formula? E.g. supposing the straight length of the side is L and the radius on that side is R, we'd do something like:

  • if L = 0 use the percentage formula
  • if L >= R use the spec’s cubic formula
  • if 0 < L < R interpolate between the two formulae

Although I think we need some experimentation to know what the right thresholds are...

Alternatively, maybe treat percentage radii as percentages of the shadow, and use the existing formula for length radii?

The CSS Working Group just discussed box shadows and circles.

The full IRC log of that discussion <TabAtkins> Topic: box shadows and circles
<TabAtkins> github: https://github.com//issues/7103
<TabAtkins> oriol: When you specify border-radius, the length is the corner radius of the outer border edge
<TabAtkins> oriol: For inner edge we subtract the border width from that
<TabAtkins> oriol: Spread is the opposite
<TabAtkins> oriol: Initial vaule of spread is 0 tho, so this would make an ellipitical border radius ahve corners
<TabAtkins> oriol: Firefox has special case for spread radius of 0, keeping it sharp. But 0.1 makes rounded corners
<TabAtkins> oriol: Suggest adding a correction term that gives sharp corners at 0 and continouously transforms to round.
<TabAtkins> oriol: So the issue: if you say border-radius:50% you get an ellipse.
<TabAtkins> oriol: If you have a shadow you probably also expect it will be an ellipse.
<TabAtkins> oriol: Blink changed impl to match the spec, and people complained their shadows were no longer circles.
<TabAtkins> oriol: Proposal in the issue is, instead of adding the spread distance and subtracting a correction term, we could express the border-radius as a % and then, for the shadow, we use the same % but resolved agaisnt th esize of th eshadow
<TabAtkins> oriol: This seems to improve various cases, we have posted images in the thread
<TabAtkins> oriol: Shadows look better, stay circular
<astearns> images in this comment: https://github.com//issues/7103#issuecomment-1146870682
<TabAtkins> oriol: Downside is if you specify a length, and elemnet has circular corners, shadow will have circular corners. New change will instead make them elliptical if the box isn't square
<TabAtkins> oriol: Vlad suggested a variant where we only resolve to a % in one axis, then keep the same aspect ratio for the corner
<astearns> problem ellipse images in this comment: https://github.com//issues/7103#issuecomment-1168157338
<TabAtkins> oriol: Problem is if th eelement is a full ellipse, you won't get an ellipse shadow. Circles stay circles but in an ellipse you'll get flat edges on the short axis.
<TabAtkins> oriol: I proposed another idea - we could say that for the shadow we just add spread-distance to the border-radius
<TabAtkins> oriol: This would imply that for 0px you'd have rounded corners
<TabAtkins> oriol: But we'd also change the initial value for border-radius is "none", which would stay sharp. Unsure about compat tho.
<TabAtkins> oriol: fantasai proposed interpolating between some radius formulas.
<TabAtkins> oriol: Various options, not sure which is best.
<TabAtkins> fantasai: Two ideas. One is the problem is largely around circular/oval shapes. Many of those are done with %s.
<TabAtkins> fantasai: So we could say if the radius is a % we maintain it as a % in the spread shape.
<TabAtkins> fantasai: Dunno if that really works since the other way to do a circle is to do a huge px length and we scale it down.
<TabAtkins> fantasai: So it depends on how authors are specifying things.
<TabAtkins> fantasai: Another possibility is to interpolate based on how much of a straight side we have.
<TabAtkins> fantasai: So if the straight side is longer than the radius, we use existing formula which is good for rectangular shapes
<TabAtkins> fantasai: If straight side is 0 we use the % formula
<TabAtkins> fantasai: And between we can interpolate.
<fantasai> TabAtkins: I hadn't gone this deep before, I suspect I have opinions, but would like to take up at a later call, maybe at F2F
<fantasai> fantasai: F2F sounds good, can draw stuff
<fantasai> astearns: Leading up to F2F, we should have a set of things to test against
<fantasai> astearns: We absolutely need to fix the spec
<fantasai> astearns: I'm not sure that we have a fix that is good enough for all the edge cases yet
<TabAtkins> fantasai: We have a bunch of example scurrently, both working and not.
<fantasai> s/have/need to have/
<TabAtkins> I suspect we might need to address this semantically at the border-radius side with a keyword that does ellipse.
<TabAtkins> astearns: Can I ask Oriol to list out the cases to consider?
<TabAtkins> astearns: Summarizing them in the issue, and people can respond in the issue.
<TabAtkins> astearns: We'll tag this for f2f and come back to it
<fantasai> https://drafts.csswg.org/css-backgrounds-3/spread-radius
<TabAtkins> fantasai: We also have this which we can update

The padding edge (inner border) radius is the outer border radius minus the corresponding border thickness. In the case where this results in a negative value, the inner radius is zero.
But a spreading shadow goes beyond the border area, so its radius needs to be increased. And here there is a problem, because a sharp 0px should also produce a sharp shadow, but we should also have continuity.

I'm still not getting it. How does the sharp corner at 0px harm continuity? Continuity of what? And how important is it to fix the issue, vs. having a break in continuity or whatever? Sorry, I'm sure I'm still missing something, because I'm still not seeing how a sharp corner at r:0 is such a problem that it warrants distorting the shape so severely at every other radius.

If this is about animating a high-spread shadow between a small border-radius and a zero border-radius, and seeing a sharp corner suddenly appear at the end, then I think that is much, much, less bad than the distortions that are occurring from the remediation. This really seems like a cure that is much worse than the disease.

I vaguely recall a conversation about that sort of thing some time ago, but I didn't realize the effect of the fix would be so pronounced and bad. I'm sorry I wasn't more involved or thorough in gaining an understanding of the issue at the time and helping to work out a better solution.

If 0px has one behavior, but .000001px has a completely different behavior, that's discontinuous. (And we try to avoid that as much as possible, because it means that rounding precision of implementations becomes observable.)

I have created https://people.igalia.com/obrufau/testcases/shadow-radius/ to play.

If you take a look at the source, it's rather easy to add new testcases or algorithms.

I haven't found any algorithm that works great in all cases, but maybe I'm leaning towards "Percentage of same axis,
ceiling to keep ratio if possible", basically:

  1. Express the radii as percentages, and then resolve them against the sizes of the shadow (just like in #7103 (comment)).
  2. But then, for corners that get a different ratio of their components, try to increase one component, to recover the old ratio.
  3. However, don't violate the constraint avoided in https://drafts.csswg.org/css-backgrounds-3/#corner-overlap, i.e.
    • The X component of border-top-left-radius plus the X component of border-top-right-radius shouldn't exceed the width of the shadow.
    • The X component of border-bottom-left-radius plus the X component of border-bottom-right-radius shouldn't exceed the width of the shadow.
    • The Y component of border-top-left-radius plus the Y component of border-bottom-left-radius shouldn't exceed the height of the shadow.
    • The Y component of border-top-right-radius plus the Y component of border-bottom-right-radius shouldn't exceed the height of the shadow.

There can be multiple ways to do (3), in the demo above I distribute space proportionally according to the desired growth.

The CSS Working Group just discussed Border Radius Spread.

The full IRC log of that discussion <fantasai> Topic: Border Radius Spread
<TabAtkins> github: https://github.com//issues/7103
<TabAtkins> fantasai: I think we should have demos before we discuss this
<TabAtkins> Rossen_: oriol made those an hour ago
<TabAtkins> oriol: you can see a comparison of the different algos
<TabAtkins> oriol: i didn't find an algo that works perfectly for all
<TabAtkins> oriol: but maybe i'm leaning towards the last one in the demo
<bramus> https://people.igalia.com/obrufau/testcases/shadow-radius/
<TabAtkins> oriol: [demos]
<TabAtkins> oriol: first is no polyfill at all, browser does different things
<TabAtkins> oriol: this is chromium
<TabAtkins> oriol: here's current spec, where a ciruclar element is a non-ciruclar shadow'
<TabAtkins> oriol: here's another showing that same, with an ellipse
<TabAtkins> oriol: another algo is ot increase radius by spread
<TabAtkins> oriol: similar to firefox, but ff has a special case for 0 radius that isn't continuous
<TabAtkins> oriol: otherwise i'd say it's not a bad algo
<TabAtkins> oriol: there are some variant algos that take the border radius, express it as a percentage, then apply the same % to the shadow
<TabAtkins> oriol: good in some cases, but in this ellipse case it gets flat edges
<TabAtkins> oriol: using the maximum axis improves some cases but not all
<TabAtkins> oriol: ellipse is still flat
<TabAtkins> oriol: then my original proposal is each % is taken independently per axis
<Rossen_> q?
<TabAtkins> oriol: this works good i'd say, but has problem shown in this long roundrect,
<TabAtkins> oriol: where the shadow gets an elliptical corner when the original element was circular corner
<TabAtkins> oriol: so final variant, finds %s in each axis, then tries to, for each corner, if the radius has changed its aspect ratio relative to the element, it tries to increase one component to hit the same aspect-ratio
<TabAtkins> oriol: with the limitation that it prevents the radius from going to [???]
<TabAtkins> oriol: we have a restriction that the x component of top-left and top-right radius shouldn't exceed the width; the spec has an adjustment for that
<TabAtkins> oriol: so the constraint we apply here is we increase one component to restore the aspect ratio, without breaking this constraint
<TabAtkins> oriol: I think this is the best I could find
<TabAtkins> oriol: 0 radius gives square shadow, 1px gives mostly-square shadow
<TabAtkins> oriol: circle and ellipse are still pretty round
<lea> would be nice if one could input their own dimension/spread/radius values to see the result with the different algorithms
<TabAtkins> oriol: circular corners stay circular on the shadow even with long rects
<TabAtkins> oriol: in this example (a "cup" shape) the shadow looks like it becomes thinner, but it is the same arc in both the shape and the shadow
<TabAtkins> oriol: So this is the best idea I could find
<TabAtkins> oriol: if people want to play with this they can look at the source, it's a standalone page
<TabAtkins> oriol: [explains how to modify the demo]
<TabAtkins> Rossen_: fantastic, thansk fo rputting in the time for this
<lea> but yes, so great to have a demo! thanks oriol!
<TabAtkins> dbaron: I was looking at the cases where you said your preferred algo wasn't great
<TabAtkins> dbaron: I agree it's not that great, like the 3rd fro mthe end case
<TabAtkins> dbaron: it seems what the algo is doing wrong is producing too large a radius
<TabAtkins> dbaron: don't know why
<TabAtkins> dbaron: the problem there is the radius we're ending up with seems larger than it should be
<TabAtkins> oriol: both arcs are circular
<TabAtkins> oriol: inner element has border-radius of 39/39
<TabAtkins> oriol: shadow has border radius of 107/107
<TabAtkins> oriol: so x and y components are equal
<Rossen_> a/fo rputting/for putting/
<TabAtkins> oriol: almost no radius in the top left, and the shadow similarly has almost no radius there
<vmpstr> q+
<Rossen_> ack dbaron
<Rossen_> ack fantasai
<TabAtkins> fantasai: last time we discussed this i wanted to ask if it's posibble alternative...
<TabAtkins> fantasai: we have a formula in the spec, and the original proposal that's to use the same %s
<TabAtkins> fantasai: so, should we decide which formula to use based on whether the original radius was expressed in lengths or %s?
<TabAtkins> fantasai: so if you have oval that's 50%, the spread would also be 50%
<TabAtkins> fantasai: whereas if it was 200px/100px, we'd use the old formula
<vmpstr> q-
<TabAtkins> fantasai: So rather than just doing math on the absolute value, carry over the initial intent
<TabAtkins> fantasai: looking at these tests, the ones that look bad in the old formula are the ones where the length of the straight part of the border are 0 or close
<TabAtkins> fantasai: whereas the bad ones for %s are where the straight side is pretty long
<TabAtkins> fantasai: so we could have two different formulas, using one when the straight side is 0 or the other when large
<TabAtkins> fantasai: and between the two for some distance, such as radius = straight side to 0, we interpolate the formulas
<TabAtkins> oriol: re: the first thing, choosing the algo depending on whether the author specified length or %
<TabAtkins> oriol: I didn't consider that
<TabAtkins> oriol: issue is you can get the same shape in either way
<TabAtkins> oriol: seems strange to have two equivalent shapes for th eleemnet can produce different shadows
<Rossen_> q
<TabAtkins> fantasai: yeah but consider i have an element that's short, and the border-radius makes a semicircle, i might want that becuase i want a pillshaped box
<lea> q?
<lea> q+
<TabAtkins> fantasai: *or* it might be a consequence of this box being short, and i have a set of boxes with rounded corners and straight sides, so i'd prefer the shadow generally to have straight sides
<TabAtkins> fantasai: dunno if it's a great idea. authors might not think about their intent, and isntead just shove in numbers that get a result
<TabAtkins> oriol: could be a possibility
<TabAtkins> oriol: didn't consider it, to have it differentiate would have to [missed]
<TabAtkins> oriol: re: the other thing about combining algos
<TabAtkins> oriol: you were mentioning the old algo, what browsers used to do and firefox still does
<TabAtkins> fantasai: my understanding is chrome updated to current spec and realized there were problems
<TabAtkins> fantasai: the cases that work poorly are those where you want to get a round shape
<TabAtkins> fantasai: in those %s work great
<TabAtkins> fantasai: but they work bad for rounded corners on a rectangular shape, that's where current spec works well
<TabAtkins> fantasai: so using both formulas and interpolating might be a way to get good results in more cases
<TabAtkins> oriol: but then how do you do this interpolation?
<TabAtkins> fantasai: you look at the straight side, if it's 0 you use % formula. if it's longer than the radius, use the old formula. between those two, you do linear interp
<TabAtkins> lea: not super confident since this is the first time iv'e seen this
<TabAtkins> lea: but looking thru these algos
<TabAtkins> lea: as an author, anything with a % produces surprising results in some cases
<TabAtkins> lea: the one that produces th eleast surprise, to me at least, is to increase radius by spread
<TabAtkins> lea: except that it produces round from 0 radius
<TabAtkins> lea: this was chrome's behavior before they changed?
<TabAtkins> oriol: yes, firefox has a discontinuity at 0
<TabAtkins> lea: so maybe bc i was used to previous behavior, this makes sense to me
<TabAtkins> lea: is discontinuity acceptable?
<TabAtkins> oriol: another possibility is adding another initial value like "none" which keeps the shadow straight
<emilio> q+
<TabAtkins> fantasai: if you think about this in terms of being a drop shadow, instaed of a way to get a rounded corner outline, th eresults of "increase radius by spread" the results are clearly pretty unreasonable
<TabAtkins> fantasai: If you switch to current spec it looks a little more like an actual shadow
<TabAtkins> fantasai: the problem with current spec is the case with circles and ovals
<TabAtkins> lea: so you're proposing neither of these
<TabAtkins> fantasai: I'm proposing using current spec for first three cases, % of side in the next two, and some interp in the rest
<TabAtkins> fantasai: so formula i'm suggesting would be... 5th and 6th case would be current spec
<TabAtkins> fantasai: 7th case would get you a combo, one axis would use each
<TabAtkins> fantasai: second to last would be %
<TabAtkins> lea: I think it's hard to picture how it looks
<Rossen_> q
<TabAtkins> fantasai: yeah i can code it up
<TabAtkins> lea: I think we should definitely avoid that if you ahve a fully curved shape, the sahdow should have any straight edges
<TabAtkins> fantasai: right, that's the issue we're trying to solve
<TabAtkins> emilio: the othe rplace where this pops up is outlines
<TabAtkins> emilio: do you propose to change both?
<TabAtkins> fantasai: both seems reasonable
<Rossen_> ack emilio
<TabAtkins> emilio: you're right that firefox's behavior for second case doesn't make sense for shadows, but i think it makes sense for an outline
<TabAtkins> TabAtkins: right i think outline is like a border
<TabAtkins> emilio: so right now outline works like a border
<TabAtkins> fantasai: so there's a discontinuity at zero
<TabAtkins> Rossen_: spec has a cubic interpolation
<TabAtkins> fantasai: been in spec for a long time, chrome just implemented it and discovered problems
<TabAtkins> fantasai: so i think using whateer we end up for spread readius would probably look good, but i think having continuous behavior generally is also pretty good
<TabAtkins> fantasai: so might want to use same for both
<Rossen_> ack dbaron
<TabAtkins> dbaron: first to emilio, i'm in favor of having a single algo in CSS for expanding a rounded rect
<TabAtkins> dbaron: that is the direction i was pushing when i code-reviewed the patch that led to this in the first place
<emilio> +1 to have a single algorithm
<TabAtkins> dbaron: I'd like us to be consistent in inflation
<TabAtkins> dbaron: the othe rpoint about inflating rounded rects, is you always start fro mthe border edge and do a single inflation
<TabAtkins> dbaron: you never do it from another algo
<TabAtkins> dbaron: because inflate(10)+inflate(10) might be different than inflate(20), and certainly inflate(10)+inflate(-10) is definitely not the identity
<TabAtkins> dbaron: can avoid problems by starting from the border edge always
<fantasai> DBaron Principle of Rounded Rectangle Flation
<TabAtkins> dbaron: one reason to keep these consistent is people might have both at the same time
<TabAtkins> dbaron: if outline and shadow use different algos, that'll look bad
<TabAtkins> dbaron: i think we'll be okay with a single algo
<Rossen_> q
<TabAtkins> dbaron: looking thru oriol's demos, i think the best results generally come from option 2 or 3
<TabAtkins> dbaron: while i understand the goal was to find the right compromise between 2 and 3, i think the % options din't really work out
<TabAtkins> dbaron: i think the goal should be to find the right way to compromise between those two
<TabAtkins> dbaron: there's also the 2-variant that is what browsers did before, which has the 0 discontinuity
<TabAtkins> dbaron: so maybe a comp between 2v and 3
<fantasai> TabAtkins: next steps, code up fantasai's idea
<fantasai> TabAtkins: anyone with other ideas, put them in the demo as well
<fantasai> TabAtkins: revisit in 2 weeks to decide?
<TabAtkins> Rossen_: again, thanks to oriol for the demo
<emilio> oriol++

If 0px has one behavior, but .000001px has a completely different behavior, that's discontinuous. (And we try to avoid that as much as possible, because it means that rounding precision of implementations becomes observable.)

I still think that discontinuity is much, much less objectionable than having flat edges on an ellipse or circle, or of having the spread amount be different in one dimension than the other. To avoid observable differences in implementations, I suggest we just pick an arbitrary length at which the radius of that length or smaller is considered zero. Maybe something like border-radius of .25px or less is treated the same as border-radius: 0. That way, this edge case of extremely small radii do not end up distorting all the larger (1px +) border radii.

@Loirooriol , would you be able to apply the following patch to your set of examples, which adds 3 additional options? One of them is the old (discontinuous) behavior which is still what non-Chromium engines implement (along with old Chromium). The other two are an additional capping method that I came up with (with 2 variants).

Patch (expand for details)
--- shadow-radius.html	2022-08-04 15:53:41.000000000 -0400
+++ shadow-radius.html	2022-08-05 09:33:16.000000000 -0400
@@ -30,16 +30,20 @@
     <input type="radio" name="algorithm" value="do-not-polyfill" checked>
     Do not polyfill
   </label>
   <label>
     <input type="radio" name="algorithm" value="increase-by-spread">
     Increase radius by spread
   </label>
   <label>
+    <input type="radio" name="algorithm" value="old-spec">
+    Old spec (discontinuous)
+  </label>
+  <label>
     <input type="radio" name="algorithm" value="current-spec">
     Current spec
   </label>
   <label>
     <input type="radio" name="algorithm" value="percentage-min-axis">
     Percentage of min axis
   </label>
   <label>
@@ -49,16 +53,24 @@
   <label>
     <input type="radio" name="algorithm" value="percentage-same-axis">
     Percentage of same axis
   </label>
   <label>
     <input type="radio" name="algorithm" value="percentage-same-axis-ratio">
     Percentage of same axis,<br>ceiling to keep ratio if possible
   </label>
+  <label>
+    <input type="radio" name="algorithm" value="linear-edge-portion">
+    Rounded portion of edges (linear)
+  </label>
+  <label>
+    <input type="radio" name="algorithm" value="cubic-edge-portion">
+    Rounded portion of edges (cubic)
+  </label>
 </form>
 <div id="output"></div>
 <script>
 const output = document.getElementById("output");
 const {algorithm} = document.forms[0].elements;
 const testCases = [
   {width: 50, height: 50, spread: 50, borderRadius: "0px"},
   {width: 50, height: 50, spread: 50, borderRadius: "1px"},
@@ -138,16 +150,30 @@
       r = {
         topLeft: radii.topLeft.map(map),
         topRight: radii.topRight.map(map),
         bottomLeft: radii.bottomLeft.map(map),
         bottomRight: radii.bottomRight.map(map),
       };
       break;
     }
+    case "old-spec": {
+      const map = (value) => {
+        if (value == 0)
+          return 0;
+        return value + testCase.spread;
+      }
+      r = {
+        topLeft: radii.topLeft.map(map),
+        topRight: radii.topRight.map(map),
+        bottomLeft: radii.bottomLeft.map(map),
+        bottomRight: radii.bottomRight.map(map),
+      };
+      break;
+    }
     case "current-spec": {
       const map = (value) => {
         if (value >= testCase.spread) {
           return value + testCase.spread;
         }
         let r = value / testCase.spread;
         return value + testCase.spread * (1 + (r - 1)**3);
       }
@@ -230,16 +256,48 @@
         }
       }
       adjust("topLeft", "topRight", 0);
       adjust("bottomLeft", "bottomRight", 0);
       adjust("topLeft", "bottomLeft", 1);
       adjust("topRight", "bottomRight", 1);
       break;
     }
+    case "linear-edge-portion":
+    case "cubic-edge-portion": {
+      // The portion of each edge that is rounded
+      const portion = {
+        top:    (radii.topLeft[0] + radii.topRight[0]) / testCase.width,
+        right:  (radii.topRight[1] + radii.bottomRight[1]) / testCase.height,
+        bottom: (radii.bottomLeft[0] + radii.bottomRight[0]) / testCase.width,
+        left:   (radii.topLeft[1] + radii.bottomLeft[1]) / testCase.height,
+      };
+      const map = (value, cap) => {
+        if (value >= testCase.spread) {
+          return value + testCase.spread;
+        }
+        switch (algorithm.value) {
+          case "linear-edge-portion": {
+            let r = value / testCase.spread;
+            return value + testCase.spread * Math.max(1 + (r - 1)**3, cap);
+          }
+          case "cubic-edge-portion": {
+            let r = Math.max(value / testCase.spread, cap);
+            return value + testCase.spread * (1 + (r - 1)**3);
+          }
+        }
+      }
+      r = {
+        topLeft: radii.topLeft.map((r) => map(r, Math.max(portion.top, portion.left))),
+        topRight: radii.topRight.map((r) => map(r, Math.max(portion.top, portion.right))),
+        bottomLeft: radii.bottomLeft.map((r) => map(r, Math.max(portion.bottom, portion.left))),
+        bottomRight: radii.bottomRight.map((r) => map(r, Math.max(portion.bottom, portion.right))),
+      };
+      break;
+    }
     default: {
       throw "Not implemented: " + algorithm.value;
       break;
     }
   }
   return `${r.topLeft[0]}px ${r.topRight[0]}px ${r.bottomRight[0]}px ${r.bottomLeft[0]}px / ${r.topLeft[1]}px ${r.topRight[1]}px ${r.bottomRight[1]}px ${r.bottomLeft[1]}px`;
 }
 show();

Also, for the record, the spec change that introduced this cubic formula was b7fc21a in November 2013.

Done. I guess I should look into github pages or something so that people can suggest changes easily.

Maybe something like border-radius of .25px or less is treated the same as border-radius: 0. That way, this edge case of extremely small radii do not end up distorting all the larger (1px +) border radii.

This shifts the discontinuity; it doesn't do anything to remove it. When we do things similar to this, we are generally hiding an asymptote by saying that everything smaller than X is treated as X (like, say, values under 1px are treated as being 1px); that retains continuity and avoids a blowup. This is instead trying to deal with two entirely different rendering strategies being desirable, and an unfortunate legacy that mixes the two into the same value range.

The CSS Working Group just discussed box-shadow shape.

The full IRC log of that discussion <TabAtkins> Topic: box-shadow shape
<TabAtkins> github: https://github.com//issues/7103
<TabAtkins> dbaron: a few weeks ago oriol prepare a bunch of demos for this problem
<TabAtkins> dbaron: underlying issue is that in 2013 we changed the spec so expansion of rounded borders, which happens primarily for box-shadow spread, was not discontinuous
<TabAtkins> dbaron: prior it was discontinuous from 0 to non-zero border-radius
<TabAtkins> dbaron: we introduced this cubic formula
<TabAtkins> dbaron: So normally a 5px border-radius and 3px of spread means an 8px spread corner
<TabAtkins> dbaron: so they're concentric
<TabAtkins> dbaron: nobody shipped it until earlier this year
<TabAtkins> dbaron: Chromium got bug reports now about circles expanded to non-circles
<TabAtkins> dbaron: So we want circles to stay circles
<TabAtkins> dbaron: Oriol had proposals, group was unhappy with all of them
<TabAtkins> dbaron: since then i cam eup with two additional proposals, fantasai had a third i attempted to put into oriol's demo
<TabAtkins> dbaron: so we can look at these additional options
<TabAtkins> dbaron: so first option is the existing formula
<TabAtkins> astearns: skip to demos, we can go back to math
<fantasai> -> https://lists.w3.org/Archives/Public/www-archive/2022Sep/att-0012/shadow-radius.html
<TabAtkins> dbaron: this third example is a circle, that's important, and there are some other pretty round cases
<TabAtkins> dbaron: this is the spec currently, you'll see the circle is no longer circle at all, other round examples have flattened ends
<TabAtkins> dbaron: my first option is based on the ratio fo the rounded part of an edge to the total length of the edge, using tat as a cap on the cubic formula
<TabAtkins> dbaron: one puts the cap on before, one after the cubing, so one is linear and the other is cubic
<TabAtkins> fantasai: in the very last, thre's a sharp corner, but it gets quite rounded in both of yours
<TabAtkins> fantasai: but your other squares arent' particularly rounded
<TabAtkins> dbaron: elika's is capping based on --- using oriol's "% of max axis" but not all the time, switches beetween it and current spec
<TabAtkins> fantasai: EAch side has a straight and a curved piece. if it's all curve, it uses % formula, if it's at least the length of the box side it uses the spec formula, and between those it interpolates
<astearns> q?
<TabAtkins> dbaron: the other things that's different is i did the capping based on the ratio of rounded vs total side length, elika did ratio of rounded to straight part of the side
<TabAtkins> dbaron: not sure which is better
<TabAtkins> dbaron: but werid thing about elika's is it requires a max because it hits the cap in the middle rather than the end
<TabAtkins> astearns: the old spec is super round for the square with 1px round corner
<TabAtkins> fantasai: yeah tha's why we change the spec
<TabAtkins> astearns: wonder if we'll get people wanting that, tho
<TabAtkins> fantasai: depends on what you're trying to do
<TabAtkins> fantasai: goal was to get something continuous
<TabAtkins> fantasai: for squarish things i think current spec is better, but for circlish it's broken
<TabAtkins> astearns: sounds like we need some more options in the dmo?

@Loirooriol is it ok with you if I commit the demo to the csswg-drafts repository? (I'll at least commit your original version followed by two changesets representing my changes, and then make some more additions. Or if you have further history that you want to commit, you're welcome to commit that. It should probably end up in css-backgrounds-3/.)

@dbaron Yes, that's fine. I don't have further history.

I fixed the bug mentioned above (which was misplaced )s in the last change I made before our discussion) and added the additional variants mentioned above.

I also added an additional testcase that I think @fantasai's variant performs poorly on... at least assuming I'm understanding that variant correctly. (The variant is a 25px radius on a narrow rectangle whose short dimension is 50px.)

@dbaron For clarification, if using the formula in #7103 (comment) , if the short side of the rectangle is 50px and it has a 25px border radius, then its spread radii in the short axis should be 50% and the spread radii in the long axis should be as dictated by the current spec formula.

(Same case except the short side of the rectangle being 75px instead of 50px, the spread radii would be as dictated by the current spec formula in both axes.)

FYI @fantasai and I just committed a trimmed down set of algorithms with a correct implementation of Elika's proposal, and made the testcases editable: https://drafts.csswg.org/css-backgrounds-3/radius-expansion.html

return Math.min(ret, straight + value + testCase.spread);

I'm not sure about straight. Should it be just Math.min(ret, value + testCase.spread), i.e. clamp by "increase-by-spread"?

Some difference can be seen with {"width":500,"height":60,"spread":30,"borderRadius":"1px 1px 49px 49px"}.

Not sure if this is the right place (or subject) to talk about this, but I'll give it a try.

I'm seeing an artifact with a drop shadow for a box with a big blur value with 0 spread:

2022-10-21-194039_413x384_scrot

It looks like it's not radiating from the corners, just spreading/blurring in a radial manner independent of the shape (0 border radius). Is this expected?

I'd appreciate if you excuse and point me in the right direction if here is not the right place to post about this. 🙏

@lobo-tuerto That would be a separate issue, this one's just about the spread shape. Expected behavior for that btw is defined by Gaussian blur, see https://www.w3.org/TR/css-backgrounds-3/#shadow-blur

@fantasai Thanks for the information I appreciate it, and will take a look.

Elika's option doesn't round at all with {"width":500,"height":50,"spread":30,"borderRadius":"0px 0px 50px 50px"}

And I'm not convinced about {"width":500,"height":70,"spread":100,"borderRadius":"0px 0px 50px 50px"}:

With the change from #7103 (comment) it seems better

But still elliptical, should it be circular instead?

I have updated https://drafts.csswg.org/css-backgrounds-3/radius-expansion.html with more testcases and a new proposal. To summarize the context of this issue:

  • When reducing a border radius, we just subtract the distance (and clamp at zero)
  • So the obvious thing to do when expanding a radius, would be to add the distance
  • But that's problematic, because non-rounded corners (radius of 0) should stay non-rounded.
  • So the current spec adds the distance multiplied by an adjustment factor. This factor is 1 if the radius is greater than the spread distance, it's zero if the radius is 0, and it's continuous between these cases.
  • The problem is that, if the original shape is a full ellipsis/circle, then the expanded shape may stop being a full ellipsis/circle.

So, my idea is: in the adjustment factor, instead of only considering the ratio between the radius and the spread distance, also consider the ratio of the element sizes covered by the radius. If the horizontal radius is at least 50% of the width of the element, or the vertical radius is at least 50% of the height, then we want the factor to be 1, even if the spread distance is much bigger than the radius.

BTW, this approach guarantees that, if a corner is circular (same horizontal and vertical radii), the expanded corner will stay circular instead of becoming elliptical.

This is the code in the demo:

for (let corner in radii) {
  let coverage = Math.max(
    2 * radii[corner][0] / testCase.width,
    2 * radii[corner][1] / testCase.height,
  ) || 0;
  r[corner] = radii[corner].map(value => {
    if (value >= testCase.spread || coverage >= 1) {
      return value + testCase.spread;
    }
    let r = (1 - value / testCase.spread) * (1 - coverage);
    return value + testCase.spread * (1 - r**3);
  });
}

I think it produces better results than Elika's proposal for {"width":500,"height":60,"spread":30,"borderRadius":"20px 20px 40px 40px"} and {"width":250,"height":35,"spread":50,"borderRadius":"0px 0px 25px 25px"}.

Elika's option doesn't round at all with {"width":500,"height":50,"spread":30,"borderRadius":"0px 0px 50px 50px"}

It should? It should result in a bottom left radius of y=50% x=80px ...

The demo produces 0px 0px 80px 80px / NaNpx NaNpx 80px 80px because of straight / value both being 0. I guess the ratio just needs to turn NaN into 0 or 1, probably not much important.

I'd like to make new proposals consisting of two weakly related parts:

  • a spread-adjustment formula based on geometric means;
  • two variants of formulae for better appoximating the actual shape of the spread of an elliptic border.

Here's the demo based on Loirooriol's with two more test cases added:

https://yarusome.github.io/box-shadow-proposal/


Summary for the spread-adjustment formula:

If the spread is less than the radius, the spread is not adjusted. Otherwise the adjusted spread satisfies:

radius + adjusted_spread = C * (radius + spread) ^ coverage * radius ^ (1 - coverage),

where

coverage = min(radius * 2 / side_length, 1),

and the constant C = 2 ^ (1 - coverage) guarantees that the formula is continuous at radius = spread.


Summary for the elliptic correction formulae:

For an elliptic border, both variants ensure that the approximate spread passes through a particular point on the actual spread. (A side effect is that circular borders automatically produce circular spreads.) But to fix the radii of the approximate spread:

  • The first variant makes a requirement on the aspect ratio of the approximate spread.
  • The second variant requires that the longer axis of the approximate spread “take as much length as possible.”

Personally I find the second elliptic correction formula aesthetically better.


If the WG is interested, I could also write a mathematical note explaining the derivations of the two correction formulae.

The CSS Working Group just discussed border-radius adjustment formula.

The full IRC log of that discussion <fantasai> Topic: border-radius adjustment formula
<fantasai> github: https://github.com//issues/7103
<fantasai> fantasai: not prepared to present; suggest taking at F2F where we have better visuals

The CSS Working Group just discussed [css-backgrounds] The shape of box-shadow should be a circle for a box with border-radius:50% and big spread.

The full IRC log of that discussion <emilio> oriol: IIRC we had a webpage where you could test the various algorithms
<fantasai> -> https://yarusome.github.io/box-shadow-proposal/
<oriol> https://drafts.csswg.org/css-backgrounds-3/radius-expansion.html
<emilio> oriol: summary, when going inwards with border-radius you can just reduce the amount of radius
<emilio> ... but when going outwards if we just add the spread-distance then we break border-radius: 0
<emilio> ... browsers checked if border-radius is zero, but that's not great because it's not continuous
<emilio> ... spec tries to use a cubic formula that keeps being zero if it was zero but that has an issue but that deforms the inner shape with circles etc
<emilio> ... we had various ideas for ways to try to approach these
<emilio> ... one of the ideas was to try to keep percentages
<emilio> ... so we'd express the border radius as percentages and apply it to the inner and outer box
<emilio> ... but that doesn't work if the aspect ratio of the inner and outer didn't match
<astearns> two more options added from the latest comment: https://yarusome.github.io/box-shadow-proposal/
<emilio> ... then fantasai proposed an interpolation between the two
<emilio> ... I found some cases when this was not behaving that welll
<emilio> ... I proposed a modification to the current spec but with the addition of another factor
<astearns> s/two more/three more
<emilio> ... that also considers the ratio of the element size that's covered by the radius
<emilio> ... so if the horiz radius is >50% of the width of the element then we want this factor to be 1
<emilio> ... even if the spread is much bigger than the radius
<emilio> ... you can see the test-case, it seems it was better in some cases
<emilio> ... then someone else (yarusome) proposed some other ideas
<Rossen_> q?
<emilio> ... not sure if we should try with a whiteboard or some brainstorming to try to come up with a solution
<Rossen_> ack oriol
<emilio> ... short version is take a look at the test-case
<emilio> ... I don't think we have the right answer yet
<emilio> ... someone added more funny cases
<fantasai> scribenick: fantasai