Monday, October 2, 2017

A Different Way to Draw a Line

(Taking a break from labeling to talk about line drawing.)

Currently, Dragons Abound draws lines using the SVG path function, and then stroking the path with a pen of the appropriate size and color.  Combined with the D3js curve functions, this provides a pretty easy way to draw nice curved lines following a path:
I can use a different curve function to make a curvier or straighter line:
To make a line with varying width, I change the pen size as I draw the line:
By changing the pen size by small amounts, it gives a good illusion of a smoothly changing line size.

However, there are a couple of problems with this approach.  SVG doesn't allow you to change the pen size while drawing a path, so this is actually accomplished by breaking the path down into a lot of smaller paths, each of which is drawn separately with a different pen size.  If you don't have enough segments, the illusion doesn't work:
I handle this by interpolating the line into as many segments as I need, but this obviously results in a lot of complexity in the SVG.

A related problem occurs when drawing a curved line.  Because I start a new path every time the pen size changes, I'm usually not actually drawing a curved line but instead a lot of small straight segments.  This starts to become obvious when the path I'm drawing doesn't have enough segments:
If I don't change the pen size, this draws as a smooth curve.  But since I'm changing the pen size, I have to draw each piece of the curve separately and the result is a jagged curve unless I break the curve down into many pieces.

In working with Inkscape to develop vector drawings of sea monsters, I realized that Inkscape doesn't generally used lines (stroked paths) at all.  Instead, lines are created by using long thin polygons.  For example, a straight line would be drawn as rectangle around the path of the line:
If the orange dashed line represents the path, then to draw a line along that path, Inkscape creates the polygon shown as the thin black line, and then fills it in with the ink color.  If you want to make a line with varying width, you can just draw the polygon appropriately:
This approach avoids the problems I have with drawing varying width lines by varying the pen size.  There's no need to break the line down into segments to achieve a smooth curve -- I can use the SVG curve capabilities to draw each side of the polygon and get smooth curves that way.  The biggest drawback is that there doesn't seem to be a handy library available that implements this, so I'll have to implement it myself.

I'm not sure how this "should be" implemented, but I'm going to do something similar to the approach described here as Method 1.  I will start by finding the normal to each point along the path:
Then on each normal I will find the point on either side of the path that is half the width of the line at that point.  If this line gets wider from left to right, that might look like this:
The final step is to draw two smooth curves through the black points, connecting at the beginning and end of the line to create a closed polygon:
As you can see from this example, depending up on how "tight" or loose the side curves are, the resulting line will follow the path more or less closely:
I've had to calculate normals/gradients before, so this is actually pretty straightforward, and here's my first attempt, using straight lines between the points and drawing a constant width line:
Obviously not quite right.  It looks to me like the normals are incorrect.  Let me tweak the code to draw in the normals (exaggerated):
Hmm.  There's definitely a problem with the normals.  If the gradient is [dx, dy] then the normal is [-dy, dx].  I always forget the minus part of -dy.
That fixed some of the trouble but you can see that there's still a problem.  Some more fussing with the math and I have this:
Now you can see the normals look okay, but the width of the line is not correct.  Unfortunately, my simple-minded scheme doesn't work, because the angle of the normal changes the effective width of the line.  It's necessary to adjust the width of the line based upon the angle of the miter.  While working on this problem I discovered polyline-normals, a Javascript package by Matt DesLauriers that calculates both the normals and the necessary adjustment.  Dropping that in yields this:
Seems to be working!  Let's try it with a varying width line:
And finally, the same thing using curves instead of straight lines:

This looks pretty good, but you can see that the width isn't exactly right everywhere.  The difficulty is that the width is only specified at certain spots on the curve, and the interpolation in-between those spots varies for the top and the bottom curves of the line.  For mathematical reasons, it's very difficult to make the two curves match exactly, so this problem is somewhat inherent in this approach.  But if  you compare this result to the current approach:
You can see the advantage to being able to draw smooth curves even while the width changes.

As with the existing method, I can make the line width more faithful by increasing the number of points in the line:
I'm not sure this is actually an improvement; there's something artistically appealing in the more inaccurate less interpolated version.

How does this look when drawing map features?  First, let's look at a mountain drawn with the existing line routine:
Two things to note:  First, the ridge lines coming down from the two peaks are not smooth and very obviously connected line segments -- even though I'm trying to use smooth lines here.  Second, the short tapering line I've accented in red is jagged because the line size is changing in discrete chunks.

Here's the same mountain rendered with the new style lines:
Here you see that the ridge lines are much smoother and curvier, and the red line shows a much cleaner edge.  Of course, I don't have to use curvy lines with the new method -- you can see the mountain outline itself is still jagged -- but at least it allows that as an option.

I cleverly (luckily?) isolated all the line drawing in Dragons Abound behind an API, so I can seamlessly switch in the new line drawing routine.  Maybe I'll run into problems but at a first glance it seems to work just fine.

No comments:

Post a Comment