Csswg-drafts: [css-shapes] Allow CSS grammar for path shapes

Created on 28 Oct 2020  Â·  21Comments  Â·  Source: w3c/csswg-drafts

Currently, when using shapes in e.g. clip-path, some features are only allowed in polygon, and some only in path.

  • Polygon: CSS custom properties as coordinates, relative or other non-pixel units
  • Path: Rounded corners with quadratic/bezier curves

So it's currently impossible to create a non-rect polygon that has both rounded corners and relative (or em) coordinates, or custom properties, for example a speech bubble with an arrow.

I suggest one of the following:

  • Allow some form of radius in polygon() (easier)
  • Allow resolving of non-pixel units in path() (more powerful)
Agenda+ css-shapes-2

All 21 comments

Thoroughly agree with this. The lack of units is just about manageable, but the inability to use var() is a real issue for anything complex. We should allow a path to be concatenated into one string from all the arguments supplied to the path() function. For example:

  • path("M 100 100 " var(--outerpath) var(--innerpath))
  • path("M 100 100 A 100 100 0 0 0 " calc(100 * sin(var(--a))) calc(100 * cos(var(--a))))

I'm presuming spaces are inserted between the tokens, and that numbers are stringified before concatenation. Even if units aren't allowed, this would go a very long way to making it more usable.

This may actually be solved by: https://github.com/w3c/csswg-drafts/issues/542

Oof, string concat isn't the way to go here.

What we've needed for a long while is a CSS-ified grammar for path strings, that takes idents and lengths and such, so it can interoperate with the rest of CSS's value system. I've been meaning to spend time on that for several years, but it's always slipped below the priority line.

Oof, string concat isn't the way to go here.

Yea, it feels like it would solve the issue but not in a nice way.

What we've needed for a long while is a CSS-ified grammar for path strings, that takes idents and lengths and such, so it can interoperate with the rest of CSS's value system. I've been meaning to spend time on that for several years, but it's always slipped below the priority line.

Sounds great! Maybe I'll have time to take a crack at it.

Sounds great! Maybe I'll have time to take a crack at it.

I'd love to review it. Note that we can be opinionated about this and impose CSS best practices for grammar design, like commas between repeated items and not just any old place, and using keywords rather than 0/1 values masquerading as booleans. It should not be a goal to allow people to just take an existing path string and remove the quotes, imo.

Sounds great! Maybe I'll have time to take a crack at it.

I'd _love_ to review it. Note that we can be opinionated about this and impose CSS best practices for grammar design, like commas between repeated items and not just any old place, and using keywords rather than 0/1 values masquerading as booleans. It should _not_ be a goal to allow people to just take an existing path string and remove the quotes, imo.

got it, I'm envisioning it looking more like the other shapes like polygon.

@tabatkins
WDYT, more like this:
clip-path: path( move(to 12px calc(8em - 1px)), line(to 50% 50%), curve(to 20px var(--something)), quadratic-curve(by 2px 8px), close() )
or like this:
clip-path: path( M 12px calc(8em - 1px), L 50% 50%, C 20px var(--something), q 2px 8px, Z )

I can go either way! I slightly lean away from the nested-functions approach just because more nesting makes it harder to read and write.

I suspect it would be nice to provide the commands as both single-letter (matching existing path strings) and full-word forms.

I can go either way! I slightly lean away from the nested-functions approach just because more nesting makes it harder to read and write.

I suspect it would be nice to provide the commands as both single-letter (matching existing path strings) and full-word forms.

Awesome. I'll go with non-nested, allowing both SVG-style and full keywords.

We can't really do the SVG-style as is because it relies on case sensitivity :)

That's not necessarily a problem. CSS is generally case-insensitive in the ASCII range, but it's allowed to not be, if it wants. For example, anything specified as a <custom-ident> must retain the author-supplied casing, even if it's fully in the ASCII range.

So it would be a slight departure from standard CSS style to make M and m have different meanings, but it's definitely possible, and I think probably a good idea.

