Wednesday, November 17, 2021

Map Compasses (Part 4): Implementing CIRCLE and SPACE

Welcome back to my blog series on implementing procedurally-generated map compasses!  In previous posts, I've started defining the Compass Description Language (CDL), built a parser for the language, and started creating an interpreter to draw the compasses.  At the end of the last posting I had prepared the web page for drawing.  The next step is to write the code to draw the current language commands (CIRCLE and SPACE).

Before I do that, let me discuss how drawing will work.  First of all, the interpreter will need to know where the compass is to be drawn (its center point) and how big the compass is (its total radius).  (For now I'll just set those parameters.)  Second, the compass will be (generally) drawn from the outside in, similar to how I handled drawing map borders.  The interpreter will have a “current radius" that will start at the total radius for the compass and be decremented after every drawing command.  For example, if the total radius of the compass was 100, the command CIRCLE(5, “black", “none") to draw a 5 pixel wide black circle would be drawn at 100 and decrement the marker from 100 to 95.  If that were followed by CIRCLE(1, “red", “none"), the red circle would be drawn at 95 and would just touch the inside edge of the black circle.

It's important to note that the marker indicates the outside edge of whatever is being drawn.  So to draw CIRCLE(5, “black", “none") at 100, I have to take into account the width of the line and draw so that the outside edge of the circle is at 100.

I'll begin by putting in the basic structure for drawing a circle:

function interpretCDL(svg, cdl, center=[0,0], debug=false) {
    let radius = 100;
    for(let i=0;i<cdl.length;i++) {
	const op = cdl[i];
	if (op.op == 'CIRCLE') {
	    // Draw a circle
	    Draw.circle(svg, center, radius-op.lineWidth/2, op.lineWidth, op.lineColor, op.fillColor);
	    radius -= op.lineWidth;
	    if (debug) console.log('Draw a circle.');
	} else if (op.op == 'SPACE') {
	    // Skip some space
	    if (debug) console.log('Skip some space.');
	} else {
	    console.log('executeCDL encountered an unknown opcode: '+op.op);
	};
    };
};
I've done a couple of things here.  First, I'm now passing the SVG element and the center of the compass into the interpreter so it will know where to draw.  Second, I've added the radius and started it at 100.  Where the interpreter handles the CIRCLE operation, I've added in a call to Draw.circle, which will be responsible for actually drawing the circle.  Note the third argument to Draw.circle, which is the radius of the circle.  Naively this is the current radius, but I have to back off by half of the line's width as well, or else the outside of the circle will go outside of radius.  After the circle is drawn I move the radius in by the width of the circle line, so that the next operation will start in the right place.

Over in draw.js is the code for Draw.circle:
function circle(svg, center, radius, lineWidth, lineColor, fillColor) {
    svg.append('circle')
	.attr('cx', center[0])
	.attr('cy', center[1])
	.attr('r', radius)
	.style('stroke-width', lineWidth)
	.style('stroke', lineColor)
	.style('fill', fillColor);
};
This is straightforward; it simply adds the SVG command to draw a circle to the SVG element that was passed into the interpreter.  If you're working in something other than SVG (say, canvas) then you can replace this with the appropriate circle drawing code for your display.

Testing this with “CIRCLE(1.5, “black", “none")" produces this:
Here's a more interesting example CIRCLE(2, “black", “none") CIRCLE(2, “red", “red") CIRCLE(2, “black", “none"): 
I've set the fill color of the second circle to red, so the inside of that circle is entirely red.  The next circle goes down on top of that with no fill color.  But no fill color doesn't hide the red, it just doesn't cover it over.  So how do you get a band of color instead of a whole circle of color?  You use a very wide line width, like so CIRCLE(2, “black", “none") CIRCLE(20, “red", “none") CIRCLE(2, “black", “none"):
What about the other command, SPACE?  This adds space between elements of the compass.  But the spacing of the elements of the compass is controlled by the current radius.  So to add space between elements, we just need to subtract that space from the radius:
	} else if (op.op == 'SPACE') {
	    // Skip some space
	    radius -= op.n;
	    if (debug) console.log('Skip some space.');
	}
So now we can do this: CIRCLE(2, “black", “none") SPACE(2) CIRCLE(2, “black", “none") SPACE(2)  CIRCLE(2, “black", “none"):
Interestingly, we can use negative numbers in the SPACE command to move “backwards" -- making the radius larger.  So, for example, we can draw a large band of color, go backwards to draw a line in the middle of the band, and then go forward to where we started like this CIRCLE(2, “black", “none") CIRCLE(20, “red", “none") SPACE(-11) CIRCLE(2, “green", “none") SPACE(9) CIRCLE(2, “black", “none"):
I'll leave it to the reader to puzzle out the CDL, but this ability to move in and out will be handy when we're doing the procedural generation of compasses.

If you want to follow along from home, you can download the Part 4 branch from the Procedural Map Compasses repository on Github, get the test web page up and open the console.  Part 1 has detailed  instructions, but the short version (on Windows) is to double-click mongoose-free-6.9.exe to start up a web server in the Part 4 directory, and then select the test.html link.  Obviously you can browse the code and other files, and make changes of your own.  Try writing some CDL, inserting it into the test function, and using the button to generate it.  You can also try out this code on the web at https://dragonsabound-part4.netlify.app/test.html although you'll only be able to run the code, not modify it.

Next time I'll start on generating radial elements.

Suggestions to Explore
  • The CIRCLE command draws a solid circle.  It might be useful to draw a dashed circle as well.  There are various ways to do this, but the easiest is to use the “stroke-dasharray" style in SVG.  Modify the CIRCLE command to take another parameter with the value for stroke-dasharray, and then use stroke-dasharray in Draw.circle to implement dashed circles.  How did you handle solid circles?

  • If you implemented the “no fill" version of the CIRCLE command in Part 2 in the parser, you can now implement that command in the interpreter.  Did you change interpretCDL() or Draw.circle, or both?  Why did you choose that approach?

  • My Map Borders Description Language also used the idea of a cursor and drawing from the outside inward, and also had a command like SPACE for moving the cursor.  MBDL also implemented a command that would save the current value of the cursor and then return to it later.  Implement that idea in CDL -- a command to REMEMBER() the current cursor value and a command to RECALL() the cursor value.  What happens if you REMEMBER() twice before RECALL()?  What happens when you RECALL() twice?  How would you extend this idea so that you could REMEMBER and RECALL multiple different values?

No comments:

Post a Comment