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] CSS flexibility for path()s (and let’s fix paths while we’re at it?) #9889

Open
LeaVerou opened this issue Feb 1, 2024 · 11 comments

Comments

@LeaVerou
Copy link
Member

LeaVerou commented Feb 1, 2024

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 for polygon() #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.

@Crissov
Copy link
Contributor

Crissov commented Feb 1, 2024

Isn’t this almost exactly what #5674 was about?

(I’m pretty sure I’m missing something essential here. It’s late.)

@LeaVerou
Copy link
Member Author

LeaVerou commented Feb 1, 2024

Isn’t this almost exactly what #5674 was about?

(I’m pretty sure I’m misting something essential here. It’s late.)

Hadn't seen that! For anyone else, this is the grammar #5674 resulted in: https://drafts.csswg.org/css-shapes-2/#shape-function

That’s great news, this means this proposal is a relatively smaller delta that can be used to improve the existing shape() function that’s already in shapes-2!

Lots of commonalities. I love that it also uses by | to, which I was a bit unsure about. I guess if we both ended up with the same prepositions that's a good signal they may actually be intuitive, but the concerns I expressed above still stand.

It does seem to follow SVG precedent a little more closely, whereas the syntax I’m proposing is a more thorough re-architecting, which seems even more appropriate if we're introducing a brand new function. From a quick look some differences are:

  • My proposal completely removes the move and close commands and introduces a separate function to combine shapes (for more motivation around that, read the proposal)
  • Instead of line-specific commands for horizontal and vertical lines, in my proposal all endpoints have a horizontal/vertical only syntax. Then all that's needed for h/v lines is the line command.
  • My proposal introduces an alternative arc syntax that follows author intent more closely by allowing them to specify the center and angle rather than the endpoint.
  • In my proposal, smooth curves are not a separate command, it's just curve [bezier | quadratic] without control points. Though I really like the idea that if only one control point is specified, the surve is automatically quadratic. I still think we should find a syntax that does it all under curve though, having a separate smooth command simply to omit arguments seems like a confusing mental model.

Some comments on the current shape() syntax:

  • cw and ccw violate the TAG principle against abbreviations
  • I’m worried <arc-size> being small | large makes no sense when you see something like arc to 1em 1em of 1em large.
  • I like via for control points, but with the current syntax they are just a series of <length-percentage> with no distinction between the y of one point and the x of the next (e.g. via 1em 2em 300px 40vmin). I think that should be clearly distinguished somehow, albeit not necessarily via a functional syntax (second keyword? Slash?).

@tabatkins
Copy link
Member

My proposal completely removes the move and close commands and introduces a separate function to combine shapes (for more motivation around that, read the proposal)

Your proposed ability to merge shapes-as-paths into larger paths is a good idea! I don't see why it would negate the use of move and close commands, tho. Sometimes it's easier and more straightforward to design things as a single path.

Instead of line-specific commands for horizontal and vertical lines, in my proposal all endpoints have a horizontal/vertical only syntax. Then all that's needed for h/v lines is the line command.

I'm not really a fan of this approach. I'm open to being convinced otherwise, but I'd prefer single-axis lines to be specific about whether they're horizontal or vertical, rather than defaulting to one.

My proposal introduces an alternative arc syntax that follows author intent more closely by allowing them to specify the center and angle rather than the endpoint.

I am absolutely in favor of adding more ways to specify arcs; I understand why SVG chose the representation it did (it lets them continue the "start point -> end point" theme all the other commands use, which makes their "you can omit command names" feature flow a little better), but I also think it's genuinely the worst way to specify arcs out of all the common reasonable ways.

But again, I don't see why we wouldn't expose SVG's exact arcs as well.

In my proposal, smooth curves are not a separate command, it's just curve [bezier | quadratic] without control points.

I can see either design as reasonable. I do prefer the current spec design, though: it doesn't require people to spell bezier or quadratic; I like the via <points> syntax slightly better than control-points(<points>); in general I think splitting the curves by "smooth" vs "not" is a more meaningful distinction than by "cubic" vs "quadratic" (SVG, of course, splits them by both axises, because they can't do optional values under their design constraints, but we can.)

Note, too, that you can't omit the control points in a smooth cubic; you get the first for free, but still need to specify the second. But that's just a grammar bug, not a substantive comment.

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?

