-
Notifications
You must be signed in to change notification settings - Fork 747
Description
One of the things that came out of #9843 was that path()
would benefit from the kind of flexibility that CSS values afford. Right now it’s an SVG path data string:
<path()> = path(
<'fill-rule'>? ,
<string>
)
CSS values would allow paths that adapt to the element geometry and other contextual parameters, and would make it possible to parameterize paths and generate parts of them via variables, solving a host of well established use cases.
Proposal
The main dilemma when doing that is whether to aim for maximum consistency with SVG paths, merely allowing <length-percentage>
for points but keeping everything else the same, or whether we should try to optimize for human usability as well.
As an experiment, I went with the latter, so the proposal below diverges from SVG paths in several ways.
Diverging from SVG also allows us to fix some of the established usability problems with its path syntax, that go beyond mere syntax. When total consistency is desired authors can always use the old syntax.
General syntax
<path()> = path(
[ <'fill-rule'>? , <string> /* Existing syntax */
| <'fill-rule'>? at <length-percentage>{2} , <path-command># /* New syntax */
]
)
- Full words rather than obscure letters
- Commands grouped in three easy to remember groups (line, arc, curve) rather than 10 separate commands, many of which are similar but distinct in subtle ways.
<path-command> = <line-path-command> | <arc-path-command> | <curve-command>
Relative vs absolute clear from the syntax (to
vs by
), rather than the obscure uppercase vs lowercase distinction
<path-endpoint> = [ to | by ] <length-percentage>{1, 2} [ down ]?
Endpoint can include one or two coordinates. If only one is provided, Y defaults to zero, unless down
is also specified, in which case X defaults to zero. This sounds weird, but results in very natural syntax (e.g. line by 1em down
)
Specific commands
Move To (M) and Close Path (Z)
SVG paths have commands to close and move, so that one can combine multiple shapes into a single path. I think this is an unfortunate coupling of unrelated concepts and we would never consider it today if it weren't so established already.
The proposal below does not include these commands at all, only an absolute starting point in the preamble. Instead, I propose we introduce separate operators to combine multiple shapes into one (union, difference, etc.), with union being by far the most needed (shape-combine()
below).
<shape-combine()> = shape-combine(<basic-shape>#)
If the last endpoint does not match the starting point, the path is automatically closed.
Yes, this means creating open paths is not possible. We can always introduce a future flag to prevent this, if it becomes needed in the future.
This may seem strange given existing SVG precedent. My justification is below.
First, I hope we all agree that as a design principle, we want to make common things easy and complex things possible. The simple case is by far more frequently a single closed shape, especially once we consider how CSS shapes are currently used. Subpaths add complexity to that simple case (since you now have to remember to close your paths), and make the syntax more error-prone (since you get unexpected results if you forget to close your paths).
But even worse, even when you do need multiple shapes, this forces you to now express all of them as paths, even when literally all but one are simpler shapes like circles and rects, introducing an ease of use to power cliff. I question whether subpaths ever reflect user intent. I hypothesize that in nearly every case they are a workaround to a different problem, which is to combine multiple existing shapes into a single shape.
Lastly, being able to assume that every CSS basic shape is a single, closed, contiguous shape involves fewer restrictions around what we can use shapes in CSS for.
Again, as a design principle, we don't add things for completeness or because they look cool, but because they are justified by use cases which are common and pervasive. I think we should first prove that subpaths pass that bar before we bake them into path()
simply because they exist in SVG.
Lines (L, H, V)
<line-path-command> = [ line ]? <path-endpoint> [ round <length-percentage> ]?
- In line with polygons, if the command name is omitted, it is assumed to be
line to
. This should also make it easier to convert a polygon to a path if we realize we need additional power - Single line command rather than three, H/V addressed via endpoint flexibility (see "General Syntax" above).
round
backported from [css-shapes] Allow optional rounding parameter forpolygon()
#9843
Arcs
<arc-path-command> = arc [
/* Compatibility with existing SVG arc syntax */
<path-endpoint> round <length-percentage>{1, 2} [ [ flip ]? && [large-angle]? && [ rotate <angle> ]? ]
/* New */
| [ at <length-percentage>{2} ] && <angle> ]
- Arcs have been a huge pain point when SVG is hand authored. User intent is typically to specify centerpoint and angle, but SVG paths require calculating the endpoint of the path instead. This arc syntax allows both.
- Boolean flags as keywords rather than obscure 0/1 flags
Curves (S, C, T, Q)
<curve-command> = curve <path-endpoint>
[ [ bezier | quadratic ] && [ control-points( <length-percentage>{2}#{2} ) ]? ]
- The "smooth" versions of curve commands (S, T) are automatically used when no control points are specified
- This syntax allows for smarter curves (e.g. splines) if only an endpoint is provided
Full Grammar
<path()> = path(
[ <'fill-rule'>? , <string> /* Existing syntax */
| <'fill-rule'>? at <length-percentage>{2} , <path-command># /* New syntax */
]
)
<path-command> = <line-path-command> | <arc-path-command> | <curve-command>
<path-endpoint> = [ to | by ] <length-percentage>{1, 2} [ down ]?
<line-path-command> = [ line ]? <path-endpoint> [ round <length-percentage> ]?
<arc-path-command> = arc [
/* Compatibility with existing SVG arc syntax */
<path-endpoint> round <length-percentage>{1, 2} [ [ flip ]? && [large-angle]? && [ rotate <angle> ]? ]
/* New */
| [ at <length-percentage>{2} ] && <angle> ]
<curve-command> = curve <path-endpoint>
[ [ bezier | quadratic ] && [ control-points( <length-percentage>{2}#{2} ) ]? ]
<shape-combine()> = shape-combine(<basic-shape>#)
Layering (MVP vs v1+)
Potential layering could involve:
- Keeping the MVP as syntactic sugar over a subset of SVG, and shipping the more substantial improvements (new arc command arguments, argument-less curve command) later.
- Shipping
<shape-combine()>
later
Issues
Relative vs absolute distinction
I have some reservations about using to
vs by
is natural enough.
But even if it is, when a command is relative in SVG, that affects all its parameters. Right now, the grammar is not making that clear — it looks as if it only affects the path endpoint. Do we even need that kind of fine grained control? In most cases a shape is either entirely absolute, or entirely relative (with an absolute start point). Perhaps absolute vs relative could be part of the preamble? Or even the function name?
Integration into SVG?
We could potentially integrate this back into SVG by introducing a set of child elements for <path>
, e.g. <line>
, <arc>
, <curve>
. <line>
is an existing element though I believe its API is compatible, so I see this as a nice synergy, not a conflict. Alternatively, a <segment>
element with an optional type
attribute (defaulting to line
).
@svgeesus might be able to provide sources, but I believe child elements for the path segments were discussed in the beginning as a more human friendly alternative and were not chosen only because compactness was considered critical at the time. 25 years later, the tradeoffs are different.