Tuesday, May 31, 2022

Adding Compasses to Dragons Abound

 As I documented in a previous series of posts, I recently wrote a procedural map compass generator.  That was partly an experiment in making my code publicly available, so I wrote it as a standalone program separate from Dragons Abound so that it could be more easily used by someone else.  (You can play with it here.)  Now I'm going to integrate that code back into Dragons Abound.

The first step is to import the libraries used by the compass generator.  A couple of these are already used by Dragons Abound.  One of these is Nearley, but Dragons Abound is a couple of releases behind the compass generator.  So the first step is to switch Dragons Abound to the current version of Nearley and see if that breaks anything.  It seems to work fine, at least for the first few test maps I generated.

The second step is to bring all the code into Dragons Abound and see if it will run.  In the compass generator, I had a simple function that ran the compass rules and then drew the compass at [100, 100] and size 75 on the test SVG.  I need to tweak that function to use the correct location and size for the map.  Otherwise the compass code “should" work as is.

But the code immediately dies with rather an odd error, which I trace down to a Compass Description Language (CDL) command that reads:

    SPACE(NaN)

A little more tracing and I eventually figure out that the problem is with a utility function called randIntRange(), which is supposed to generate a random integer in the given range.  I borrowed that function from Dragons Abound, but in the course of the Compass work, I beefed up that function a bit to take a wider variety of more convenient inputs, like so:

// Random integer in a range.
// randIntRange(lo, hi)
// randIntRange([lo, hi])
// randIntRange(hi) lo == 0
function randIntRange(lo, hi=false) {
    if (!hi) {
	if (Array.isArray(lo)) {
	    hi = lo[1];
	    lo = lo[0];
	} else {
	    hi = lo;
	    lo = 0;
	};
    };
    return Math.floor(randInt(hi-lo+1)+lo);
};

The flexibility of Javascript isn't always a blessing, but here I'm using it to create a function that takes any sort of representation of a range and “does the right thing."  This new version is compatible with the old version, so I just have to replace the version in Dragons Abound with the one from the compass generator. 

That fix in place, the next problem is an attempt to set the opacity of the compass.  Dragons Abound expects the compass generator to return an SVG element of the whole compass, but in the compass generator project I wasn't actually using the compass SVG for anything so I wasn't returning it.  It's simple to tweak the generator to draw the compass in an SVG group element and return that.

At this point the code is completing, but still with errors.  Some of these trace back to the size of the compass.  The compass generator expects to make square compasses, but Dragons Abound can handle non-square compasses.  (Because some of the canned compasses it uses are not square.)  This leads to a parameter mismatch, where the compass generator expects to get a single number as the size of the compass, and Dragons Abound is supplying an array of width and height.  But in fact, Dragons Abound is providing square dimensions, so the generator can use either the width or the height as the size of the compass.

At that point, I'm finally getting a compass on the map:

The compass is a *little* too big.  This is mostly another parameter mismatch; the compass generator is expecting the size to be the radius of the compass, and Dragons Abound expects it to be the total size of the compass.  So the compass is twice as big as it should be.


That's better, although still a bit larger than it should be.  This image also illustrates another problem -- the compass should be centered on the windrose network.  If you look, you'll see that the center of the E and the S are centered on lines.  This suggests that the bottom-right corner of the compass box is centered on the windrose network.  And in fact, this is another mismatch between Dragons Abound and the compass generator.  The anchor point for images (like the canned compasses) in Dragons Abound is usually the lower left corner of the image.  For the compass generator, the anchor point is the center of the compass.  So for a procedurally-generated compass, I need to shift the center accordingly.


And with that the basic functionality is working.  However, there are still several additional features to implement.

The first and “easiest" feature is to set the font and size of the labels on the cardinal points.  In the compass generator, these are specified in the RTEXT command.  For Dragons Abound, it's better to control that with the map parameters, so that the compass can use a specific font, or default to the same font as the rest of the map.  So before I can fix the font problem, I first have to modify the compass generator code so that the map parameters structure gets passed down to RTEXT (and all the other CDL commands).

