w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.47k stars 658 forks source link

[css-shapes-2][css-borders-4] Add a way to change an element's shape #6997

Open SebastianZ opened 2 years ago

SebastianZ commented 2 years ago

@LeaVerou mentioned an element-shape property in https://github.com/w3c/csswg-drafts/issues/6980#issuecomment-1020301916 as a way to influence the shape of an element. The idea behind that property is to change the shape of the element so that border, shadows, etc. follow that shape.

So far we have the shape-outside property which influences the float area. I suggest to extend this property to be able to specify that it not only affects the float area but also all the other boxes created for the element.

Sebastian

jsnkuhn commented 2 years ago

this issue about trying to get borders etc to play nice with clip-path seems related: #5881

bradkemper commented 2 years ago

I think we should either extend shape-outside or clip-path or both. Probably clip-path would make more sense, since it is already creating a background shape, and we want it to also create a border shape. Whether it is floated via shape-outside is related, but maybe less relevant.

I was thinking it could be a keyword we add to those properties, as in clip-path: circle(50%) border-shape. When that keyword is added, it would mean that you would get the border-top value and use it to stroke the inside of the shape and making the padding box that much smaller if it has box-sizing: border-box, or stroking the outside of the shape if it has box-sizing: content-box. The stroke would affect layout, much like a regular border. The border would not be clipped by its own clip-path. The normal rectangular border would not be added. We would in effect be creating a shaped border-box that is used for box-shadow and filters. Those filters and box-shadows would not be applied to the normal rectangular border-box, just to the stroked shape.

bradkemper commented 2 years ago

Also, add clip-path as a keyword value of shape-outside, so that shapes only have to be defined once, in the clip-path property, and used to also define the path of shape-outside.

faceless2 commented 2 years ago

Big +1 to Brad's comments. We have shape-outside and clip-path already, they should be reused rather than adding another definition of the shape-of-a box. The clip-path value to shape-outside is also a good (and probably overdue) idea.

jsnkuhn commented 2 years ago

with the reuse of a clip-path like syntax in mind clip-path already has an inset() option right now that allows for a round option similar to border radius. So it seems like things are already primed to be extended for corner-shape

clip-path: inset(20px round 10px)

does fell a bit strange hiding the corner-shape-ing just inside inset() though.

wonder if something like corners(angle round round 50%); could be used to make the corners thing less of an after thought here.

jsnkuhn commented 2 years ago

would it theoretically in anyway be feasible to get border-image to follow an element-shape path? Like down the left side of this text box?

border-image-shape

SebastianZ commented 2 years ago

with the reuse of a clip-path like syntax in mind clip-path already has an inset() option right now that allows for a round option similar to border radius. So it seems like things are already primed to be extended for corner-shape

Any solution found here needs to define how to deal with border-radius and corner-shape.

If we end up with extending clip-path for that, we might close this issue in favor of #5881. But we'll see.

Sebastian

SebastianZ commented 2 years ago

Btw. just referencing #2180 in regard of the clip-path value for shape-outside.

Sebastian

jsnkuhn commented 2 years ago

I don't really think giving clip-path the ability to have borders/box-shadows etc is the right way to go here. clip-path is already doing what it's intended to do i.e clip a path. It makes more sense to for there to be a separate property to deal with the creation of an element shape.

There are plenty of existing use cases of having a rectangular shape that is revealed with a clip-path animation. No reason folks wouldn't want to do the same thing with non-rectangular shapes. If you are already using clip-path for the elements shape how would it be possible to reveal the element with a clip-path animation?

GIF

https://codepen.io/jsnkuhn/pen/BaryjEV?editors=1100

jsnkuhn commented 2 years ago

Ran across this page with lots of interestingly shaped elements: https://atlus.com/persona5/home.html

image

smfr commented 2 months ago

shape-outside is about wrapping content around an element. clip-path is about clipping, and does not affect border drawing.

I think the solution here is a property that allows the author to specify a shape that affects the painted border, like border-shape or element-shape. The author might want to specify a single path which is stroked to generate the border, or two paths which form the inner and outer edge of the painted border (i.e. a path with a hole), in which case the space between them is filled with the border color(s).

noamr commented 2 months ago

I think this is a duplicate of #5881 in some ways. We should perhaps discuss them as one thing?

smfr commented 1 month ago

Here's a very rough proposal for a border-shape syntax, with the intent of having border-shape encompass corner-shape and the desire to have user-defined element shapes.

<corner-shape> = [round | bevel | scoop | notch]{1,4}
<border-line-style> = dotted | dashed | solid
<border-radius> = <length-percentage [0,∞]>{1,4} [ / <length-percentage [0,∞]>{1,4} ]?

border-shape: (<corner-shape> <border-radius>?)
              || (<basic-shape> <line-width> <border-line-style>? <geometry-box>?)
              || (<basic-shape> <geometry-box>? <basic-shape> <geometry-box>?)

If the <corner-shape> version is specified, this can be used to specify the corner treatment for each corner, and optionally border radii that override border-radius values. The appearance of corner shapes is influenced by border widths, as with border-radius currently (note this might be non-trivial for some corner shapes).

