Monday, June 3, 2019

Map Borders (Part 13)

When I left off last time, I'd gotten the basic display capability working:
One problem that you can see above is that there are some artifacts where the individual segments of the pattern join up.  This occurs because the browser introduces some anti-aliasing artifacts where two SVG elements meet exactly.  One way to address this is to overlap the elements slightly to try to eliminate the seams.  There's a bit of black magic and experimentation to figuring out how much you have to overlap and in what order (and worse, this varies depending upon the magnification of the SVG), but after some tweaking I have removed most artifacts:
I've got a growing list of things to do with Celtic knots, but before I tackle anything else I want to add shading to accentuate the crossings.  The basic idea can be seen on this tutorial page:
The idea is to add some shading to either side where a cord passes under another cord.  There are various ways to do this in SVG, but the simplest for me is to draw another line on top of the cord and fill it with a transparent gradient:
I've left it pretty subtle here, but if this lacks impact I can go back and tweak it as necessary.

While I was adding that code I also parameterized various other constants -- a couple of which control whether I use “hand-drawn" lines.  There are some challenges with using hand-drawn lines where something is pieced up of different parts and where lines are laid atop one another.  The code takes care of making sure that end points don't move around, but it's possible that angles can change so that segments no longer meet smoothly.  So if I use it, I will have to use a fairly small amount of jitter.  Here's an example with about the maximum decent perturbations:
I could also vary the width of lines, but that would be even more problematic so I don't think I'll allow that as an option.

Next I'm going to implement a more rounded style of knot.  This works the same as the existing style, except the cords that meet horizontal and vertical walls are replaced by quarter circles.  I already have a routine to generate circular arcs, so this change is mostly a matter of figuring out the right arcs and then tweaking them so that they don't reveal any anti-aliasing artifacts where they overlap.
Another common style squares off the turns in cords, as in this example:
This is a little trickier to implement, because how a cord that meets a wall is drawn depends upon whether it's neighbor in that direction is also meeting a wall.  For example, cords that meet horizontal walls work normally (as they do in the first style) except if they have a vertical wall to the right or left, in which case they go straight into the corner:
Here I'm showing just crossings and one half of the pattern for a horizontal wall.  Along the middle bottom of the pattern it looks the same as in the first style.  But in the corners, where there's a wall next to it, it goes straight into the corner.

(In the middle of implementing this style, I decided to refactor the code so far to simplify some of the special cases that have built up.)

After implementing all the other cases:
The appeal of this style is that it more clearly defines the boundaries of the knot.  Unfortunately, the anti-aliasing problem in the mitered corners here is very hard to get rid of completely.  Close inspection of the knot above will show some problems.  I tried a number of different approaches with no real luck.  I'm going to declare myself defeated on this issue for the moment.

There's one more style capability I want to implement before I move on, and that's the capability to blur the cords.  (The purpose of this will become evident below.)   This is a little tricky because of the way SVG implements blur.  If you apply blur to an SVG element, the blur can spread anywhere within the object's bounding box.  So if I just apply a blur to cord, it smears into the adjacent areas:
You can see how the cord no longer has sharp edges because the white has blurred into the black spaces, and even over the red outline.  To prevent this, I have to clip the blur to shape of the cord. (And hopefully I can do this piecemeal to each of the segments of the cord.)
Here I've created clipping paths for some pieces of the knot and applied a blur.  You can see how the blur now ends sharply at the edge of the piece.

Unfortunately, as I continue implementing this I discover that I can't in fact implement it piece-meal:
As you can see, there are some pretty obvious artifacts at the joints where the clipping areas meet.  (The same locations as the anti-aliasing problems!)  In retrospect, using clipping areas was probably a poor choice.  Difficult to implement and with some predictable problems.  A much better choice is to use a mask.  It's also easier to construct; I just draw into the mask at the same time I'm drawing onto the screen.  I like to leave at least a few stupid dead-ends in these write-ups so that you don't think I'm some kind of super-human coder.  I'm definitely not!

With the mask in place:
If you compare this blur to the earlier full pattern blur, you'll see this one stays cleanly within the pattern.

So why go to all this trouble for blur?  With blur and careful color choice, I can create a faux-3D style:
I can also draw the same knot in gray underneath this knot, offset it and blur it to create a shadow:
I'm not sure this is very useful for map borders, but it is pretty!