RTEXT can be used to write text other than the cardinal point labels, so I want to be careful to maintain that capability as well.  I decide that if the font specified in RTEXT is “cardinal" I'll then look up the compass font and use that instead:


For the moment, that's all I'll do; I'll still take the font style and weight (e.g., bold, italic, etc.) from the RTEXT command but obviously I can pull them from the map parameters in the future if that seems desirable.

The second and much more difficult feature is to swap out all the drawing routines for the corresponding Dragons Abound routines.  The compass generator uses straightforward SVG routines (such as 'circle' and 'path') to draw on the screen.  This results in very precise, mechanical graphics.  Dragons Abound uses drawing routines that insert some of the imprecision of hand-drawing.  By replacing the precise SVG routines with the imprecise Dragons Abound routines I'll get something that looks a little more hand-drawn.

Mostly this is straightforward.  Dragons Abound has routines to draw lines and polygons, so circles are drawn by constructing a circular polygon and then drawing the polygon.  Here's an example with the “hand-drawn" parameters turned up artificially high:
Of course normally this is set to a more subtle level:

There are slight inconsistencies here that aren't immediately eye-catching (examine the inside circle in the NW quadrant) but still result in something that is more artistically appealing than mechanical precision.  (Well, at least to me; your mileage may vary.)

A feature that Dragons Abound has that isn't in the compass generator is to vary the width of drawn lines, again to simulate hand-drawing, or the imperfections of old printing presses on coarse paper.  Here's an example turned up artificially high:
You can see that the circles behind the compass points vary quite noticeably from thick to thin.  Some care has to be taken with both these effects.  It's all too easy to run lines into each other or otherwise create a mess.  But with the proper setting, this produces a pleasingly imperfect result:  
Of course, I can turn both these effects off if I want a very precise render.  (Now go back and look at the first example compass in this posting.  You probably didn't register it at the time, but if you examine it now you'll see these effects.)

When I wrote the compass generator, I used black and white as the colors.  In Dragons Abound, I will also have a dark and light compass colors, but I might want to use some different colors than black and white to better match the map.  To do this, I replace all the “black" and “white" color names in the compass rules with “line" for the line color, “dark" for the dark fill color, and “light" for the light fill color.  Then before I render the compass, I can substitute whatever color I desire.

Here I replace the white with a subtle cream color to make it work better with the dominant land color:


Lastly, I want to add the capability to use a saved CDL compass rather than generate a new one.  That allows me to use a particular compass as part of a map style, or to select from a list of curated compasses.  (I have something very similar for map borders.)  I don't know if I'll have much use for this; almost all of the generated compasses are acceptable to my eye.  But we'll see.

And that's it.  At some point I'm likely to revisit compasses to improve/extend the procedural generation, but for now I'm happy to have something solid working.

Sunday, May 22, 2022

Map Compasses (Part 18): Center Ornament

Welcome back to my blog series on implementing procedurally-generated map compasses!  Last post I looked at some elaborations of the compass points.

This time I want to look at adding a new feature to the compasses -- the center ornament.

So far, all the compasses I have generated have been what I've called “two level" compasses.  They comprise a back layer of various rings, and a front layer of points.  Some compasses add to this a third layer that creates a center ornament on top of the rings, like this compass from the examples:


Most often the center ornament is a filled circle of some sort that covers the center part of the compass where the primary points meet.  Sometimes it is just a ring, as in this example:

Sometimes the circle is filled with an illustration of some sort, as in this example:

I don't have any way to draw illustrations (yet?) so I'll work on some circles and rings.