If the single <basic-shape> variant is specified, this provides a path, with the given stroke width, which is rendered instead of the border[1]. The path is resolved relative to the given <geometry-box>, defaulting to border-box. Colors are taken from the border-color property (different colors on each side are rendered with corner joins, as they are now with border-radius). The border is rendered by stroking the path, centered on the path (insetting an arbitrary path involves non-trivial compute, and masking to get an inset path doesn't work for paths with loops). Dotted and dashed paths are supported. (Double borders could also be supported by masking and rendering the path a second time for the center slot with clearing.) A future property could allow control over the dash origin.

If the double <basic-shape> variant is specified, the first shape is used as the outside of the border shape, and the second shape as the inside. The border fill is painted in the space between these two paths. Each path is resolved relative to the given <geometry-box>.

border-clip: border-area fill the border shape.

Box shadows follow the outer and inner border shapes as appropriate.

border-shape does not affect the box geometry, which is still computed using the widths specified via border-width. Areas of the shape that project outside the border-box are considered as ink overflow. Areas of the inner shape that project outside of the border-box will be considered part of the background area, so filled with backgrounds (we probably need a new value for background-clip to allow for this).

border-shape does not affect the flow of content inside the box.

The clipping of content behaves as it does for border-radius: the inside shape of the border clips content, if overflow is non-visible.

The shape variants of border-shape have no effect in cases where it's necessary to clip to the content box (replaced elements?). Currently border-radius does affect content box clipping in some cases, but it's not possible to easily inset arbitrary shapes to compute content shape clipping.

[1] Maybe the rectangular border also renders, and the author has to set the color to transparent to hide it? The shape would then be a way to just extend the background.

Here's a screenshot for some in-progress experiments in WebKit:

border-shape which is a rendering of

            padding: 40px;
            overflow: hidden;
            border: 60px solid transparent;
            box-shadow: 0 0 20px black, 0 0 20px black inset;
            border-shape:
                shape(from 0% 0%,
                    hline to 100%,
                    vline to 75%,
                    smooth by -8.295% 0% using -3.7% -2.2%,
                    smooth by -8.5% 12.8% using -4.1% 10.7%,
                    smooth by -7.9% -3.2% using -3.7% -5.2%,
                    smooth by -7.3% 9.3% using -2.7% 7.1%,
                    smooth by -9.0% 0% using -4.1% -2.3%,
                    smooth to 0.82% 100% using 0.82% 100%,
                    close) border-box,
                shape(from 10% 10%,
                    hline to 90%,
                    vline to 64.621%,
                    smooth by -8.295% 0% using -3.7% -2.2%,
                    smooth by -8.5% 12.8% using -4.1% 10.7%,
                    smooth by -7.9% -3.2% using -3.7% -5.2%,
                    smooth by -7.3% 9.3% using -2.7% 7.1%,
                    smooth by -9.0% 0% using -4.1% -2.3%,
                    smooth to 10% 90% using 10% 90%,
                    close) border-box;            
            background-image: linear-gradient(to bottom right, orange, blue, green), none;
            background-origin: border-box;
            background-clip: border-area, border-box;
noamr commented 1 month ago

Here's a very rough proposal for a border-shape syntax, with the intent of having border-shape encompass corner-shape and the desire to have user-defined element shapes.

I'm sure other people would have more to say about the specifics, but my initial reaction is "let's do it!"

smfr commented 1 month ago

[1] Maybe the rectangular border also renders, and the author has to set the color to transparent to hide it? The shape would then be a way to just extend the background.

On second thoughts, since the stroke-based and area-based shapes take their colors from border-color, the normal rectangular borders should not be rendered.

noamr commented 1 month ago

@smfr what does the two-shape syntax add on top of using a single shape with close and evenodd?

.shapre {
            padding: 40px;
            overflow: hidden;
            border: 60px solid transparent;
            box-shadow: 0 0 20px black, 0 0 20px black inset;
            border-shape:
                shape(from 0% 0% evenodd,
                    hline to 100%,
                    vline to 75%,
                    smooth by -8.295% 0% using -3.7% -2.2%,
                    smooth by -8.5% 12.8% using -4.1% 10.7%,
                    smooth by -7.9% -3.2% using -3.7% -5.2%,
                    smooth by -7.3% 9.3% using -2.7% 7.1%,
                    smooth by -9.0% 0% using -4.1% -2.3%,
                    smooth to 0.82% 100% using 0.82% 100%,
                    close,
                    move to 10% 10%,
                    hline to 90%,
                    vline to 64.621%,
                    smooth by -8.295% 0% using -3.7% -2.2%,
                    smooth by -8.5% 12.8% using -4.1% 10.7%,
                    smooth by -7.9% -3.2% using -3.7% -5.2%,
                    smooth by -7.3% 9.3% using -2.7% 7.1%,
                    smooth by -9.0% 0% using -4.1% -2.3%,
                    smooth to 10% 90% using 10% 90%,
                    close) border-box;            
            background-image: linear-gradient(to bottom right, orange, blue, green), none;
            background-origin: border-box;
            background-clip: border-area, border-box;
            }
smfr commented 1 month ago

The single-shape syntax is a request to stroke the given path (which may contain multiple sub-paths). The two-stroke syntax is a request to fill the area between the paths. It's possible we should switch between these behaviors with a keyword, but then you might have an author providing a single-path path and requesting a fill, which would be an error (I don't think we ever want the border to act as a fill over the entire element).

noamr commented 1 month ago

The single-shape syntax is a request to stroke the given path (which may contain multiple sub-paths). The two-stroke syntax is a request to fill the area between the paths. It's possible we should switch between these behaviors with a keyword, but then you might have an author providing a single-path path and requesting a fill, which would be an error (I don't think we ever want the border to act as a fill over the entire element).

Maybe I'm still getting this wrong, but is this equivalent to using a different winding-rule for fill and stroke? evenodd for fill and nonzero for stroke?

smfr commented 1 month ago

I don't think so. Authors might want to specify different winding rules for the two-path form. It's about wanting the path to be stroked, vs. wanting the path to be filled. But maybe we should also have the two-path form allow stroking as well?

yisibl commented 1 month ago

@smfr For the example above, I'm curious, how would adding border-bottom-right-radius render?

jsnkuhn commented 1 month ago

Just for confirmation we are changing the entire shape of the element here correct?

