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

SVG marker orientation at a 180° U-turn not well defined #333

Open
AmeliaBR opened this issue Jul 15, 2017 · 14 comments
Open

SVG marker orientation at a 180° U-turn not well defined #333

AmeliaBR opened this issue Jul 15, 2017 · 14 comments

Comments

@AmeliaBR
Copy link
Contributor

AmeliaBR commented Jul 15, 2017

SVG 2 provides clear a definition of the "Path Directionality", which should help define the correct behavior of auto-orienting markers in most cases.

However, for markers that are not at the start or end of an open sub-path, the current SVG 2 guidance says:

Otherwise, the marker is oriented in a direction half way between the direction at the end of the preceding segment and the direction at the start of the following segment.

I think that "half way between" needs to be defined more explicitly. Browsers are currently inconsistent about what that means when the start and end tangent angles are the same (that is, if the path does a U-turn).

Consider this test case (CodePen link):

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 10">
  <marker id="m" overflow="visible" orient="auto">
    <polygon fill="red" fill-opacity="0.8" points="-2,0 0,-4 2,0" />
  </marker>
  <path fill="none" marker-mid="url('#m')" stroke="navy" 
             d="M 0 3 Q 10 0 10 6 Q 10 0 20 3" />
</svg>

(Edited. See revised screenshots in #333 (comment))

@DavidBruchmann
Copy link

DavidBruchmann commented Jul 15, 2017

I see here two and not only one problem:

  • the form (here triangle) can be turned around or not, so either the peak is showing to the top or to the bottom.
  • the midpoint of the triangles differs in both testcases. it might be related to a rotation around the endpoint of the path and so reasonable related to the rotation but had to be verified and stated like that then.

Another question is, what is the code exactly "ordering"?

  • The initial state of the triangle is showing up?
  • The path makes it moving, but also turning (like mentioned in the headline)?
  • Where the midpoint of the rotation shall be located (not to be confused with the midpoint of the triangle)?

@BigBadaboom
Copy link
Contributor

BigBadaboom commented Jul 15, 2017

The two sections contradict one another. According to the definition in "9.4 Path directionality", it should be pointing up. But according to "13.7.4. Rendering markers" it should be pointing either to the left or the right up or down. (Correction: I assumed the marker was pointing in the path direction).

If a line makes a 20deg change in direction. It makes sense for the marker to be rotated 10deg. That would also be perfectly fine behaviour for motion along a path.

But the 9.4 behaviour is not as suitable to markers as it would be for motion.

@AmeliaBR
Copy link
Contributor Author

the midpoint of the triangles differs in both testcases. it might be related to a rotation around the endpoint of the path and so reasonable related to the rotation but had to be verified and stated like that then.

I don't see this. The (0,0) reference point of the marker is the middle of the base of the triangle, and this is correctly aligned with the marked point in both cases.

Another question is, what is the code exactly "ordering"?

When a marker is orient="auto" the positive x-axis of the marker is re-oriented to match the direction of the path. For this marker, that means that the triangle will point up if the path is going left to right, and will point down if the path is going right to left. I have modified the path in the CodePen to include some smooth mid-points so this is more clear (on the left), and also some more typical corners (on the right).

But of course, in the center point, the path is going neither left-to-right nor right-to-left. It is going straight down and then straight back up again. The spec says to take an angle halfway between those two, but it doesn't say whether "halfway" should be measured clockwise or counterclockwise.

I also modified the CodePen to create a mirror-image path, one where the mid-point has an incoming segment going straight up and an outgoing segment straight down. I'm glad I did. It turns out that the non-MS browsers aren't as consistent as I first thought. Firefox flips the direction of the mid-point marker, but Chrome, Safari, and Inkscape do not.

So here's my revised test-case code

<svg xmlns="http://www.w3.org/2000/svg" 
     viewBox="0 0 20 20"
     width="400" height="400">
  <marker id="m"
          overflow="visible" 
          orient="auto">
    <polygon fill="red" fill-opacity="0.8"
             points="-2,0 0,-4 2,0"/>
  </marker>
  <path fill="none" stroke="navy"
        d="M0,5 L2,4 
           Q10,0 10,6
           Q10,0 18,4
           L19,8"
        marker-mid="url(#m)" />
  <path fill="none" stroke="seaGreen"
        d="M0,15 L2,16 
           Q10,20 10,14
           Q10,20 18,16
           L19,12"
        marker-mid="url(#m)" />
</svg>

And here are the three different renderings:

MS Edge (also IE, except that IE also draws strokes on the markers because of completely unrelated bug):

Two mostly-horizontal curved paths, with the path shapes mirror images of each other, reflected top-to-bottom. Both pinch together into a vertical point in the middle and also have a sharp bend on the right.  There are red triangle markers on the left smooth curve, at the pinch point, and at the sharp bend.  Both center markers point down; other markers all point mostly up, though slightly angled.

Firefox:

The same shapes, except that one marker points up and the other points down.

Inkscape (also Chrome and Safari):

The same shapes, except that both markers point up.

@BigBadaboom
Copy link
Contributor

I am sure we are spending an unnecessary amount of time thinking about something no-one will ever notice, but...

May I suggest reorienting your arrow so it points along the X axis. I think it makes the directions more intuitive.

Also I think that for completeness, the test should also include right-to-left versions of the lines. Ideally the angle of the marker should make sense given the overall direction of the line.

<svg xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 20 40"
     width="250" height="500">
  <marker id="m"
          overflow="visible"
          orient="auto">
    <polygon fill="red" fill-opacity="0.8"
             points="0,-1, 3,0, 0,1"/>
  </marker>
  <!-- ltr "M" shape -->
  <path fill="none" stroke="navy"
        d="M0,5 L2,4
           Q10,0 10,6
           Q10,0 18,4
           L19,8"
        marker-mid="url(#m)" />
  <!-- ltr "W" shape -->
  <path fill="none" stroke="seaGreen"
        d="M0,15 L2,16
           Q10,20 10,14
           Q10,20 18,16
           L19,12"
        marker-mid="url(#m)" />
  <!-- rtl "M" shape -->
  <path fill="none" stroke="navy"
        d="M20,25 L18,24
           Q10,20 10,26
           Q10,20 2,24
           L1,28"
        marker-mid="url(#m)" />
  <!-- rtl "W" shape -->
  <path fill="none" stroke="seaGreen"
        d="M20,35 L18,36
           Q10,40 10,34
           Q10,40 2,36
           L1,32"
        marker-mid="url(#m)" />
</svg>

Edge:

markers_edge

Firefox:

markers_firefox

Chrome/Safari/Inkscape

markers_chrome

Ideally the orientation would be as follows I think:

markers_ideal

@BigBadaboom
Copy link
Contributor

In 2010, this topic was discussed on the mailing list. Discussion petered out without any conclusion.

https://lists.w3.org/Archives/Public/www-svg/2010Oct/0033.html

@AmeliaBR
Copy link
Contributor Author

Thanks for finding that link, Paul. Also thanks for the better demo. I guess my brain still thinks better in "up" vs "down" than "in the direction of the positive X-axis". But the latter version is what's important for markers.

However, your "ideal" solution seems to assume that there is an obvious directionality to the path as a whole. What about a talon-shape, where the curve backtracks. At the exact point of tangent, there is no obvious directionality: incoming and outgoing paths cancel out.

Another demo

I am sure we are spending an unnecessary amount of time thinking about something no-one will ever notice, but...

I noticed this in a demo I was making, when I opened it up in a different browser & things were completely different. It's one of those things where I don't really care one way or the other, just so long as everyone is consistent!

About the only thing I'm going to argue for is that mirror-reflected path definitions should have mirror-reflected marker orientations. (AKA, Firefox is wrong).

@BigBadaboom
Copy link
Contributor

BigBadaboom commented Jul 20, 2017

I have been experimenting with various algorithms to try and improve the direction calculations at these 180° turn sites. I've come up with what I think is a reasonable solution that can be implemented in just a handful of lines of code.


The algorithm

If the incoming and outgoing vectors at a midpoint are anti-parallel, there will be two possible options for an average vector. They are the two perpendiculars. To select between the two:

  1. Let Va be a vector from the last marker location (last mid point or start point, whether displayed of not) to the current marker location.
  2. Test against Va. Choose the potential marker direction that points in the same direction as Va. The vectors point in the same direction if the dot product is greater than zero.
  3. If the dot product is zero (Va is perpendicular), then let Vb be a vector from the current marker location to the next marker location (next mid point or end point, whether displayed of not).
  4. Test against Vb. Choose the potential marker direction that points in the same direction as Vb.
  5. If that dot product is also zero (Vb is perpendicular), then choose the potential marker direction that has a positive X coordinate. If X is zero, then choose the direction that has a positive Y coordinate.

I've implemented this algorithm in my renderer. It added only about a dozen lines to my marker code. Here's the result:

improved_markers

It's not perfect, but I think it improves things a lot. There are are few of what I consider "failures" there: the third one down in the second column, and the second down and bottom ones in the third column.
But fixing every scenario would add a lot more complexity to the algorithm.

Here's the test file if you want to see the difference on other browsers:

<svg xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 70 50"
     width="700" height="500">
  <marker id="m" overflow="visible" orient="auto">
    <polygon fill="red" fill-opacity="0.8" points="0,-1, 3,0, 0,1"/>
  </marker>
  <marker id="se" overflow="visible" orient="auto">
    <polygon fill="gold" fill-opacity="0.8" points="0,-1, 3,0, 0,1"/>
  </marker>
  <!-- ltr "bird" shape -->
  <path fill="none" stroke="navy"
        d="M 5 6 q 6 -3 6 3 q 0 -6 6 -3"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- ltr "inverted bird" shape -->
  <path fill="none" stroke="navy"
        d="M 5 18 q 6 3 6 -3 q 0 6 6 3"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- ltr "T" shape -->
  <path fill="none" stroke="navy"
        d="M 24 3 l 5 5 l -5 5 l 5 -5 l 5 5"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- ltr "M" shape -->
  <path fill="none" stroke="navy"
        d="M 24 21 c 0 -6 6 -6 6 0 c 0 -6 6 -6 6 0"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- ltr "claw" shape -->
  <path fill="none" stroke="navy"
        d="M 45 12 c 0 -3 6 -3 6 0 q 0 -6 -6 -6"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- ltr "claw" shape drawn top first -->
  <path fill="none" stroke="navy"
        d="M 45 15 q 6 0 6 6 c 0 -3 -6 -3 -6 0"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- ltr "slash" shape -->
  <path fill="none" stroke="navy"
        d="M 58 10 l 6 -6 l -6 6"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- ltr "backslash" shape -->
  <path fill="none" stroke="navy"
        d="M 58 14 l 6 6 l -6 -6"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- ltr horizontal line shape -->
  <path fill="none" stroke="navy"
        d="M 58 22 l 6 0 l -6 0"
        style="marker: url(#se); marker-mid: url(#m);" />

  <!-- rtl "bird" shape -->
  <path fill="none" stroke="seaGreen"
        d="M 17 28 q -6 -3 -6 3 q 0 -6 -6 -3"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- ltr "inverted bird" shape -->
  <path fill="none" stroke="seaGreen"
        d="M 17 40 q -6 3 -6 -3 q 0 6 -6 3"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- rtl "T" shape -->
  <path fill="none" stroke="seaGreen"
        d="M 36 35 l -5 -5 l -5 5 l 5 -5 l -5 -5"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- rtl "M" shape -->
  <path fill="none" stroke="seaGreen"
        d="M 36 43 c 0 -6 -6 -6 -6 0 c 0 -6 -6 -6 -6 0"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- rtl "claw" shape -->
  <path fill="none" stroke="seaGreen"
        d="M 51 34 c 0 -3 -6 -3 -6 0 q 0 -6 6 -6"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- rtl "claw" shape drawn top first -->
  <path fill="none" stroke="seaGreen"
        d="M 51 37 q -6 0 -6 6 c 0 -3 6 -3 6 0"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- rtl "slash" shape -->
  <path fill="none" stroke="seaGreen"
        d="M 64 32 l -6 -6 l 6 6"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- rtl "backslash" shape -->
  <path fill="none" stroke="seaGreen"
        d="M 64 36 l -6 6 l 6 -6"
        style="marker: url(#se); marker-mid: url(#m);" />
  <!-- rtl horizontal line shape -->
  <path fill="none" stroke="seaGreen"
        d="M 64 45 l -6 0 l 6 0"
        style="marker: url(#se); marker-mid: url(#m);" />
</svg>

https://jsfiddle.net/rchpq06L/7/

Test file in Chrome

chrome

Test file in Firefox

firefox

@BigBadaboom
Copy link
Contributor

What about a talon-shape, where the curve backtracks. At the exact point of tangent, there is no obvious directionality: incoming and outgoing paths cancel out.

Maybe not obvious, but I think that natural tendency of people would be to follow the outside of the shape. So you would turn away from the last point you were at, and towards the next point. So in the my test file above, you would turn left for the top blue talon shape, and right for the one below it.

@karip
Copy link

karip commented Jul 28, 2017

A note about motion paths: the orientation at motion path vertices should not be halfway between directions. The orientation at a vertex is drawn only for one frame, when the motion path is sampled at the vertex position. Using a halfway value would cause undesirable flickering.

The CSS Motion Paths spec was recently changed to define the orientation at sharp corners. The orientation of the preceding segment is used: "If the offset path is composed of multiple line segments, the orientation at the connection between the segments is the same as the direction of the previous segment."

Google Chrome already implements CSS motion paths like that.

So, you don't have to worry how Path Directionality affects motion paths.

@AmeliaBR
Copy link
Contributor Author

Motion path rotation will always have a discontinuity at sharp corners. I agree that adding a single frame where the object switches to the half-way rotation is not worth the extra complication.

For markers, @BigBadaboom's algorithm seems quite reasonable, following the overall directionality of the path if it exists.

Can we get any comments from browser teams about whether this seems do-able?

@BigBadaboom
Copy link
Contributor

While I think of it, it would be quite nice to have the rotation available to the front-end in some form (either the marker slope, or the motion slope, or both). Something like a getSlopeAtLength() method.

@fsoder
Copy link

fsoder commented Jul 28, 2017

Seems like a reasonable tiebreaker strategy, feel free to file a Chromium/Blink bug and I'll see if I can get to it eventually. (Bugs for other engines would probably be good from an interoperability standpoint too.)

And to follow the tangent (sorry, couldn't resist!) from #333 (comment) about getSlopeAtLength(). If that's intended as a companion to SVGGeometryElement.getPointAtLength(), I don't think that would be much work to implement. (Slightly more efficient would be to add a method to return both the point and slope/angle/normal - or even one taking an array of lengths and return an array of [point, slope] tuples, or else we'll probably need to make sure there's a lookup cache to prevent the obvious risk for N^2 behavior...)

@AmeliaBR
Copy link
Contributor Author

Agreed about the usefulness of a method to get the angle of a point on a path, so I've copied the discussion + comments of my own to a new issue: #338

BigBadaboom added a commit to BigBadaboom/androidsvg that referenced this issue Dec 11, 2017
@boggydigital boggydigital added this to the SVG 2.1 Working Draft milestone Jun 11, 2018
@boggydigital
Copy link
Contributor

Not blocking updated 2.0 CR publication - assigning 2.1 WD milestone

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

7 participants