In my personal experience writing SVG by hand, that "most cases" is definitely wrong. I mix absolute vs relative all the time, depending entirely on what's more convenient both for authoring in the moment and editting in the future. It indeed needs to be a per-command thing. (Sometimes it would be useful to be able to split it even finer, and do per-coordinate, but I think the syntax design needed for that to work would be terrible.)

cw and ccw violate the TAG principle against abbreviations

Sure, but clockwise and counter-clockwise are terribly long. We shouldn't optimize for length, but given the size that path data can sometimes get (even when hand-written), making sure it's not overly long is still important.

I’m worried being small | large makes no sense when you see something like arc to 1em 1em of 1em large.

It certainly make more sense than 0 or 1. ^_^ The SVG arc syntax is confusing, but there's not a lot we can do about that. I think small/large are the best way to spell this piece of functionality. Like I said earlier, introducing more ways to do arcs that aren't as confusing would be nice, but exposing all of the SVG path commands directly is useful in its own right.

@Crissov
Copy link
Contributor

Crissov commented Feb 2, 2024

Couldn’t agree more regarding better / more ways to express arcs.

  • cw 2, ccs 3
  • clockwise 9, counterclockwise 16
  • dextral 7, sinistral 9 😈

@LeaVerou
Copy link
Member Author

LeaVerou commented Feb 2, 2024

Thanks for the extensive response @tabatkins!

My proposal completely removes the move and close commands and introduces a separate function to combine shapes (for more motivation around that, read the proposal)

Your proposed ability to merge shapes-as-paths into larger paths is a good idea! I don't see why it would negate the use of move and close commands, tho.

I addressed this in the proposal, and just edited it a bit for clarity:

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.

[snip]

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.

Sometimes it's easier and more straightforward to design things as a single path.

I would love a few use cases where this is true!

I'm open to being convinced otherwise, but I'd prefer single-axis lines to be specific about whether they're horizontal or vertical, rather than defaulting to one.

I think you may have misunderstood, it’s not about lines defaulting to one, it's about endpoints having a single coord syntax for when they are only a horizontal/vertical offset. Lines just take an endpoint.

Horizontal and vertical lines are still lines. There is no reason for them to live under a separate command just so we could provide defaults to one of their parameters. I think it should be an explicit goal to keep the number of distinct commands small and easily memorable, and avoid having many different commands for similar but slightly different things, which the original SVG path syntax severely suffers from.

My proposal introduces an alternative arc syntax that follows author intent more closely by allowing them to specify the center and angle rather than the endpoint.

I am absolutely in favor of adding more ways to specify arcs; I understand why SVG chose the representation it did (it lets them continue the "start point -> end point" theme all the other commands use, which makes their "you can omit command names" feature flow a little better), but I also think it's genuinely the worst way to specify arcs out of all the common reasonable ways.

Yes, I see the rationale too, and agreed the ergonomics are terrible for human use.

But again, I don't see why we wouldn't expose SVG's exact arcs as well.

I think you may have missed that my proposal includes syntax for both. :)

In my proposal, smooth curves are not a separate command, it's just curve [bezier | quadratic] without control points.

I can see either design as reasonable. I do prefer the current spec design, though: it doesn't require people to spell bezier or quadratic; I like the via <points> syntax slightly better than control-points(<points>); in general I think splitting the curves by "smooth" vs "not" is a more meaningful distinction than by "cubic" vs "quadratic" (SVG, of course, splits them by both axises, because they can't do optional values under their design constraints, but we can.)

Note, too, that you can't omit the control points in a smooth cubic; you get the first for free, but still need to specify the second. But that's just a grammar bug, not a substantive comment.

Agreed on most of this.

  • I’m not a huge fan of control-points(), but note that the current syntax has the points specified as basically a series of <length-percentage> with no distinction of what is x and what is y, which will be quite hard to read (try it: via 1em 2em 300px 30vmin vs control-points(1em 2em, 300px 30vmin)). If we can find a non-functional syntax to distinguish the two points, that would be the best of both worlds (e.g. separate keywords? slash?).
  • Agreed that not requiring people to spell out bezier or quadratic is generally a good thing.