If so shouldn't the property name be "element-shape" in seat of "border-shape"? The border is only that shape because it traces/strokes the element's shape. It also seems that a "border"/stroke would not be required and therefore calling it "border-shape" could be confusing. A border-less "border-shape" just seems odd to me. If you have a border-less element-shape the name still makes sense.

noamr commented 1 month ago

Just for confirmation we are changing the entire shape of the element here correct?

If so shouldn't the property name be "element-shape" in seat of "border-shape"? The border is only that shape because it traces/strokes the element's shape. It also seems that a "border"/stroke would not be required and therefore calling it "border-shape" could be confusing. A border-less "border-shape" just seems odd to me. If you have a border-less element-shape the name still makes sense.

It's consistent with border-radius, and has very similar semantics.

jsnkuhn commented 1 month ago

Agreed that "border-shape" would have the same semantics of border-radius. But the point is that those semantics are confusing. We've 15 years of precedent with blog posts titled something like "border-radius/rounded corners" because people did not know what border-radius was on it's own. This doesn't feel to me like something to lean into.

smfr commented 1 month ago

I feel like border-shape is more appropriate. It doesn't affect the box geometry (other than possibly adding ink overflow), so does not affect layout. It picks up the border colors, and background-clip: border-area fills the shape, so it is really best thought of as a customized border.

jsnkuhn commented 1 month ago

border-radius has a long standing issue with color "bleed" (for lack of a better term). When a parent and child element have the same border-radius the corners don't line up exactly and this potentially causes the parents background color to bleed through. Are these shaping properties all likely to have this same issue? If so is there anything that can done about this in the spec?

Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=664154 Chrome bug: https://issues.chromium.org/issues/41361515 Safari bug: https://bugs.webkit.org/show_bug.cgi?id=62639

schenney-chromium commented 1 month ago

I don't think there's anything that can be done at the spec level regarding bleeding around borders or backgrounds or between adjoining elements. I think it's already clear to almost everyone that things should not bleed and I can't think of a spec change that would make it easier to avoid bleeding.

The bugs are generally due to implementation of both the rendering engine and the underlying graphics systems. I'm beyond counting how many days of my life I have spent trying to fix color bleeding problems related to backgrounds and borders. At a meta level the problems are very hard to fix because a general solution requires globally knowledge of how everything should align, but what we have in practice is a logical tree.

LeaVerou commented 1 month ago

Let’s please not use border-radius as design precedent, it’s one of the recognized mistakes in the design of CSS: https://wiki.csswg.org/ideas/mistakes

noamr commented 1 month ago

Let’s please not use border-radius as design precedent, it’s one of the recognized mistakes in the design of CSS: https://wiki.csswg.org/ideas/mistakes

The link said that we should have named it corner-radius. That makes sense because the radius itself applies to the corners and not to the whole border, but I don't see how it's a "mistake in design", or something that applies here? The path in this case does affect the whole border and not just the corners.

LeaVerou commented 1 month ago

Let’s please not use border-radius as design precedent, it’s one of the recognized mistakes in the design of CSS: wiki.csswg.org/ideas/mistakes

The link said that we should have named it corner-radius. That makes sense because the radius itself applies to the corners and not to the whole border, but I don't see how it's a "mistake in design", or something that applies here? The path in this case does affect the whole border and not just the corners.

That is not the reason it's a mistake. It’s a mistake because it affects the element shape whether there is a border or not. Naming it border-radius was unduly influenced by its specification details (the fact that it rounds the border box) but as far as authors are concerned, that's almost an implementation detail.

smfr commented 1 month ago

I think you're suggesting that element-shape would be a better name, because even when a border is not present, the property would affect the shape of the background area, and how contents are clipped? That makes sense, but then the values of the property that are really about embellishing the border (e.g. the two-shape form) feel a bit odd as values for element-shape.

schenney-chromium commented 1 month ago

I would expect something like "element-shape" to affect layout, in the sense that other elements would somehow fit around the shape. Here the element is still a rectangle, but it has a complex border around it that optionally draws and always clips the background.

The compromise between @LeaVerou and @smfr and myself may be "background-shape".

We also need to consider potential interactions with shadows, aka https://github.com/w3c/csswg-drafts/issues/7103, particularly when there are concave corners.

SebastianZ commented 1 month ago

It looks like we won't be able to have "one property to rule them all". Though @smfr's approach still seems like a great approach to handle the border shape issue. And I agree with him that border-shape fits best here. As the background's shape can be adjusted to follow the border area via background-clip: border-area. And there is also a shape-inside property defined in CSS Round Display 1 (that spec. really needs a rebranding as it's about arbitrary display shapes), which affects the content area and also takes a <basic-shape>, and might possibly also take a border-area value.

With those three, authors can basically change the whole element's shape.

Regarding the suggested syntax for border-shape, I think it has a few mistakes in it based on how I understand @smfr's descriptions and it might also see some additions and relaxationst. Therefore, my interpretation of this is,

Given those points, I think the syntax of border-shape should look like this:

border-shape: [ <corner-shape> || <border-radius> ]
              | [ <basic-shape> [ <line-width> || <border-line-style> || <geometry-box> ] ]
              | [ <basic-shape> [ <line-width> || <geometry-box> ] ]{2}

<corner-shape> = [ round | angle ]{1,4}
<border-line-style> = dotted | dashed | solid
<border-radius> = <length-percentage [0,∞]>{1,4} [ / <length-percentage [0,∞]>{1,4} ]?

Open questions are:

Sebastian

smfr commented 1 month ago
[ <basic-shape> [ <line-width> || <geometry-box> ] ]{2}

The two-shape form doesn't take a line width, because it just fills the space between the shapes. But I suppose you could stroke both shapes? If you allow that, then authors will want different colors and widths for the inner and outer strokes.

