Monday, February 25, 2019

Map Borders (Part 3)

In the last posting, I developed the first iteration of the Map Border Description Language (MBDL) and wrote a context-free grammar for the language that can be used with Nearley, a Javascript parsing toolkit.  Now it's time to implement the actual drawing primitives.  (At this point I don't yet have the parser hooked up to the drawing primitives.  I'll just be calling them manually to test them.)

I'll start with the line primitive.  Recall that this has the form:
L(width, color)
In addition to width and color, there's an implicit parameter that is the distance from the outside edge of the map.  (By convention, I am drawing the borders from the edge of the map outward.  Note this is a change from how I started!)  This doesn't have to be specified in MBDL because the interpreter that executes MBDL to actually draw a border can keep track of this.  However, it does need to be an input to all the drawing primitives, so they know where to draw.  I'll call this parameter the offset.

If I only had to draw the border across the top of the map, the line primitive would be pretty straightforward to implement.  However, I actually have to draw it at the top, bottom, left and right sides.  (At some point I may implement slanted or curved borders, but at the moment I'll stick with the standard rectilinear borders.)  Finally, the length and placement of the line element depends upon the dimensions of the map (combined with the offset).  So I need all those things as parameters as well.

With those parameters defined, it's fairly easy work to create the line primitive and use it to draw a line around the map:
(Note that I'm using the various Dragons Abound features to create a “hand-drawn" line.)  Let me attempt a more challenging border:
L(3, black) L(10, gold) L(3, black)
That looks like this:
Pretty good.  Note that there are spots where the black lines and the gold line do not quite align due to the jitter.  I can tune down the amount of jitter if I want to eliminate those spots.

Implementing the vertical space primitive is pretty easy; it just increments the offset.  That lets me skip some space:
L(3, black) L(10, gold) L(3, black)
VS(5)
L(3, black) L(10, red) L(3, black)
When drawing lines, I can make the corners work by drawing between the offsets and drawing clockwise around the map.  But in general I need to implement clipping on each side of the map border, to create mitered corners.  This will be necessary for making patterned borders fit together smoothly at the corners, and in general will eliminate the need to draw elements with angled ends as would otherwise be required.  (*)

(Note: As will be noted in a later posting, I eventually abandoned using clipping areas to implement corners.  The primary reason is that to do complex corners such as square offsets:
requires increasingly complex clipping areas.  I also eventually figured out a better way to deal with patterns in corner.  Rather than go back and fix this posting, I thought I'd leave it this way to illustrative the “creative" process :-)

The basic idea is to clip each border along the diagonals, creating four clipped areas in which to draw each side of the border:
The clipping will cause anything drawn into the area to be cut off at the proper angle.
Unfortunately, this creates a slight discontinuity along the diagonal line -- probably due to the browser not doing a perfect job of anti-aliasing along the clipping edge.  A test shows this is the background color showing through a gap between the two edges.  A working fix is to extend one of the masks slightly -- about a half pixel seems to do the trick -- although this also seems to fail sometimes.

The next things to implement are the geometric shapes.  Unlike lines, these repeat in a pattern to fill a side of the map border, like this:
A person would draw this left to right, drawing a box, a diamond and then repeating the same way to fill the border.  So intuitively you might implement it like that in a computer program, drawing the pattern repeatedly across the border.  However, it's somewhat simpler if you draw all the boxes first and then all the diamonds.  Each part just becomes a matter of drawing the same geometric shape at intervals across the border.  And conveniently, each element is at the same interval.  Of course a person wouldn't do it this way because it would be too hard to place elements in the right spots, but that's not a problem in a program.

With that insight, the basic geometric shape routine needs parameters for all the shape dimensions and colors (i.e., width, length, line width, line color and fill color) as well as an initial position (which I will treat as the center of the shape for reasons that shall become apparent), an interval of horizontal space to skip between repeats, and the number of repeats.  It's also convenient to specify the direction of repeat as a vector [dx, dy] so that I can run the repeat left to right, right to left, up or down just by changing the vector and the starting point.  Put that together and it creates a row of repeated shapes:
Using this multiple times and drawing in the same offset, I can combine black and white bars to create a map scale:
Before I look at how to use these in an actual map border, let me implement the same functionality for ellipses and diamonds.

Diamonds are just boxes with the vertices rotated around, so that's only a small code change.  It turns out I don't have any ready-made code to draw an ellipse, but it's simple enough to take the parametric form of an ellipse and create a function to give me points on the ellipse:
Here's a (hand-crafted) example using the capabilities so far:
Looks pretty good for just a small amount of code!

Now let's address the tricky part about borders with repeating elements:  the corners.

There are several choices for corners when you have a border with a repeating element.  The first choice is to arrange the repeat so that it flows around the corner without a visible break:
Another option is stop the repeat somewhat short of the corner on both sides.  This is often done when the pattern can't be easily “turned" around a corner:
The final choice is to obscure the pattern behind some sort of corner decoration:
I'll get to corner decorations at some point, but for the moment let me consider the first option.  How can I get a pattern like the bars or circles above to flow seamlessly around the corners of the map?

The basic idea is to put an element of the pattern precisely at the corner, so that (logically speaking) one half is on one edge of the map and the other is on the adjacent edge.  In this example, there's a circle element that sits precisely on the corner and could have been drawn from either direction:
 
In other cases, the element is half drawn in one direction, and half in the other but the edges match up:
In this case the white bar is drawn from both directions but still meets seamlessly in the corner.

There are two things to note about placing an element into the corner.

First, the corner element will be split and mirrored along a diagonal line that passes through the center of the element.  Elements with radial symmetry -- like circles, squares and star shapes -- will not change shape.  Elements without radial symmetry -- like rectangles and diamonds -- will change shape as they are mirrored along the diagonal.

Second, for the corner elements of two sides to meet correctly, there must be an integer number of elements along both sides of the map (*).  There do not need to be the same number of elements along both sides, but there must be a whole number of elements on both sides.  If one side has a fractional number of patterns, at one end the pattern won't match up with the adjoining side.

(* In some cases, as with long bars, a partial repeat can meet with a full repeat and the elements will still line up.  However, the resulting corner element will be asymmetrical and a different length than the same element on the map sides.  An example can be seen here:
The white bar element of the scale meets at different partial repeats and results in an off-center element.  This isn't necessarily wrong for a map scale, which is intended to show absolute distance and not to be symmetric.  But for a decorative pattern this usually looks bad.)

Here's an example showing how a whole number of repeats gets cut off exactly in the corner:
If you do the same thing on all four sides the corners match up and the pattern runs seamlessly around the border:
A close inspection will show that the pattern doesn't meet exactly in the corners.  Half of the circle in each corner comes from each side, and those two halves are each being independently “hand-drawn" so they don't match exactly.  But they are close enough for now.

So I can make the pattern meet seamlessly in the corners by picking a whole number of repeats on each edge.  However, that's not a trivial problem.

First, suppose we know our side is 866 pixels long and we want to repeat our element 43 times.  Then the element should be repeated every 20.14 pixels.  So how should I adjust the length of an element (really a pattern of elements in the general case) to be a certain length?  In the example above, I added extra space between the circles.  But if the circles were originally touching, that changes the pattern.  Perhaps I should have stretched the circles instead to preserve the touching?
Now the elements still touch, but the circles have become ellipses and the corners are odd shapes.  (Remember when I said above that elements without radial symmetry would change shape when reflected around the corner?  This wouldn't be a big problem with bars.)  Or maybe I should shrink all the elements enough so that they'll both touch and fit in the proper length:
But to do this, I've had to make the elements considerably smaller than they were originally.  None of these seems like a perfect choice.

A second problem arises when the sides of the map are not the same size.  Now I must solve the problem of finding a whole number of repeats to fit for both sides.  Ideally I'd like to find one solution that works for both sides.  But I don't want to do that at the cost of changing the pattern too drastically.  It might be better to have a slightly different pattern on the two sides if they were both fairly close to the original pattern.

Finally, a third problem arises when I am using the overlay capability to stack elements:
I don't want to make any changes to the pattern that will break the relationship between the elements.  I think that proper scaling will generally maintain the relationships but I need to test that notion.

A fun problem, eh?  I don't have any particularly good answers.  Maybe I'll have something by next time!


No comments:

Post a Comment