That's enough styling (for now).  Now I want to move on to a few capabilities I'll need to use knots in borders.

The first capability I want is to be able to turn a knot 90 degrees.  This way I can draw a knot along the top border, turn it 90 degrees, draw it along the right border and so on.  One way to do this is to always draw it horizontal and then use an SVG transform command to rotate the whole thing.  I might well end up doing this, but I'd like to explore the option of actually rotating the knot pattern before I draw it.

Recall that the knot pattern is just an array filled with values for CROSSING, VERTICAL, and HORIZONTAL.  To rotate that 90 degrees, I need to create a new array with the x and y sizes swapped, and then copy values into that new array.  The non-obvious part is that when copying, I have to swap VERTICAL walls for HORIZONTAL walls, and vice versa.
Okay, so now I can rotate the knot if necessary.

Another feature I need is to be able to repeat a knot to fill a particular length.  For example, to take the above knot and repeat it three times.  Obviously I can draw the knot repeatedly end to end:
But that leaves obvious seams where the knot begins and ends.  What I'd like to do is eliminate the seams by connecting the knot end to end to create a single bigger knot.  This involves repeated copying of the knot and replacing the end walls with crossings (lower image):
Right now my implementation is limited to whole numbers of repeats.  I can imagine situations where it might be nice to have a partial repeat, but I'll cross that bridge if I get to it.

The next feature is the capability to save and restore a knot.  I'm not entirely sure I'll want to be able to save a particularly good knot to use over again, but it doesn't seem too far-fetched.  The knot is defined by it's dimensions and the pattern of crossings and walls encoded in the array.  The array can be saved as a long string of characters representing crossing and walls.  The sample knot I've been using comes out like this:

25,5,.|.|.-.-.-.X.X.-.X.-.|.X.-.X.-.X.X.-.|.-.X.X.-.X.-.X.X.-.-.-.|.X.-.-.-.X.X.-.X.-.X.X.-.
|.-.X.X.-.X.-.|.X.-.X.-.X.X.-.-.-.|.|.

Long, but not intended to be particularly human-friendly.  Restoring involves reading the dimensions and making sure the knot is the correct size, and then loading the long string into the array that represents the knot.

The last (?) feature I know I'll need is to create a random knot.  At some level, creating a random knot is just taking a vanilla knot and randomly dropping in some vertical and horizontal walls (handcrafted on top, random below):
But as this example shows, there are some constraints that need to be met.  The “gaps" need to be maintained, and (for my purposes anyway) the walls around the edge of the knot need to be maintained.
That produces a legal knot, but it's not necessarily very appealing.  And, as this example shows, vertical walls in the same column can chop the knot in two, which probably isn't what I want.  I don't really want a completely random knot; I want one that is semi-random but still “beautiful."

I'm not exactly sure what makes a knot beautiful but one element is symmetry.  Since I'm generating long knots, I will use mirror symmetry on the X axis.  This is easy to implement:  Every time I place a wall, I also place the same wall at the symmetrical position:
This goes a long ways towards creating an acceptable knot.  I can still get placements that chop the knot into pieces:
This is actually three knots.  But I'm not sure this is bad.  Here is another example of a knot getting broken into separate pieces:
This also seems okay to me.  So maybe symmetry is all I need to create an acceptable random knot.  I'll at least start with this and see how it works out.

A sampler of styles:

Next time I'll start working (at long last!) on using Celtic knots in map borders.

2 comments:

  1. Hey, that blurring is really pretty! Very cool.


    How does _Dragons Abound_ handle configuration? Is it a a json file, or a TOML file, or something else?

    With all the customisation available that you've talked about so far, it must be a challenge managing it all. I'm curious to know what strategies you use to keep it under control :P

    ReplyDelete
  2. It's just a Javascript file which is a single big, complex Javascript object. That has some advantages, like I can easily initialize something to a multi-level Javascript object. But it isn't very "maintainable."

    I talked a little bit about parameter management here: https://heredragonsabound.blogspot.com/2018/04/parameter-management.html. That's a fairly flexible scheme for what I need, but I do wonder if someone has written some utility that I should be using instead.

    ReplyDelete