How does this interact with other properties like corner-shape, border-radius, border--width, border--style, etc.? Is it meant to be a shorthand for them?

It could just subsume corner-shape. Do we need both? Interaction with border-radius gets tricky because of all the longhands. Also specifying all corner radii in this property could get cumbersome. I'm not sure what to do here.

May <line-style> be used instead of the restricted <border-line-style>?

These values don't make sense on arbitrary paths: groove, ridge, inset, outset and double is hard to implement efficiently.

How are line styles meant to be drawn?

That's why I limited to solid, dashed and dotted :)

Are the shapes restricted somehow to avoid overly huge ink overflows?

I say no. Authors can already create large ink overflow with shadows.

jsnkuhn commented 4 weeks ago

An alternative to "corner-shape" I've brought up a couple of times is to include the the complete functionality of "corner-shape" in css-shapes as expanded options in inset(), xywh() and rect(). This currently includes round for corner rounding so seems like it could be expanded to include the other shapes and combinations of shapes from "corner-shape"?

inset(0 round 12px);
inset(0 angle 8px);
inset(0 round none 12px );
inset(0 round angle notch scoop 12px / 24px ); 
LeaVerou commented 2 weeks ago

The ergonomics of the single shape version are very poor — essentially you are specifying a shape that is not shown anywhere in your layout, it’s neither the inner nor the outer shape.

I strongly believe we need to find a way to offset these paths so that the single shape syntax can be useful, since there is a host of use cases that only require a single shape (use cases with no borders for one). It cannot be an insurmountable problem to offset a closed path!

Offsetting a path will be needed anyway so we can specify shape-inside and shape-outside to reasonable values.

smfr commented 2 weeks ago

Offsetting paths with loops and holes (multiple subpaths) will give unexpected results (holes disappear).

css-meeting-bot commented 2 weeks ago

The CSS Working Group just discussed [css-shapes-2] Add a way to change an element's shape, and agreed to the following:

