whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
8.03k stars 2.63k forks source link

Canvas stroke(): Subpaths of length 0 should render their line caps. #1079

Open junov opened 8 years ago

junov commented 8 years ago

The spec is currently inconsistent with itself.

Step 3 of the "trace a path" algorithm says: "Remove from path any subpaths containing no lines (i.e. subpaths with just one point)."

In step 9, in the part that explains line caps, it says: "Points with no lines coming out of them must have two caps placed back-to-back as if it was really two points connected to each other by an infinitesimally short straight line in the direction of the point's directionality (as defined above)."

Points with no lines coming out of them should never occur at step 9 since those subpaths are supposed to be pruned in step 3. This is confusing.

Currently, implementations are not compatible. Gecko and Edge will draw back to back line caps for a zero-length subpath WebKit and Blink draw nothing.

From developer feedback on the chromium issue tracker, it seems that drawing back to back line caps is desired behavior: https://bugs.chromium.org/p/chromium/issues/detail?id=577655 Blink is being changed to match the behaviours of Gecko and Edge.

Any objections to removing step 3 of the "trace a path" algorithm? https://html.spec.whatwg.org/multipage/scripting.html#trace-a-path

junov commented 8 years ago

Also, rendering back to back lineCaps on zero-length subpaths would match the behavior of SVG path rendering.

domenic commented 8 years ago

It sounds like there's a compelling argument in favor of removing that step, so works for me. It would be good to get a WebKit canvas person's take to make sure they are OK eventually aligning with the majority and with the proposed spec, but I don't think that should block a PR.

junov commented 8 years ago

I took a closer look at this to make sure there aren't any weird edge case side-effects from removing step 3:

If we want to match SVG, this is what it says for square line caps: "If a subpath has zero length, then the resulting effect is that the stroke for that subpath consists solely of a square with side length equal to the stroke width, centered at the subpath's point, and oriented such that two of its sides are parallel to the effective tangent at that subpath's point. See ‘path’ element implementation notes for details on how to determine the tangent at a zero-length subpath." [1] I followed the link to the implementation notes [2] and was unable to find anything relevant to "effective tangents" on that page.

Summoning @dirkschulze : Help!

References: [1] https://www.w3.org/TR/svg-strokes/#LineCaps [2] https://svgwg.org/svg2-draft/implnote.html#PathElementImplementationNotes

annevk commented 6 years ago

cc @whatwg/canvas

annevk commented 6 years ago

@AmeliaBR could you maybe help with the SVG question in https://github.com/whatwg/html/issues/1079#issuecomment-212626882 or know someone who can?

AmeliaBR commented 6 years ago

The SVG behavior is that a zero-length path segment is drawn with line caps, but a single point (e.g., created by two consecutive move-to commands) is not.

There's a detailed algorithm in SVG 2, and it includes some edge case details (like what is the direction of a zero-length path, and what to do with a zero-length close path) that have been clarified since SVG 1.1. Browsers are mostly consistent, but an updated test suite for all the edge cases is still required.


Current behavior:

Example showing the same path in SVG and Canvas with square line caps: https://codepen.io/AmeliaBR/pen/KQKjXJ

The path includes:

As expected, all browsers draw line-caps for most zero-length SVG path segments, but with some quirks (Edge skips the zero-length arc segment, and Safari draws two double-linecap squares at different orientations for each--not sure what's going on there.)

Chrome and Safari both draw line-caps for the zero-length path segments in canvas (although they disagree on orientation & for reasons I don't understand Safari initializes a canvas to solid black).

Firefox and Edge do not draw any of the zero-length segment linecaps in canvas, conforming with the current canvas spec. (Note that I'm using a polyfill for Edge; the thin black square confirms that it can draw from a path data string.)


My opinion:

I believe it would be preferable to authors (and maybe even convenient for implementers) if canvas stroking behavior was consistent with SVG behavior, unless there is a legacy reason to maintain a difference. Since some major implementations do not match the current canvas spec, I doubt there would be a significant compat issue with changing it.

annevk commented 6 years ago

I tend to agree. (I think it would be great if eventually SVG and canvas could use a shared underlying model for paths, instead of having separate descriptions that are somehow magically intertwined (canvas accepts SVG paths after all). CSS needs this too to define its various path-related things, some of which seemingly accept SVG path syntax already.)

fserb commented 3 years ago

bumping this, as we seem to have stumbled upon a similar problem. It seems that the current browser behavior is non compatible. As Firefox is doing what SVG does (drawing), while Chrome only draws if there's a moveTo/lineTo to the same point, while Safari never draws.

We are planning to change behavior to match Firefox and SVG.

jfkthame commented 1 year ago

bumping this, as we seem to have stumbled upon a similar problem. It seems that the current browser behavior is non compatible. As Firefox is doing what SVG does (drawing), while Chrome only draws if there's a moveTo/lineTo to the same point, while Safari never draws.

We are planning to change behavior to match Firefox and SVG.

@fserb I notice that in https://bugs.chromium.org/p/chromium/issues/detail?id=644067, this behavior was recently changed in Blink -- but not in the way you suggest here, rather the opposite. I think that's regrettable, as it's a significant step away from the goal of a common canvas2d/svg rendering model that people in the discussion here seemed to agree was desirable.

Can you perhaps raise this with the concerned engineers/teams, and maybe get people to reconsider that change?

Kaiido commented 1 year ago

I believe the original comment was a bit mislead.

Points with no lines coming out of them should never occur at step 9 since those subpaths are supposed to be pruned in step 3. This is confusing.

In the previous step (8), the line dash algorithm can create such points, and these are the ones that step 9 has to handle.

So the specs made sense, but the current behavior is anyway too far from being interoperable even today, and I won't be arguing that keeping the step 3 is the best move. I guess going as close as possible to SVG would be good, but most implementations are relatively far from it.

I ran a few quick tests to see how bad the discrepancies are between implementations (along with their SVG counter parts), and it's pretty bad. Even with the latest changes done in Chrome Canary. Also even SVGs implementations aren't really 100% interoperable in the way they render line-dashes. I didn't check what SVG specs really ask there. And my reading of the canvas specs might be mislead too in some cases.

Toggle to see the results: ### [JSFiddle](https://jsfiddle.net/ur5cawot/) | [Graphic results](https://github.com/whatwg/html/assets/7843391/d7726a5c-7803-4fcd-972e-79653dd9b91c) ## Rendering of zero-length sub-paths ### moveTo (✅) Removed everywhere, in accordance with the specs, step 3. ### moveTo + moveTo (✅) Removed everywhere, in accordance with the specs, step 3. ### moveTo + lineTo (❌) Kept in Chrome stable & Safari - Removed in Firefox and Canary - Kept in SVG everywhere. Specs ask to prune zero-length line segments. Not 100% sure but I take it to mean this should not be rendered, in contradiction with apparent SVG behavior. ### moveTo + closePath (❌) Kept in Chrome stable - Removed in Firefox, Safari, and Canary - Kept in SVG everywhere. Specs ask to remove single points subpaths, should not render in contradiction with apparent SVG behavior. ### moveTo + moveTo + closePath (❌) Same as *moveTo + closePath*. ### moveTo + lineTo + closePath (✅*) Same as *moveTo + closePath*. Regarding the specs there is the same reserves as with *moveTo + lineTo*. ### moveTo + lineTo(1) (✅) Kept everywhere. Line cap is drawn on both sides of the point. ### moveTo + lineTo(1) + closePath (✅) Kept everywhere. Line cap is not drawn on any side of the point. (Safari has a weird 1px dot in the middle, in both canvas and SVG). According to the specs, merged points of an enclosed path should be converted to joins (step 5) so no line cap should be drawn. ### moveTo + lineTo + draw more (❌) Kept in Chrome stable and Canary. Removed in Firefox and Safari. Kept in SVG everywhere. Specs ask to remove single points subpaths, should not render in contradiction with apparent SVG behavior. *Note*: Canary's behavior is not consistent with itself in the simpler *moveTo + lineTo* test case, while all other UAs are. ### lineTo x4 + closePath + moveTo + closePath (❌) Kept in Chrome stable, Canary, and Firefox... Removed in Safari. Kept in SVG everywhere. Specs ask to remove single points subpaths, should not render in contradiction with apparent SVG behavior. *Note*: Firefox is not consistent with itself in the simpler *moveTo + lineTo + draw more*. I have no clue why to be honest... ___ ### lineTo x4 + closePath lineDash([0, width]) (✅) Kept everywhere. (Tests single points segments are correctly rendered when they're created from the stroke-dash algorithm). ____ ## Rendering of line-dash caps ### lineTo x4 + closePath lineDash([length, 0]) (✅*) Only the top left corner has its cap rendered everywhere. Firefox's SVG does not render the top-left cap. The specs ask that all the joins are removed and two points are added at the start and end position of the subpath. ### lineTo x4 + closePath lineDash([length, length]) (❌) Chrome (stable and Canary), and Safari render the top left corner's cap. Firefox doesn't. Same varying results in SVG. Should be the same as *lineTo x4 + closePath lineDash([length, 0])* ### repeat2(lineTo x4 + closePath) lineDash([length / 2, length / 2]) (✅) Top-left corner's cap is rendered everywhere, even in Firefox, even in SVG. ### lineTo x4 + closePath lineDash([width, 0]) (❌) Top-left corner's cap is not rendered in Chrome (stable & Canary) in both canvas and SVG. Rendered in Firefox and Safari (canvas & SVG). I think it's supposed to be rendered. ### lineTo x1 + closePath lineDash([length, 0]) (✅*) Left cap is rendered everywhere, but in Firefox's SVG. Right cap is not rendered anywhere. (Safari has a weird 1px glitch in the middle). The line-dash cuts the enclosed path at the beginning. ### lineTo x1 + closePath lineDash([length / 2, 0]) (❌) Left cap is not rendered in Chrome (both) and Firefox, both in Canvas and in SVG. Safari renders it. Right cap is rendered everywhere, but in Firefox's SVG. A bit unsure on this one, but I'd expect the line-dash to cut the path in both ends and thus to have two caps. ### lineTo x4 lineDash([length / 4, length / 4]) (✅*) Renders caps on both ends of both segments everywhere, but with a compositing bug in Safari (looks like the stroke filling crosses itself).
yiyix commented 1 year ago

Hi Kelsey, @kdashg, since this discussion is generally positive to render the line end-caps for empty paths, I am tempting to change the spec to reflect it (#9663). What's the firefox perspective on it?

Note that Firefox's current behavior almost matches with the spec after change (https://wpt.fyi/results/html/canvas/offscreen/path-objects?label=master&label=experimental&aligned&q=2d.path.stroke.prune, failling the test meaning draw the line caps).

kdashg commented 1 year ago

Mozilla think this is a good idea to standardize on yes-drawing. When we tried to match spec with this, we had web-compat issues and reverted to our draw-zero-subpath-end-caps behavior.

Edge and Safari will begin to fail these tests if this change is made. Would you update behavior to match a changed spec (and tests), @annevk (Apple) and @RafaelCintron (Microsoft)?

annevk commented 1 year ago

I discussed this with colleagues and it seems reasonable to us.