My most substantive objection to the current syntax would be that I think curve should be a single command (for the same reasoning as v/h lines above), and everything drawing curves should live under it.

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?

In my personal experience writing SVG by hand, that "most cases" is definitely wrong. I mix absolute vs relative all the time, depending entirely on what's more convenient both for authoring in the moment and editting in the future. It indeed needs to be a per-command thing. (Sometimes it would be useful to be able to split it even finer, and do per-coordinate, but I think the syntax design needed for that to work would be terrible.)

Fair enough.

cw and ccw violate the TAG principle against abbreviations

Sure, but clockwise and counter-clockwise are terribly long. We shouldn't optimize for length, but given the size that path data can sometimes get (even when hand-written), making sure it's not overly long is still important.

Agreed that ideally you want both terseness and readability, but when they conflict, readability is more important than length. Again, code is written once but read many times. Also remember that we only need the keyword to override the default. FWIW in my proposal I used flip for this, which does satisfy both.

I’m worried being small | large makes no sense when you see something like arc to 1em 1em of 1em large.

It certainly make more sense than 0 or 1. ^_^ The SVG arc syntax is confusing, but there's not a lot we can do about that. I think small/large are the best way to spell this piece of functionality. Like I said earlier, introducing more ways to do arcs that aren't as confusing would be nice, but exposing all of the SVG path commands directly is useful in its own right.

Sure, but these are not the only options. Again, we only need one keyword really, to override the default. In CSS color interpolation we use hue-longer. Perhaps something similar? arc-longer? angle-longer?

@fantasai
Copy link
Collaborator

fantasai commented Feb 6, 2024

General +1 to Tab's comment, also +1 to longer/shorter, and also +1 that 4 lengths in sequence is a bit hard to parse as two coordinates though I don't know how to solve that nicely. I think it's also interesting to compare the two proposals, both to see where they're the same and where they're different. :) In particular it seems like the arc syntax in shape() could use some improvements, and maybe it would be better to call it path() if it's defaulting to open paths.

@tabatkins
Copy link
Member

Again, as a design principle, we don't add things for completeness

The thing is, tho, we do add things for completeness sometimes, when their lack would be a surprise to authors. Strict adherence to "use-case or gtfo" can result in weird holes in APIs, and gratuitous differences between things that are intended to be very similar or even identical otherwise. Predictability is one aspect of usefulness in an API.

That's my feeling here. SVG paths have Move and Close, and these can be expressed in path(). shape() is intended to be a friendlier path() that adds additional powers without us being bound by the legacy restrictions of the SVG syntax. This implies that it should be able to do everything that a path() can already do; otherwise, you'll force people who just want to rewrite a path (to gain access to CSS units, for example) to much more substantially change their code, turning it instead into multiple shapes in a shape-combine().

Having the ability to write simple shapes using the existing shape functions, and merge them with a path or other shapes, is great. But forcing people to write it in that way, when the existing functionality to do it all in one path is right there, isn't.

I would love a few use cases where this is true!