The full IRC log of that discussion <noamr> https://github.com/w3c/csswg-drafts/issues/6997#issuecomment-2311513957
<dbaron> noamr: I posted the comment from smfr with a proposal
<dbaron> fantasai: The proposal is to add a new border shape property
<dbaron> fantasai: We can probably split discussion into concept vs. specific syntax.
<Rossen9> q?
<dbaron> fantasai: The idea of a border-shape property is that it defines shape of border edge of box.
<dbaron> fantasai: It can both cut in to border edge and go out. It just gives a new path.
<dbaron> fantasai: We use that for painting purposes only.
<dbaron> fantasai: So backgrounds, border. Doesn't affect layout.
<dbaron> fantasai: The syntax here subsumes the corner-radius step. We can talk about that...
<dbaron> fantasai: We could make it a shorthand for corner-shape and border-radius and another thing that has a path.
<dbaron> fantasai: The proposed syntax has several variations.
<lea> q+
<dbaron> fantasai: One is to provide border radius and corner shape values and derive shape from that.
<dbaron> fantasai: Another is a basic-shape that gets stroked.
<dbaron> fantasai: With shape function you can provide shapes relative to the box.
<dbaron> fantasai: Third option gives 2 paths, one for the outer border edge and the other for the inner border edge.
<dbaron> fantasai: you fill in between them.
<dbaron> fantasai: There's an example in the issue showing how that would work.
<dbaron> fantasai: With the shape function you can have fully responsive shapes.
<Rossen9> q?
<dbaron> fantasai: That's the basic proposal, we can go into details from there.
<TabAtkins> q+
<noamr> q+
<dbaron> lea: First, +1 to pursuing this. This needs solving, outstanding for some time. This avoids complexity about how borders adapt to regular shapes.
<Rossen9> ack lea
<dbaron> lea: I think we should avoid basing things on corner-shape or using it as precedent. I think at this point it was a bad design that we need to rethink.
<dbaron> lea: It introduces a lot of complexity for things that aren't needed.
<dbaron> lea: I'd design this ignoring corner-shape.
<dbaron> lea: Naming border-radius around borders is a recognized mistake in CSS.
<dbaron> lea: I worry about calling this border-shape. Element might have no borders.
<dbaron> lea: I think it makes more sense than border-radius here because it's defining the shape of the border.
<dbaron> lea: Though there's complexity about what if these paths intersect.
<fantasai> SebastianZ's refinement of the proposal -> https://github.com/w3c/csswg-drafts/issues/6997#issuecomment-2342081298
<dbaron> lea: Also, does border line style need to be a part of this?
<dbaron> lea: Would be nice to see examples with real world use cases and the syntax.
<dbaron> fantasai: I've gone back and forth on the border-radius question for that reason.
<dbaron> fantasai: It does give border edge rather than padding edge or content edge.
<dbaron> fantasai: We could call it box-shape if we wanted something different, but might be useful to be consistent.
<dbaron> fantasai: I think I disagree on corner-shape, it's a good way to do common cases like beveling.
<Rossen9> ack TabAtkins
<dbaron> TabAtkins: This proposal looks good to me. I think all 3 variants are useful.
<dbaron> TabAtkins: Simple one, single path, or two paths.
<dbaron> TabAtkins: A little weird: the way to specify fill on 2 stroke path is weird. The fill is specified using a border-area background-clip value.
<lea> TabAtkins: background-clip: border-area; was something we resolved on months ago
<lea> it's not part of this proposal
<fantasai> s/TabAtkins:/TabAtkins,/
<dbaron> lea: background-clip border-area was something we resolved in February
<dbaron> lea: It's something we've already specified, not being proposed here.
<dbaron> TabAtkins: I'll leave it alone though I think we're overloading background.
<Rossen9> ack noamr
<dbaron> noamr: Wanted this in my web developer days. Popup baloon on wikipedia.
<dbaron> noamr: I wonder if this is 3 proposals.
<dbaron> noamr: I think the third one with the 2 paths is complex and has many questions. Could we start with other 2 and then add the third?
<dbaron> noamr: Or do we need to specify all at the same time?
<dbaron> noamr: Most use cases seem handled by second type.
<dbaron> noamr: Also needs clarification, important for this proposal: it's not just about painting, but also clips descendants like border-radius.
<dbaron> fantasai: only if you specify overflow
<dbaron> noamr: yes, same clipping semantics as border-radius
<lea> q+
<TabAtkins> I'm not sure what looks particularly complex about the two-path version. Fill one shape, cut out the second shape, that's well-defined to do with a single path using reverse winding on the "inner".
<dbaron> noamr: Reason why ??? are specific and not using the existing ones is that some of the values don't work with arbitrary paths.
<dbaron> fantasai: This is the rendering simon gave me about how border colors should work.
<dbaron> fantasai: Now when you specify border colors there's a diagonal cut where the color changes. That's calculated from the inner and outer border stuff.
<astearns> s/???/border-line-style options/
<dbaron> fantasai: From this picture I think he's proposing we use that same calculation. This isn't especially useful. But it's a clear path from the existing behavior to what we'd do as the border path.
<dbaron> fantasai: and generally you only use one color.
<dbaron> fantasai: gives a defined rendering for the other cases.
<dbaron> fantasai: We could consider other options but this seems like the best idea so far.
<dbaron> fantasai: Regarding inner/outer paths, Simon's rendering is weird. But use case: consider a speech bubble. You don't typcially want a single thickness stroke around the entire speech bubble. The corners are narrow, middle of curve is thicker, etc. Or some other pen/brush-like effect.
<dbaron> fantasai: So variation in thickness of stroke of border is a common scenario.
<dbaron> fantasai: That's the use case for having two.
<dbaron> astearns: To noamr's point, those could be separated and we could get to them later. The first 2 bits don't depend on it.
<dbaron> fantasai: but we have a proposal that also addresses those.
<Rossen9> ack lea
<dbaron> lea: Re fantasai, I agree the diagonal could be any angle, just needs ot be well defined.
<dbaron> lea: grammar is hard to parse here. What happens if you specify only one shape, or do you always need an inner and outer.
<dbaron> TabAtkins: Can specify one shape and you stroke that shape.
<dbaron> lea: Discussions in June about difficulty of offseting a path to make it smller or bigger.
<kizu> q+
<dbaron> TabAtkins: IT strokes the center of the path; if you want inside/outside you need to specify two.
<dbaron> lea: You could have it represent the outer edge, stroke it, and clip it, but that's...
<dbaron> s/lea/fantasai/
<dbaron> fantasai: The stroking in the middle is one thing I don't like about this.
<dbaron> lea: That would force authors to use 2 shapes in many cases when they could use one.
<fantasai> s/lea: You could/fantasai: You could/
<dbaron> lea: forces authors to specify a path that's not visible at all in their layout
<dbaron> lea: I noticed in the rendering that some text is clipped, how does that work?
<dbaron> fantasai: With border-radius it's clipped if you specify overflow.
<dbaron> lea: Would be nice to see what happens if you don't
<dbaron> fantasai: Just draws on top like any other box.
<dbaron> fantasai: we're not chagning the layout, just the border and background painting
<dbaron> kizu: This issue with overflow -- we might want how to make it accsesible in most cases. ??? ??? How we can make a big border-radius. For example, making big area by default -- by default the content if hidden or auto will not ??? and will exapnd further
<dbaron> kizu: It would be nice by default to not put content under something that's hidden.
<dbaron> kizu: Would be nice to find a safe solution for this.
<Rossen9> ack kizu
<dbaron> kizu: This is a proposel that I'd want to see. ??? define shape in a way that borders still work.
<dbaron> kizu: I will check... It will depend on ths proposal to make compat shapes as boxes.
<dbaron> fantasai: With an arbitrary shape there's no real way to make extra padding magically. The intrusions might not be at the bottom. If it's hourglass shape, what does that do?
<astearns> there is shape-inside…
<dbaron> fantasai: This has to be something that ... shape of the box. Up to author to adjust the padding.
<dbaron> fantasai: For authors adjusting shape... we still don't have a way to make shape-inside to something s in a responsive + performant way.
<dbaron> fantasai: border-image doesn't avoid the content. If you make the border thick it does. But if you use insetting ability then you don't have that.
<dbaron> fantasai: It will paint under the content.
<Rossen9> q?
<dbaron> fantasai: I have some questions.
<TabAtkins> +1
<dbaron> fantasai: First, do we want to adopt a border-shape property such as this one, in princple?
<dbaron> (questions to the group)
<astearns> and as a hack, you could use shape-outside to mimic the inside edge of the fancy border
<noamr> +1
<kizu> +1
<kbabbitt> +1
<astearns> +1
<dbaron> fantasai: with details ot be worked out later
<chrishtr> +1
<miriam> +1
<lea> +1 but very concerned about the single shape syntax
<dbaron> astearns: back to noam's point of separability, we can draft this, and if we find the 2 shape thing is more complicated we can mark it at risk or move to another level
<dbaron> noamr: or give it TODOs
<chrishtr> q+
<dbaron> fantasai: but this is the beginning, so draft what we can
<lea> q+
<Rossen9> ack fantasai
<dbaron> chrishtr: what about animations?
<dbaron> fantasai: yes, you can animate it
<dbaron> fantasai: given with shape syntax, animates same way as shapes
<dbaron> chrishtr: can it be HW accelerated?
<noamr> q+
<dbaron> fantasai: as much as anything else?
<Rossen9> ack chrishtr
<dbaron> chrishtr: has the team tried? Expressed in shader, put on compositor thread?
<dbaron> fantasai: I don't know implementation details.
<astearns> q+
<dbaron> chrishtr: would be good to follow up on that question
<dbaron> fantasai: does that block anything?
<dbaron> TabAtkins: Given that we can hw accelerate clip-path, seems similar.
<fantasai> s/does/sure, but does/
<dbaron> noamr: we don't hw accelerate border (?), we don't hw accelerate clip-path yet. We don't accerelate border-raidus if it's complex.
<dbaron> chrishtr: We draw it with a shader when possible. But we don't accelerate animations of it.
<dbaron> chrishtr: I want to make sure the syntax gives us a path to doing that.
<dbaron> noamr: I think the challenge is the complextiy of drawing borders, not the shapes.
<dbaron> chrishtr, noamr: color, image, storke, shadow
<Rossen9> ack lea
<dbaron> lea: Were lots of +1 about working on this. I wanted to get clarity on what's a detail?
<dbaron> lea: Would people be open to improving single shape syntax so it doesn't define something that's not visible anywhere.
<dbaron> fantasai: Just question of whether we want to solve the problem.
<noamr> q-
<dbaron> fantasai: I don't want to dive into all the details without agreeing that we want to pursue the general idea.
<ydaniv> q+
<Rossen9> ack ydaniv
<dbaron> ydaniv: Why you can do something like shape-outside to offset text?
<dbaron> fantasai: you can, yes
<dbaron> ydaniv: so if you want text overflowing, and also ?? on top of it, you can use shape-outside
<astearns> shape-outside uses floats. this would not
<dbaron> fantasai: You can use shape-outside on a shape element to flow text around it. But there are interesting problems with shape-inside.
<dbaron> fantasai: This proposal doesn't change layout. You could use the same path in shape-outside (which we have) and shape-inside (which we don't).
<dbaron> Rossen9: do we want to resolve on all 3 or one at a time?
<dbaron> fantasai: Let's go one at a time, I have more questions on specifics.
<dbaron> Proposed resolution: adopt border-shape in principle and continue to discuss specifics
<dbaron> RESOLVED: adopt border-shape in principle and continue to discuss specifics
<noamr> q+
<dbaron> fantasai: Next question: what should we call it?
<astearns> q-
<dbaron> fantasai: Current proposal is border-shape.
<dbaron> fantasai: Do people want to suggest other names?
<kizu> I like `box-shape`
<dbaron> fantasai: Quickly, could bikeshed more later.
<ydaniv> s/ydaniv: Why you can/ydaniv: Why you can't/
<Rossen9> ack noamr
<dbaron> noamr: Argument for border-shape -- it's in the family of existing things. We already have border-radius even if we don't like it.
<dbaron> noamr: this makes it discoverable
<dbaron> fantasai: We have border-shape and box-shape. Should we straw poll?
<fantasai> 1. border-shape
<fantasai> 2. box-shape
<castastrophe> 1
<TabAtkins> 1
<kizu> 2
<kbabbitt> 1
<fantasai> 0
<futhark> abstain
<dbaron> 1
<noamr> 1
<alisonmaher> 1
<astearns> 1 (for now, ok to bikeshed later)
<Rossen9> 1
<kschmi> 1
<masonf> abstain
<oriol> abstain
<ydaniv> abstain
<noamr> (box-shape feels like a layout thing)
<dbaron> RESOLVED: Call it border-shape for now
<dbaron> fantasai: next question: should this be a shorthand that sets corner-shape and border-radius properties, or should it be a separate property than when sets, overrides everything else regardless of cascade
<astearns> q+
<lea> q?
<dbaron> fantasai: I feel like shorthanding makes sense, but question about what happens if you set both.
<lea> q+
<dbaron> fantasai: If we make it a shorthand then if you specify either a shape or corner then that's what you get for sure.
<TabAtkins> q+
<dbaron> fantasai: If it's a separate longhand, then the border-shape always wins.
<dbaron> fantasai: We could go either way... could also open separate issue.
<dbaron> astearns: I would like to take this to an issue. I'm happy to have something in the draft with issue in it. But I don't think we can design this in committe right now.
<astearns> ack astearns
<Rossen9> ack lea
<dbaron> lea: I agree that this needs more design work. I also don't think we should design around corner-shape, in spec for >10 years and not implemented. If we're adding a new thing, do we *also* need corner shape? We already have border-radius for common cases. We don't need another new way to set element shape.
<dbaron> lea: For something it might be easier, let's define a shape funciton and continue to use element shape for them.
<lea> qq+
<dbaron> fantasai: Counter to that is that there are common use cases. When you're using a path you have to give a path for the whole shape. Corner shape lets you set corners independently, can do writing-mode-relative easily.
<dbaron> fantasai: When you're doing something compcilated you need to specify the whole path.
<Rossen9> ack lea
<Zakim> lea, you wanted to react to lea
<Rossen9> ack fantasai
<Zakim> fantasai, you wanted to respond to that
<dbaron> lea: That would apply if all we could do is specify path. But there's a lot to shapes: inset, etc.
<dbaron> fantasai: But you have to set them all. But if you want a slash shape, you have to define the path of the whole box rather than just the corner.
<castastrophe> q+
<dbaron> fantasai: I think that's a much easier thing when that's what they want,which is common.
<dbaron> fantasai: So I think the border-shape properties make sense.
<dbaron> fantasai: But that doesn't really affect this question because we still have the border-radius properties.
<dbaron> fantasai: corner-shape is only a minor additional complexty
<dbaron> lea: That's a ???. corner-shape does make some cases simpler. But it has cruft for things that aren't really needed.
<dbaron> lea: For things like bevel, helps with some polygons, but most polygons in the wild have rounding which it doesn't account for.
<Rossen9> ack TabAtkins
<dbaron> TabAtkins: ??? concern is that I don't care about shorthand or complicated direction. If you just set border-shape it should do the right thing. It looks like in the example he didn't to do something to explicitly cause the regular border to not render. It should work better in that case.
<dbaron> TabAtkins: If you set border-shape it should affect the other border properties appropriatly without author needing to do extra work.
<dbaron> castastrophe: Question: is there a perf gain for repainting just a border corner versus a larger shape?
<dbaron> fantasai: no idea
<dbaron> fantasai: I also don't think that's a consideration; we should worry about the ergonomics.
<Rossen9> q?
<dbaron> noamr: We could probably figure out from any shape, could reverse-engineer it to a corner-shape and get the same hypothetical perf benefit.
<Rossen9> ack castastrophe
<dbaron> fantasai: So on this question we're opening a new issue.
<dbaron> fantasai: ANother question: do we want the 2 path syntax where you can fill between or do we think that's not worth trying?
<noamr> q+
<lea> q+
<dbaron> fantasai: The current proposal has a stroked 1 path version and a filled 2 path version. Do we like both options or want just 1?
<astearns> +1 to add it in for now, issues to remove or postpone as necessary
<Rossen9> ack noamr
<dbaron> noamr: I tihnk we want both shapes, question of editorial understanding and iteration on spec to make it readable.
<TabAtkins> 1-shape super restrict you to *solely* the geometry of a shape's stroke, that's quite limiting (if common)
<Rossen9> ack lea
<dbaron> lea: especially if we don't figure out how to offset paths property, I think the 2-shape syntakx is essential for specifying what you want
<dbaron> lea: If you don't have a way to specify the shapes explicitly then it doesn't have the necessary level of control.
<dbaron> lea: we definitely need the 2 path syntax esp. if we don't figure out the 1 path syntax.
<dbaron> (I think lea meant offsetting for the 1 path syntax.)
<dbaron> RESOLVED: specify the 2 path syntax
<dbaron> RESOLVED: also specify the 1 path syntax
<astearns> q+
<dbaron> fantasai: One other question from smfr, a little orthogonal, do we need additional keywordsfor background-origin.
<dbaron> fantasai: and maybe background-clip
<astearns> q-
<dbaron> fantasai: to change from using the layout box to using the box of the whole border shape
<dbaron> Rossen9: also sounds like a feature we can define once we get ...
<TabAtkins> Yeah, I think it's needed.
<dbaron> astearns: noam, would you be willing to become an editor for shapes2 for these features
<dbaron> noam: yes
<dbaron> astearns: my proposal is to add Noam as editor to shapes 2
<dbaron> fantasai: this goes in borders 4
<dbaron> astearns: ok, that spec instead!
<astearns> s/instead!/in addition!/
<dbaron> fantasai: I'm going to take actions to file followup issues, and I'm going to add this to the ED of borders 4.
LeaVerou commented 2 weeks ago