Great. The current syntax I have in mind is approximately this:

[[[[relative]? move to] | M | m] <<length-percentage>> <<length-percentage>>] |
[[[[relative]? line to] | L | l] <<length-percentage>> <<length-percentage>>] |
[[[[relative]? [horizontal|vertical] line to] | h | H | v | V] <<length-percentage>>]
[[[[relative]? curve to] | Q | q | c | C]<<length-percentage>> <<length-percentage>> <<length-percentage>> <<length-percentage>> [<<length-percentage>> <<length-percentage>>]] | 
[[[[relative]? smooth curve to] | S | s | T | t] <<length-percentage>> [to|by] <<length-percentage>> [<<length-percentage>> <<length-percentage>>]] |
[[[[relative]? arc to] | a | A] <<length-percentage>> <<length-percentage>> <<length-percentage>> <<length-percentage>> <<length-percentage>> <<length-percentage>> <<length-percentage>>] |
[close | z | Z]

For the example of a scalable balloon-with-cursor clip, it would look like:

path(m 0 calc(var(--radius) + var(--cursor-height)),
     Q 0 var(--cursor-height) var(--radius) var(--cursor-height))
     H calc(var(--cursor-offset) - var(--cursor-width) / 2),
     L var(--cursor-offset) 0,
     l calc(var(--offset-width) / 2, calc(0 - var(--cursor-height))),
     L calc(100% - var(--radius)),
     Q 100% 0 100% var(--radius),
     V calc(100% - var(--radius)),
     Q 100% 100% calc(100% - var(--radius)) 100%,
     H var(--radius),
     Q 0 100% 0 calc(100% - var(--radius)),
     Z)

or

path(from 0 calc(var(--radius) + var(--cursor-height)),
     curve to 0 var(--cursor-height) var(--radius) var(--cursor-height))
     horizontal line to calc(var(--cursor-offset) - var(--cursor-width) / 2),
     relative line to calc(var(--offset-width) / 2, calc(0 - var(--cursor-height))),
     line to calc(var(--cursor-offset) + var(--cursor-width) / 2),
     horizontal line to calc(100% - var(--radius)),
     curve to 100% 0 100% var(--radius),
     vertical line to calc(100% - var(--radius)),
     curve to 100% 100% calc(100% - var(--radius)) 100%,
     horizontal line to var(--radius),
     curve to 0 100% 0 calc(100% - var(--radius)),
     close)

or a combination of both.

Some doubts I'm contemplating:

  • Whether both syntaxes are actually necessary, maybe the SVG-like one is sufficient, though the other one feels more like CSS (e.g. like the gradient syntaxes)
  • Perhaps use by instead of relative for relative lines/curves/etc?
  • Maybe use lineto etc instead of line to, as it conforms with the names in SVG
    -

For what it's worth, I'd prefer to keep the SVG d attribute syntax with case-sensitive single-letter commands for verbatim dumps into path(<string>) and define the more CSSish keyword syntax as a new type, e.g.:

