Monday, December 10, 2018

Hand-drawn Lines Revisited

I have written previously about how Dragons Abound creates lines that look “hand-drawn."  This turns a straight, obviously computer-drawn line and adds jitter and small width changes to make it look like it might have been drawn by a person:
Despite using this, someone commented on my blog recently that my maps would look better if the coast lines were more hand-drawn.  That reinforced my own thoughts that the coastlines didn't look very “hand-drawn", so I decided to look into more to see if something was broken, or if there was something I could do better to give a hand-drawn feel.

The short version is that I didn't find anything really wrong in the hand-drawn line code, but I also agreed with the commenter that the lines it was producing didn't look very hand-drawn.  For example, here's a normal (magnified) section of coastline:
The coastline is a smooth, natural-looking line which varies a bit in width but doesn't have any of the jitter we associate with hand-drawn lines.

The primary source of the hand-drawn feel is jitter along the line.  Here's what that coastline looks like with some jitter added:
Two things that you might notice.  First, the coast line now strays a bit from the land in places.  This is a good indication that the line is being jittered because the random offsets to the line mean it sometimes no longer aligns with the land-sea boundary.  Second, the coastline now seems much more angular and composed of line segments instead of smooth curves.  Instead of getting a “jittery" version of the curved coastline in the first example, I got something else entirely.  So what happened?

You may recall that SVG is a vector format.  Curved lines in SVG are drawn by smoothly connecting the points on the line with Bezier curves.  In the first case above, the smoothly curving coastline is created by drawing Bezier curves between the points of the coastline.  (This happens via D3, which provides some helper functions to make this easy.)  For example, if I had a coastline with three points, D3/SVG would generate a Bezier curve to connect them and it would be drawn like this:
Now suppose I want to “hand-draw" this same coastline.  To do this, I want to make the line jitter back and forth a bit along its length.  So I can't use an SVG Bezier curve.  I need to break the line up into a bunch of smaller chunks and then add a bit of jitter to each new point.  That gives me this:
Oops.  How did that happen?  Recall that the coast line is really just those three blue points forming a triangle.  When I connect those three points with a Bezier curve, I get the smooth curve above.  When I interpolate the line between each of those points, I get straight line segments.  Those points are still drawn with curves (as above) but it is now constrained much more closely to the triangle shape.

The basic problem here is that the coastline is defined as line segments but drawn by connecting the points with smooth curves using SVG Bezier curves.  To make it look more hand-drawn, I want to break up and jitter the coastline as it is drawn -- that is, I want to jitter along the path that SVG creates for the Bezier curves.  How can I do that?  Well, one solution would be to throw out any use of SVG Bezier curves and calculate all curves myself -- essentially re-implementing SVG curves.  (That's actually quite doable, since for other purposes I already have routines that implement Bezier curves.)  Alternatively, I can continue to draw using SVG, but capture the line before it gets to the screen so I can interpolate and jitter it.  This has the advantage that I don't have to re-implement all of SVG's curve drawing capabilities!

The method for capturing the line as it is drawn is not immediately obvious, but is not difficult.  The key is that the D3 functions that draw the curves can be fed an optional “context" object that implements the CanvasPathInterface and will use that interface for drawing rather than using the screen as they normally would.  To capture the curve as drawn on the screen, I can create a custom context to interpolate the moves, lines and Bezier curves into a sequence of points.  Then I can jitter that series of points and draw it as a polyline.

It turns out that the D3js curves use only lines and cubic Bezier curves, and since I already have functions to interpolate those, implementing the context is pretty straightforward.  As a test I feed it the three points from above and see if it can interpolate the original curve:
Looks like it works.  (The points are not actually equally spaced here -- the math to do that is too complicated to be worth the effort.)

Now I can “hand draw" these points:
That looks pretty good.  Now I'm essentially jittering the curve as it was drawn on the screen.

The bad news is that if I use this to draw coastlines they look ... just about the same:
Again, you can see some spots where the land sticks out from behind the coast line, indicating that the jittering is actually happening.  So why doesn't it look hand-drawn?

There are two reasons.  First and most importantly, you don't perceive jitter very well on an irregular line.  Which makes sense -- if a line is close to being a regular curve, then the little imperfections are obvious.  If a line is irregular, you can't tell whether a little bump is jitter or intentional -- it's the fact that you know how a perfect line should look that allows you to see the imperfections!  Second, the use of curves to connect the coast points tends to smooth out the jitter.  (Although drawing with straight line segments doesn't make the jitter much more obvious, and looks less fluid.)

The lesson here is that the hand-drawn look really only works for regular, predictable lines where the computer can do a perfect job and the human hand cannot.  In retrospect, this shouldn't be too surprising -- the inspiration for the hand-drawn lines approach comes from various efforts to Xkcd-ify plots, and plots are of course mostly made up of regular, predictable lines.

Well, you win some and lose some!  Not every new idea works out.  Although this improvement didn't work for coastlines, it does work in some other places, such as hand-drawing the frame around the map:
So what does make a coastline look more hand-drawn?  I reviewed my inspiration maps and it's hard to see any consistent differences between the coastlines on those maps and the ones produced by Dragons Abound.  (In terms of line quality, at any rate.)  One area where human-drawn maps differ is in the level of detail on the coastline, as in this comparison:
You can see here that on the human-drawn map the coastline has a lot more fine detail.  Many human-drawn maps have an almost fractal level of detail in the coastlines.  This is problematic for Dragons Abound for reasons I reviewed when creating barrier islands -- namely, that Dragons Abound creates coastlines by drawing around the edges of the Delauney land triangles.  The amount of detail there is limited by the size of the triangles.  And since the land is colored according to the same triangles, if I add detail to the drawn coastline, it will no longer match up with the land-sea border.

The solution is probably to get away from drawing the land based upon the underlying Delauney triangles and instead use the coastline paths to draw and fill the land.  This would be a significant change to how Dragons Abound works, but it will enable a number of features I'd like to have.  Not just fractal coastlines, but it would make it easier to draw coastlines with decorative edges, like this:
It would also enable small islands and very narrow barrier islands.  I'll have to give some consideration to how hard the change would be.  Stay tuned!

(Spoiler Alert:  Yeah, I went ahead and did it.  More next time.)

2 comments:

  1. Have you tried applying "jitter" to the color? When drawing fast the path becomes light and when drawing slow the path is dark darker.

    ReplyDelete
    Replies
    1. I did experiment with that. It turns out that when your lines are pretty thin to start with, changing the width looks a lot like changing the darkness, and changing the darkness looks a lot like changing the width, if you understand what I mean. So it didn't end up adding much interest, and for the sake of simplicity I only kept the width variation.

      Delete