Offsetting paths with loops and holes (multiple subpaths) will give unexpected results (holes disappear).

That’s fine since the vast majority of use cases won’t have loops and holes. We just need an algorithm that a) will work well for the cases we actually need here and b) will be well-defined for the rest (even if it produces poor results). There is always the escape hatch of using two shapes if offsetting produces poor results, so it’s totally acceptable to focus this on the common cases.

fantasai commented 2 weeks ago

@LeaVerou If we're offsetting by default (which I assume is your proposal) then we need to have reasonable behavior by default, though.

LeaVerou commented 2 weeks ago

So over the break @tabatkins and I came up with an algorithm for how to not only draw the border on a single shape, but even respect the different top, right, bottom, left border widths.

Conceptually, the general idea was: for each point along the path, you find the tangent, and draw a vector perpendicular to the tangent with a length calculated as follows:

I don’t have access to our sketches as I write this, but it seemed to us that this would produce a very nice result for many common cases, and a not-terrible result for edge cases.

Ideally, the single shape syntax should allow specifying either the border edge (and draw the border on the inside of it) or the padding edge (and draw the border on the outside of it). We'd still need to decide how to do line joins and miter limits.

@svgeesus @fserb we'd love your feedback!

kizu commented 2 weeks ago

Just quickly sharing two examples from the latest Josh Comeau's blog redesign which seem like a good use case for the border-shape:

A screenshot of an “info” icon in the corner of a message box, with a thick border on the left going around it, as a single nicely rounded line. A screenshot of a triangular warning icon, again, with a line going around it, following the triangular shape at a distance.

smfr commented 1 week ago

The current proposal doesn't have a way to just get that border on the left edge (with rounded ends), though.

tabatkins commented 1 week ago

So over the break @tabatkins and I came up with an algorithm for how to not only draw the border on a single shape, but even respect the different top, right, bottom, left border widths.

A little more detail on our motivation:

It's reasonable to view border-shape as providing three levels of increasing complexity & power for specifying borders: the border- properties define a rounded rect path that gets stroked; the single-path version defines an arbitrary path that gets stroked; the two-path version defines an Iarbitrary shape* that gets filled. Generally, we want increasing complexity to track with increased power, so authors that want only a small deviation from the built-in stuff can just go up a small amount in complexity, rather than having to jump all the way to some "primitive" version that's super hard to work with (but super powerful).

So, the "rounded-rect" borders can have up to four distinct widths and colors, one for each horizontal/vertical side, with the rounded corners smoothly interpolating between the values. Our proposal is, we think, both the simplest and most natural way to reproduce this ability in the single-path syntax, with the path direction dictating which of the four border-width/color values to draw from (or which two to interpolate between, and by how much).