To start with, a three layer compass is just a two layer compass with an added center decoration:
<compass> => <twoLayerCompass> | <threeLayerCompass>;
<twoLayerCompass> => <labels> REMEMBER("start") <bottomLayer> <topLayer>;
<threeLayerCompass> => <twoLayerCompass> <centerDecoration>
For the center decoration, I'll start with a plain filled white circle.  To place it, I need to move the “cursor" to the radius I want.  Generally, I'd like the circle come out to about where the shoulders meet on the four cardinal pointers.  This turns out to be somewhere in the range of 51-58 pixels in from the outside of the compass, but I'll add some to the range so that I can get circles that more than cover the center, or that reveal some of the center.  To get to that radius, I'll use RECALL to get back to outside of the compass points, and then SPACE to move inward to around where the four cardinal points meet:
<centerDecoration> => RECALL("start") SPACE(`Utils.randRange(48,63)`) CIRCLE(`1`, "black", "white");
Some examples:

So that works, but I really don't like hardcoding the range for the center decoration into the rule.  I'd rather figure out where the shoulder of the cardinal points is and work from there.  Unfortunately as it stands that information isn't available.  The rule that creates the cardinal points looks like this:
<cardinalPoints> => RECALL("start")
		    RPOINT(0, 4, `Utils.randRange(0.825,0.925)`, 1, 1,
		    "black", "white", "black");
The third parameter to the RPOINT command is what places the shoulders of the cardinal points.  Since it is less than 1, it is treated as a percentage of the current radius, and the actual value is calculated in the implementation of RPOINT, so it isn't known until the CDL is actually executed.

To work around this, I'll create a special RECALL point “shoulder" with the idea that this won't be set by a REMEMBER command but instead set within the execution of the CDL when an RPOINT is drawn.  Since the cardinal points are always the last ones drawn, this will let me RECALL back to exactly the correct radius.