~~~~ ebnf
::= shape( [
[[horizontal|vertical] [to|by] <>+] |
[[move|line] [to|by] +] |
[qurve [to|by] [ [via ]?]+] |
[curve [to|by] [ [via {1,2}]?]+] |
[arc [to|by] [ [at <>{1,2}] <>? large-arc? sweep?]+] |
[close]
]# );

::= <>{2};
~~~~

PS: Replacing [horizontal|vertical] by [level|plummet] would work for me as well. As does forcing curve to have two control points for cubic bezier and one for quadratic, i.e. ditching qurve or the smooth prefix.

~~ ebnf
curve [to|by] [
[via auto] | ; quadratic
[via ] | ; quadratic
[via auto ] | ; cubic
[via auto auto] | ; cubic
[via {2}] | ; cubic
; quadratic
]+
~
~

For what it's worth, I'd prefer to keep the SVG d attribute syntax with case-sensitive single-letter commands for verbatim dumps into path(<string>) and define the more CSSish keyword syntax as a new type, e.g.:

<complex-shape> ::= shape( [
    [[horizontal|vertical] [to|by] <<length-percentage>>+] |
    [[move|line] [to|by] <coordinate-pair>+] |
    [qurve [to|by] [<coordinate-pair> [via <coordinate-pair>]?]+] |
    [curve [to|by] [<coordinate-pair> [via <coordinate-pair>{1,2}]?]+] |
    [arc [to|by] [<coordinate-pair> [at <<length-percentage>>{1,2}] <<angle>>? large-arc? sweep?]+] |
    [close]
  ]# );

<coordinate-pair> ::= <<length-percentage>>{2};

I can go with shape instead of path, and some of your other suggestions... thanks!
I don't like qurve though, feels a bit like a word that's not used anywhere else... Maybe something like:

<complex-shape> ::= shape([evenodd, ]? [from <coordinate-pair>]+,  [
    [[horizontal|vertical] [to|by] <<length-percentage>>+] |
    [[move|line] [to|by] <coordinate-pair>+] |
    [smooth curve [to|by] [<coordinate-pair> [via <coordinate-pair>]?]+] |
    [curve [to|by] [<coordinate-pair> [via <coordinate-pair>{1,2}]?]+] |
    [arc [to|by] [<coordinate-pair> [at <<length-percentage>>{1,2}] <<angle>>? large-arc? sweep?]+] |
    [close]
  ]# );

where curve without smooth would be cubic or quadratic based on number of arguments.

I find the functional notation (move(), line(), etc.) MUCH easier to read that the single letters. I pretty much hate the single letters, and they are not CSSy. Could we have those functions as the outer value, and NOT embed them in another function like shape()?

I find the functional notation (move(), line(), etc.) MUCH easier to read that the single letters. I pretty much hate the single letters, and they are not CSSy. Could we have those functions as the outer value, and NOT embed them in another function like shape()?

It's possible, but not sure how to represent the initial parameters in this case (fill-rule and starting-point).

An example in the current PR looks something like the gradient functions:
clip-path: draw(evenodd, from 50% 100px, hline by 80px, vline to var(---height));

If you remove the draw() function and use functions, it wold look like:

clip-path: evenodd, from 50% 100px, hline-by(80px), vline(var(--height)), ...);

Or if you make it more like transform, it would look like:

clip-path: fill-rule(evenodd) from(50% 100px) hline(by 80px) vline(to var(--height)) ...);

I like how the last one looks, but it also feels like it's inconsistent with the other shapes which have their own functions, which makes draw feel like a "special" shape (maybe it is?)

Is the path constructed in physical coordinate space? Should this new syntax allow for building paths which are writing-direction aware?

Is the path constructed in physical coordinate space? Should this new syntax allow for building paths which are writing-direction aware?

I think that it should be direction-aware by default. It might not even need additional syntax, it's enough to use the existing dir attribute, and of course specify that explicitly.

Or maybe even this:

clip-path: evenodd 50% 100px, hline(by 80px), vline(to var(--height)), ...;

...if it always needs a starting point. I think I would put commas there where it separates the series of drawing instructions, but not after evenodd, which feels more like a separate value part (not one in a series of path parts).

I think would be sort of special as a value type that consists of sub-values and functions (a little bit like box-shadow, which might contain a color function), rather than needing to be a function of its own. At least, I would prefer it that way and think it would be simpler and pretty intuitive to write.

Or maybe even this:

```
clip-path: evenodd 50% 100px, hline(by 80px), vline(to var(--height)), ...;
I'm not in favor exploding the shape out, it will not allow additional parameters to clip-path or to anything caller of the different shape functions. I still the current gradient-like proposal the best.

Regarding the starting point, look at the PR (https://github.com/w3c/csswg-drafts/pull/5711).
I suggested picking a "corner" rather than a starting point, which would also allow a logical coordinate system.

Agenda+ to discuss merging the PR in #5711.

Was this page helpful?
0 / 5 - 0 ratings