The most obvious use-case is any time a relative move-to is convenient or meaningful. A fresh shape is effectively an absolute move-to, which might be annoying vs just saying "and then start the circle 10px to the side of this". (And I suspect it's not worthwhile to add syntax to allow shifting the coordinate space of another shape function.)

I think you may have misunderstood, it’s not about lines defaulting to one, it's about endpoints having a single coord syntax for when they are only a horizontal/vertical offset. Lines just take an endpoint.

No, I understood what you were referring to here, it's the idea that there's a meaningful default direction for a single length to be pointing in that I disagree with. I don't believe it's very readable to assume that line by 5px means "5px to the right". I prefer hline by 5px and vline by 5px much better, as they say right away that this'll be a horizontal or vertical line, so you know exactly what the length/coord is referring to. And for any other commands my belief about "single value meaning horizontal only" being non-obvious is even stronger.

I think it should be an explicit goal to keep the number of distinct commands small and easily memorable, and avoid having many different commands for similar but slightly different things, which the original SVG path syntax severely suffers from.

I agree the SVG path syntax suffers from this (see C/S/Q/T, for example), but I don't think that "minimal command set" should be a goal in and of itself. The command set should be the "right" size, using optionality and control parameters when it helps readability, but feeling free to introduce multiple commands when it helps instead.

[current curve via ... being 4 lengths in a row for cubic curves]

Yeah, that's not the best, but I also don't know what to do about it. I don't think control-points() makes it meaningfully better, tho, and it's so verbose to boot. I think 2 points in a row (4 lengths) is probably the most we should ever allow, tho.

Hm, a possibility - what if the coord-pair grammar was <len-per>{2} | point( <len-per>{2} )? That is, you could write via 1px 2px 3px 4px or via point(1px 2px) point(3px 4px), if that grouping makes it more readable. That's a similar length to control-points(1px 2px, 3px 4px), but makes it optional to use when the author decides it helps readability, without requiring the verbosity when you only have a single control point (as in quadratic curves, or smooth cubics). This would also let you use point() elsewhere in the commands when it makes things more readable there, too. (I'm unsure if I like this, fwiw. We'd also have to decide whether to serialize with or without it.)

I think curve should be a single command (for the same reasoning as v/h lines above), and everything drawing curves should live under it.

I'm not opposed to this. curve smooth ... isn't offensive, it's just a little longer, when part of the appeal of smooth curves is they're even shorter to write than non-smooth ones. ^_^ Then the number of control points still serves to distinguish cubic vs quadratic: without smooth you have to give two or one control points, with smooth you have to give one or zero (omitting the via entirely).

(Or maybe the curve command's name is [curve || smooth], so you can specify just smooth if you want, or both if you want to be explicit? smooth curve to ... would also then be allowed, which reads nicely too.)

Perhaps something similar? arc-longer? angle-longer?

We're already in the context of the arc command here. The entire concept of choosing between the four possible arcs is already confusing as hell; I don't think making the name longer makes it any easier to understand or place in context.

I think it might be reasonable to make the size keyword non-optional, in fact; while CSS does have a preference for cw/ccw direction, it doesn't generally have a preference for large vs small things. Forcing it to be always be present ensures that people will have some idea of what it does in order to even write the command.

FWIW in my proposal I used flip for this, which does satisfy both.

If we default to the clockwise arc pair, in accordance with CSS's general preference for clockwise angles, then using flip to mean the counter-clockwise seems reasonable to me, and does avoid both the length issues of clockwise/counter-clockwise and the abbreviation issues of cw/ccw.

@tabatkins
Copy link
Member

tabatkins commented Feb 8, 2024

(See next comment for a much shorter actual proposal; this comment is noodling in the problem space.)

So, let's talk about better circle commands, since SVG arcs are, indeed, terrible.

  • In SVG, you provide the size of the circle, and two points on the perimeter (your starting point and your ending point). The UA then figures out where it needs to place the circle to make that work (there are always two locations), and then you select between the four possible arcs thus defined (which location, and which half of the circle at that location). Total necessary information: one coord, one radius (or two radiuses + a rotation), and then two binary flags for the arc choice.

    Pros: no need to calculate the circle center yourself, which can be complicated trig. Fits into the SVG path model of every command providing an explicitly-stated endpoint for the next to pick up from (tho why that is necessary, I have no idea).

    Cons: you need to calculate the endpoint yourself, which can be complicated trig. It's very sensitive to small numerical errors if you try to draw nearly a whole circle; actually drawing a whole circle in on command is impossible, and you need to do two half-circles instead. The arc choices are pretty confusing.

  • Lea suggests a very simple command: you provide the center of the circle, and it automatically gets a radius sufficient to put the starting point on the perimeter. You then provide the angle sweep you want to make with the arc, either positive (clockwise) or negative (counter-clockwise); the endpoint is automatically calculated as the end of that arc. Total necessary information: one coord, one angle.

    Pros: Minimal data. If you know you want a quarter-circle or something, very easy. Can trivially do an entire circle.

    Cons: Need to calculate the center and angle, which can be complicated trig. Endpoint isn't obvious (if you want to know where it is, you have to do trig.)

    As described, Lea's command can't do ellipses, and since the radius is auto-calculated, there's not a clean way to slot that in anyway. Maybe if you provide the eccentricity, or the major/minor axis ratio? Plus a rotation. That would be a little awkward, but possible.

  • <canvas> defines arc() and ellipse() commands, which are basically more explicit variants of Lea's idea. You provide a center point, a radius (or two radiuses + a rotation), and both a starting and ending angle to sweep between. If your starting point isn't already at the indicated start of your arc, it draws a straight line to there, then sweeps the indicated arc. Total necessary information: one coord, one radius (or two radiuses + a rotation), and two angles.

    Pros: Can do ellipses as easily as circles. Can draw any arbitrary segment of a curve without having to calculate either endpoint. Can draw an entire circle.

    Cons: Need to calculate the center and angles, which can be complicated trig. Endpoint isn't obvious. Can draw more than just the desired circle, if your starting point isn't already on the arc.

  • <canvas> also has an arcTo() command, which is better thought of as a "round corner" command. You provide two points (defining a line from your starting point to the first, and then from the first to the second), and a radius. The UA auto-calculates the position of a circle of the given radius that is tangent to the given lines, and draws the shorter arc from its two contact points with the lines (the arc closer to the corner). Total necessary information: two coords and a radius.

    Pros: Makes rounded corners of a given radius extraordinarily trivial, just need the coord of the corner you're rounding, and some point on the next side. Can draw an entire rounded rect with four commands, using only the four corners and the desired corner rounding; more complex shapes like a rounded star just require the star points, no need for trig to precisely place the arcs themselves.

    Cons: Can draw more than just the desired circle, if your starting point isn't already on the circle. As defined by HTML, can do a funky backwards line, since the radius isn't clamped. Endpoint isn't obvious (and the "endpoint" you provide for the line isn't necessarily relevant). Have to make sure your starting point is on the line before the curve connects, or you'll get a weird segment. Not a general-purpose arc-drawer.

    As written, doesn't allow ellipses, but it's fairly straightforward to allow two radius and a rotation. To match the ease-of-use, you probably want a few auto-rotate options, too - align first radius's axis with first line, with second line, or with corner bisector.

    The ability for the arc to start further back than your starting point is also kinda weird. We probably want to allow a clamp, so the radius will be reduced if it would get further from the corner than the start point. And, optionally, the second point.


Okay, so I think all of these are pretty reasonable; three of them already exist on the web platform, so supporting them has a good consistency argument, and the variant proposed by Lea is a nice simplification of one of them.

Let's talk grammars. We already have a grammar for the SVG arcs:

arc <by-to> <coordinate-pair> of <length-percentage>{1,2}
    [ [cw|ccw] || [small|large] || rotate <angle> ]?

Tho with the discussion here, we might want to fix it up a bit to:

arc <by-to> <coordinate-pair> of <length-percentage>{1,2}
    [ flip? || [small|large] || rotate <angle> ]

(Tho both of these allow specifying some ellipse-specific options when doing a circle. Maybe instead:

arc <by-to> <coordinate-pair>
   [ of <length-percentage>    [ flip? || [small|large] ]
   | of <length-percentage>{2} [flip? || [small|large] || rotate <angle> ]]

)

Lea's simple arc is:

arc2 <by-to> <coordinate-pair> <angle>

HTML's arc()/ellipse() are:

circle <by-to> <coordinate-pair> of <length-percentage> <angle>{2}

ellipse <by-to> <coordinate-pair> of <length-percentage>{2} [rotate <angle>]? <angle>{2}

HTML's arcTo() is:

corner <by-to> <coordinate-pair> via <coordinate-pair> <length-percentage>

And expanded to be elliptical:

corner <by-to> <coordinate-pair> via <coordinate-pair> 
       <length-percentage>{2} [rotate [ <angle> | auto-first | auto-second | auto-center ]]?

Lea's suggested arc looks to be doable just as a subset of circle, aka a grammar like:

circle <by-to> <coordinate-pair>
     [ of <length-percentage> <angle>{2}
     | <angle> ]

We might want to make the "straight line to the start of the arc" for circle/ellipse/corner skippable,
so you just get the arc itself.
Like a skip-line keyword.


In theory, there are more possible ways to define circle segments, but these are the three ways that already exist on the web platform (and thus have consistency as a justification), plus Lea's reasonable simplification of one of them. I don't think we should go any further without strong use-cases; we can easily add shortcut commands all day if we don't control ourselves.

@tabatkins
Copy link
Member

tabatkins commented Feb 8, 2024

OKAY. All together, I suggest then that our circle/ellipse command set be:

arc <by-to> <coordinate-pair> of <length-percentage>{1,2}
    [ flip || [small|large] || rotate <angle> ]?

circle <by-to> <coordinate-pair>
     [ of <length-percentage> [ <angle>{2} || skip-line ]
     | <angle> 
     ]

ellipse <by-to> <coordinate-pair> 
        of <length-percentage>{2} 
        [ [rotate <angle>] || <angle>{2} || skip-line ]

corner <by-to> <coordinate-pair> via <coordinate-pair>
     [ of <length-percentage> [ [clamp [start|end|both]?] || [skip-line [start|end|both]?] ]
     | of <length-percentage>{2} 
       [ [rotate [ <angle> | auto-first | auto-second | auto-center ]] 
       || [clamp [start|end|both]?]]
       || [skip-line [start|end|both]?]
       ] 
     ]
  • arc is as defined already, the only change is that we're changing from [cw | ccw] to flip?
  • circle defines a circle with the given center.
    • You can specify the radius explicitly, along with start/end angles (0deg = top of circle), and whether to draw the line to the from the start point to the beginning of the arc. If angles are omitted, draws a full circle, starting/ending at the point closest to the start point.
    • Or you just let the radius and start angle be auto-calculate to make the arc begin at the start point, and just specify the sweep of the arc.
  • ellipse is the same as circle, but with two radiuses and a rotation added. No shorthand form to auto-calculate radius.
  • corner defines a rounded corner of the given radius. If elliptical, can also rotate, with some auto-rotate options to align the ellipse with the first or second line, or the bisector of the lines. You can clamp the radius to not exceed the start/end/both points (default to both), and skip the line from the starting point to the curve and/or from the curve to the ending point (if totally omitted, defaults to skip-line end; if skip-line is specified but with choice omitted, defaults to skip-line both)