To do this, I'll modify Draw.rpoint so that it returns the radius at the shoulder, and I'll modify Draw.repeat to return that as well.  Then, in the CDL interpreter I can do this:
	} else if (op.op == 'RPOINT') {
// Draw radial compass points let shoulder = Draw.repeat(svg, center, radius, op.start, op.repeats, op, Draw.rpoint); memory['shoulder'] = shoulder; if (debug) console.log('Draw radial compass points.'); }
and save that value into the memory under the name 'shoulder' so that I can then jump to that radius with a RECALL command.  Now I can write:
<centerDecoration> => RECALL("shoulder") CIRCLE(`1`, "black", "white");
and the center decoration will be exactly the size to reach the shoulders of the cardinal points, regardless of the configuration.


It's nice to have a little variation on the size of the center decoration.  I can do this by putting in a random SPACE to move the radius in or out a bit.
<centerDecoration> => RECALL("shoulder")
		      SPACE(`Utils.rand(-2,2)`)
		      CIRCLE(`1`, "black", "white");

With that in place, let me elaborate that a bit and do multiple circles, as I did with rings:
<centerDecoration> => RECALL("shoulder") SPACE(`Utils.rand(-2,2)`) <circleFilled>;
<circleFilled> => <thinCircleFilled> | <thickCircleFilled> | <thickThinFilled> |
                <thinThickFilled>;
<$circleSpacingFilled> => 1 | 2 | 2 | 3;
<thickThinFilled> => <thickCircleFilled> SPACE(<$circleSpacingFilled>) <thinCircleFilled>;
<thinThickFilled> => <thinCircleFilled> SPACE(<$circleSpacingFilled>) <thickCircleFilled>;
<thinThinThinFilled> => <thinCircleFilled> SPACE(<$circleSpacingFilled>) <thinCircleFilled>
	          SPACE(<$circleSpacingFilled>) <thinCircleFilled>;
<thinThickThinFilled> => <thinCircleFilled> SPACE(<$circleSpacingFilled>) <thickCircleFilled>
                   SPACE(<$circleSpacingFilled>) <thinCircleFilled>;
<$thickWidthFilled> => 2.5 | 2 | 1.5;
<thickCircleFilled> => CIRCLE(<$thickWidthFilled>, "black", "white");
<thinCircleFilled> => CIRCLE(<$thinWidth>, "black", "white");
This is just the ring rules copied and slightly modified to suit the center decoration.  It produces circles within circles, some with thick outlines.  Some examples:


I don't love duplicating a big chunk of rules this way, but this is an area where using a rule-based generator has shortcomings.  There isn't really a notion of a “parameter" in these types of rules, so that limits how reusable the rules can be.

A variant of this type of center decoration is to have the inner circle be filled with black (as in one of the example compasses above).  That produces center decorations like this one:


Here's an example of a different style of center decoration:
Here the middle of the center decoration is empty, and let's the compass underneath show through.  This has to be drawn in a slightly different way.  It's really three lines:  a thin black line, a thick white line, and another thin black line.
<whiteRing> => <thinCircle> <thickWhiteCircle> <thinCircle>;
<thickWhiteCircle> => CIRCLE(<thickWhiteWidth>, "white", "none");
<thickWhiteWidth> => 3 | 2.5 | 2;
Here again there's a certain amount of redundancy with the similar rules for the first layer of the compass; since you can't parameterize rules for the fill color or the width, this is inevitable.



The second example here is on the edge of what I consider acceptable.  Since the rules have no way of knowing the current radius, there's no way to change the style of the center decoration for the compasses with very narrow cardinal points.

This compass from my examples has a scale as part of the center decoration:
There are several differences between the scale I'll use here and the ones used further out in the compass.  There isn't room for a lot of the elaboration I was able to use there, and likewise the lines have to be generally quite thin.  Also, it's problematic to start this scale with 8 segments in synch with the cardinal points, because it “disappears" as in this example:

Using just the simplest form of the scale seems to work the best:

At it's smallest, it really just looks like a pattern or even something like a flower.

Speaking of flowers, let's do a flower.  This is something that's going to take some careful planning, so I'll implement some better mechanisms for knowing where the current radius is and moving to a specific point.  To start with, I'll implement a one-time variable that will always be the current radius.  I don't know if I'll really need this, but it seems useful and easy enough to do.  I just add this line at the top of interpretCDL:
    for(let i=0;i<cdl.length;i++) {
	const op = cdl[i];
	memory['radius'] = radius;
	if (op.op == 'CIRCLE') {
Next I'll implement a command to move to a specific radius.  The SPACE command moves relative to the current radius; the MOVE command will just jump to the given radius.  In the CDL grammar, the command looks just like SPACE; in the interpreter it looks like this:
	} else if (op.op == 'SPACE') {
// Skip some space radius -= op.n; if (debug) console.log('Skip some space.'); } else if (op.op == 'MOVE') { // Move to the specified radius radius = op.n; if (debug) console.log('Move to a radius.');
So now I can just jump to a particular radius that works well for a flower.  We'll start with just a blank canvas:
Now we'll add some radial circles.
Now I'll move back out to the center of the radial circles and draw a white circle.

That looks pretty cool just like that, so I'm going to keep that as an option.  But to carry on, I'll now draw radial lines to the center of the compass.
That isn't what I was intending to do but again it looks pretty good so I'll keep it.  What I want is to shift the lines so they match up with the radial circles to form petals.
Optionally, I want to add a white or black center disk:

It can also look good to put the flower on a black background:
Here's another fun variant -- it should be obvious how it is drawn:
You can see the rules for all of these in the compass.rules file.

I can also do a center star of sorts:
I'm reusing the compass points to draw this, and there are a couple of drawbacks to this approach.  For one thing, I can only draw black stars, and secondly, because of a bug in Chrome there are some faint white lines visible.

This will wrap up compasses for now, although I'll have at least one more posting where I integrate this into the Dragons Abound code.  If you want the final version of the compasses code, you can download the Part 18 branch from the Procedural Map Compasses repository on Github.  You can also try out this code on the web at https://dragonsabound-part18.netlify.app/test.html although you'll only be able to run the code, not modify it.

Next time I'll write a short post-mortem on the Compasses project, what worked and what didn't, and what I learned from it.