If you use the single-path to produce a rounded rect, or any reasonably close variation on that, you'll get a result essentially identical to what is defined for the border-* properties today. (The way the corners are interpolated will be slightly different, which I think is unavoidable - I don't think the corner interpolation rules can be applied to an arbitrary path without an explosion of complexity. But I could be wrong about that.)

If you do other shapes, you'll still get a reasonable result, with decent control knobs to twiddle. If this still isn't enough, the two-path syntax still exists, but we think this should provide enough power to the single-path syntax that you'll often be satisfied.


We could also provide a further intermediate syntax, where you can provide the colors and widths along the path explicitly, tied to the path distance; similar to defining gradient stops. We're already using that syntax in gradients and in linear(), and will likely use it in more places (cubic splines for more easing?), so it is a familiar pattern to work with. This is more complex than needed for simple shapes, but is still vastly less complicated than the two-path syntax.

tabatkins commented 1 week ago

Here's my fuller proposal, in more detail:

noamr commented 1 week ago

A couple of questions/clarifications:

  1. Wouldn't we want the same inner/outer functionality for the border-radius/corner-radius case? Like have rounded corners of different radiuses outside and inside? Feels a bit arbitrary that this simple rounded/smoothes version is only a one-shape syntax and the full basic-shape syntax doesn't include it. Perhaps we could utilize outline for this, where the inner shape strokes based on outline values and the outer shape strokes based on border values? In any case the rectangular outline makes little sense here.

  2. It's very surprising for me that background is what fills between the shapes. I would expect backgrounds to fill the inside shape, like in border-radius. I think we should find some semantic that works with border-image and aligns with Tab's proposal above for how the different border-widths work.

smfr commented 1 week ago

I understand the motivation for the single path syntax taking its widths from border-width, but I think in practice this is going to be pretty hard to implement. The proposal to inset the path by taking a perpendicular line at each point whose length is an interpolated width is interesting, but may result in a generated path with thousands of points; both the computation and rendering could be slow. Perhaps there's a way to do something similar with a scale transform which just scales the path, using a center point that takes the border widths into account?

Optionally, you can provide a gradient-stop-like list of widths and colors, each associated with a path distance (0% indicating the start, and 100% indicating the end, or lengths can be used)

I don't think this will work very well if you apply it to boxes with different aspect ratios; the author would have a hard time matching gradient points with the box corners.

It's very surprising for me that background is what fills between the shapes

I'm not sure it does? I think we can fill the space between the inner and outer shapes with border colors by default, with the usual diagonal corner joins.

noamr commented 1 week ago

It's very surprising for me that background is what fills between the shapes

I'm not sure it does? I think we can fill the space between the inner and outer shapes with border colors by default, with the usual diagonal corner joins.

I guess that's what I understood from https://github.com/w3c/csswg-drafts/issues/6997#issuecomment-2311513957?

smfr commented 1 week ago

I guess that's what I understood from #6997 (comment)?

That was with border-clip: border-area. Using border colors, you'd get:

border-colors

tabatkins commented 1 week ago

I understand the motivation for the single path syntax taking its widths from border-width, but I think in practice this is going to be pretty hard to implement.

Is this a limitation of CoreGraphics or a more general issue? To my knowledge, Skia is okay with variable-width strokes (@fserb ?).

(I think it's okay if we end up doing something simpler by default, fwiw, so long as it's possible to do variable widths/colors in the 1-path syntax somehow in a reasonably usable manner. Like, worst case, we could just take the border-top width and color, or something like that.)

I don't think this will work very well if you apply it to boxes with different aspect ratios; the author would have a hard time matching gradient points with the box corners.

Yeah, this is definitely a special-case "one specific box" sort of thing, which doesn't make me very happy. We need something to control this, tho; even if you fall back to the two-path syntax, you'd then need to do a one-off fill image, which is just as wonky.

The only other suggestion I would have is being able to match up the widths/colors with the path commands (or rather, between the commands), either with a separate list that has a 1:1 correspondence, or inserting them directly into the shape() syntax. I lean toward the latter; we can just say that contexts that aren't stroking the path would ignore that information (or if they're stroking it but doing a stroke fill some other way, they'd ignore the color info).

That way, you could reproduce a rounded-rect with 8 path commands, as normal, and just place the desired border-width/color before/after each straight-line segment; the corner arcs would interpolate. Or, of course, do something else, like in this example (where the angled corners take the color of the left/right sides, rather than being half-colored by each side).

We already might want to put in corner-rounding controls into shape() between commands (rather than always requiring them to be communicated separately, and applying equally to all corners), so I think this fits in quite naturally.

Using border colors, you'd get:

What's determining the angle/position of the color change there? It can't just be radial lines from the center, as that would mean the three sharp corners would only look right if the border-width ratios happened to match the box's own aspect ratio.

tabatkins commented 1 week ago

Or, another alternative: define another function, stroke(), which is identical to shape() but specifically defines a stroked path. It can then have the additional grammar items that handle stroke width and color, along with future items in a prelude argument like stroke direction (center/left/right) and such. (And it would not have the fill-specific items in its prelude argument, like the nonzero/evenodd keyword that shape() has.)

Then, individual contexts would define whether they expect a <shape> or a <stroke>. If you pass stroke() to a <shape>, it's allowed, but we ignore all the stroke-specific stuff and default the fill-specific details in a context-specific manner. If you pass shape() (or any of the basic-shape function) to a <stroke>, it's allowed, but we ignore all the fill-specific stuff and default the stroke-specific details in a context-specific manner (such as getting the stroke width from 'border-top-width' or something). (We already do this if you use path() or shape() in Motion Path, for instance; the nonzero/evenodd keyword is simply ignored, since that spec actually takes a third semantic type, a plain ol' path.)

So the one-path syntax would actually be a <stroke>; the two-path syntax would be a pair of <basic-shape>s.