I'm slightly uncomfortable with the fact that circle/ellipse/corner all use <by-to> <coordinate-pair> for things that aren't the endpoint; in English it reads slightly oddly that circle to 50px 100px doesn't end at (50px, 100px). But I think that the benefit of making this read better are far outweighed by the downside of using different terms that mean the same thing.

@tabatkins
Copy link
Member

One thing to note is that, since SVG commands all rquire an explicit endpoint, you can always "skip" a command by just changing to a move command with the same endpoint, achieving the equivalent of a "pen up" command. This isn't possible if we introduce commands without explicit endpoints, like circle/ellipse/corner; you'd have to do the trig yourself to find the endpoint. We should fix this, by allowing any command to be prefixed with move to make it just move to the endpoint of the given command without drawing. Like, move circle ... to move the pen to whereever that circle happens to stop at.

This also fixes the existing problem in HTML where, if you do want to draw a rounded rect using just four arcTo() commands (or any other polygon with rounded corners), you have to first identify a spot you know is on the straight segment preceding the curve and move to it, which again might require trig, and then end with an additional line to that point to finish the straight side. With this move prefix, you could easily do it without any math - just start with the last corner being a move, and then go thru the whole shape.

Like:

/* 100x100 square, with 5px corner rounding */
shape(
	move corner to 0px 0px of 5px, 
	corner to 100px 0px of 5px, 
	corner to 100px 100px of 5px, 
	corner to 0px 100px of 5px, 
	corner to 0px 0px of 5px)

That initial move corner would start you at the end of the 0,0 curve, so your first real corner command draws the entire straight side, then the remaining commands finish the rect. The same would apply to any other polygon/star/etc: just repeat the final corner command as an initial move corner command.

While only circle/ellipse/corner need move-prefix support, for simplicity it would be good to support it for all the commands. For everything else it just ignores everything about the command except the ending point.

@DavidJCobb
Copy link

I’m not a huge fan of control-points(), but note that the current syntax has the points specified as basically a series of <length-percentage> with no distinction of what is x and what is y, which will be quite hard to read (try it: via 1em 2em 300px 30vmin vs control-points(1em 2em, 300px 30vmin)). If we can find a non-functional syntax to distinguish the two points, that would be the best of both worlds (e.g. separate keywords? slash?).

For sequences of multiple control points, why not just say via again? via 3px 5px via 6px 7px? Shorter than functional notation, still explicit, and avoids burning another keyword (e.g. and).

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

No branches or pull requests

5 participants