Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[css-shapes] Allow CSS grammar for path shapes #5674

Closed
noamr opened this issue Oct 28, 2020 · 22 comments · Fixed by #5711
Closed

[css-shapes] Allow CSS grammar for path shapes #5674

noamr opened this issue Oct 28, 2020 · 22 comments · Fixed by #5711

Comments

@noamr
Copy link
Collaborator

noamr commented Oct 28, 2020

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)
@faceless2
Copy link

faceless2 commented Oct 29, 2020

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.

@noamr
Copy link
Collaborator Author

noamr commented Oct 29, 2020

This may actually be solved by: #542

@tabatkins
Copy link
Member

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.

@noamr
Copy link
Collaborator Author

noamr commented Oct 29, 2020

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.

@tabatkins
Copy link
Member

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.

@noamr
Copy link
Collaborator Author

noamr commented Oct 29, 2020

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.

@noamr
Copy link
Collaborator Author

noamr commented Oct 29, 2020

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

@tabatkins
Copy link
Member

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.

@noamr
Copy link
Collaborator Author

noamr commented Oct 29, 2020

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.

@noamr
Copy link
Collaborator Author

noamr commented Oct 29, 2020

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

@tabatkins
Copy link
Member

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.

@noamr
Copy link
Collaborator Author

noamr commented Nov 1, 2020

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

@Crissov
Copy link
Contributor

Crissov commented Nov 2, 2020

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};

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.

curve [to|by] [<coordinate-pair> 
  [via auto] |                   ; quadratic
  [via <coordinate-pair>] |      ; quadratic
  [via auto <coordinate-pair>] | ; cubic 
  [via auto auto] |              ; cubic 
  [via <coordinate-pair>{2}] |   ; cubic
                                 ; quadratic
]+

@noamr
Copy link
Collaborator Author

noamr commented Nov 2, 2020

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.

@noamr noamr changed the title [css-shapes] Find a way to create a polygon with rounded-corners and non-absolute coordinates [css-shapes] Allow CSS grammar for path shapes Nov 10, 2020
noamr added a commit to noamr/csswg-drafts that referenced this issue Nov 10, 2020
noamr added a commit to noamr/csswg-drafts that referenced this issue Nov 10, 2020
@bradkemper
Copy link
Contributor

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()?

@noamr
Copy link
Collaborator Author

noamr commented Nov 15, 2020

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?)

@smfr
Copy link
Contributor

smfr commented Nov 15, 2020

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

@noamr
Copy link
Collaborator Author

noamr commented Nov 15, 2020

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.

@bradkemper
Copy link
Contributor

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.

@noamr
Copy link
Collaborator Author

noamr commented Nov 29, 2020

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 (#5711).
I suggested picking a "corner" rather than a starting point, which would also allow a logical coordinate system.

@tabatkins
Copy link
Member

Agenda+ to discuss merging the PR in #5711.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-shapes] Allow CSS grammar for path shapes, and agreed to the following:

  • RESOLVED: Merge this PR into next level of Shapes
The full IRC log of that discussion <dael> Topic: [css-shapes] Allow CSS grammar for path shapes
<dael> github: https://github.com//issues/5674
<dael> TabAtkins: I've been looking for something like this for a while. SVG path is a little weird. RIght now you have to do path as a string and you can't concat a string it means you can't build path with variables
<dael> TabAtkins: User friction thing
<dael> TabAtkins: Proposal, not from me, with good syntax converting svg syntax to css friendly version. I really like how it looks. Extensible to logical coordinates. I think this is a nice good approach and I'l like to add to spec
<dino> q+
<dael> fantasai: I reviewed the PR and discussion and I think it's a good prop, well specified
<astearns> ack fantasai
<astearns> ack dino
<dael> dino: I left some minor comments, but don't care if they're not addressed.
<dael> dino: If one was to animate; this was defined as eq to SVG path so thus if you animate between the svg rules apply and thus you need same number of segments with same type?
<dael> TabAtkins: Yeah
<dael> dino: Therefore a curve, if a command was curve xy via something if one gave 2 coord and the other 1 they would not animate, right?
<dael> dino: THe distinction between quadratic and cubic is number of params, not command type.
<dael> dino: Maybe a bit strange curve says where it's going first. But again it's minor. I'll mention it, but I don't care
<dael> astearns: And one of the reasons to take the PR is so we can open more issues to make amendments. I assume animations is not defined in PR so we'd unpack in spec.
<dael> TabAtkins: Yeah
<dael> astearns: dino you mentioned you made comments. I saw one on PR about typo
<dael> dino: They're all minor and can be issues after
<dael> astearns: Hearing agreement. Objections to merge this PR?
<dael> RESOLVED: Merge this PR into next level of Shapes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
8 participants