csswg-drafts
csswg-drafts copied to clipboard
[css-backgrounds] The shape of box-shadow should be a circle for a box with border-radius:50% and big spread
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.

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:
- Calculate the radius of the border corners in percentages of the border box size
- Set the radius of the shadow corners to be the same percentages, but now resolved with the shadow size.
Or in other words,
- Resolve the radius of the border corners as a pair of lengths
x / y. - Let the sizes of the border box be
b_xandb_y - Let the sizes of the shadow box be
s_xands_y - 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.

@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:

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" :smile:) 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 https://github.com/w3c/csswg-drafts/issues/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/w3c/csswg-drafts/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

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/w3c/csswg-drafts/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/w3c/csswg-drafts/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/w3c/csswg-drafts/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:
- Express the radii as percentages, and then resolve them against the sizes of the shadow (just like in https://github.com/w3c/csswg-drafts/issues/7103#issuecomment-1135036126).
- But then, for corners that get a different ratio of their components, try to increase one component, to recover the old ratio.
- 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-radiusplus the X component ofborder-top-right-radiusshouldn't exceed the width of the shadow. - The X component of
border-bottom-left-radiusplus the X component ofborder-bottom-right-radiusshouldn't exceed the width of the shadow. - The Y component of
border-top-left-radiusplus the Y component ofborder-bottom-left-radiusshouldn't exceed the height of the shadow. - The Y component of
border-top-right-radiusplus the Y component ofborder-bottom-right-radiusshouldn't exceed the height of the shadow.
- The X component of
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/w3c/csswg-drafts/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 b7fc21a672cc8b189c3117f7d4d5c16c25ca6117 in November 2013.