Welcome back to my blog series on procedurally-generated map compasses! I've been implementing the Compass Design Language (CDL) and an interpreter for the language. I've been measuring my progress (on the right) against this compass (on the left):
I've made good progress on the graphic elements of the compass, and now I'll tackle the labels.Start and repeat are the usual parameters for a radial element that defines where to start and how many repeats to place around the compass. Font, size, color, and style are parameters that will define the font to use and its characteristics (e.g., italic, bold, etc.). The final element is the list of texts to use, one for each repeat. Most of these parameters are similar to what I've implemented in the parser for previous commands, but I've never tried to parse a list in Nearley so it will be interesting to figure that out.RTEXT(start, repeats, font, size, color, style, weight, texts)
# DQLIST # A list of double-quoted strings, possibly empty dqList -> "[" WS dqElements WS "]"
# DQLIST # A list of double-quoted strings, possibly empty dqList -> "[" WS dqElements WS "]" dqElements -> null | dqstring | dqstring WS "," WS dqElementsYou might (if you've taken any classes on grammars) have been discourage from writing rules like that last clause because it does “right recursion" -- that is, the last element in that rule loops back to the rule itself. That causes problems in some grammar parsers, but it turns out that Nearley is fine with this.
Mostly this should be familiar from previous blog entries -- the post-processing for all except the last clause just removes an unnecessary level of arrays. The last clause is a little more interesting. The three dots I use there is the spread operator, and essentially what it does is stick in the contents of an array. (It “spreads" out the array so to speak.) In this case, I use it to concatenate together the first element of the list (data[0]) and the list of all the remaining elements (data[4]).# DQLIST # A list of double-quoted strings, possibly empty dqList -> "[" WS dqElements WS "]" {% data => data[2] %} dqElements -> null {% data => [] %} | dqstring {% data => data %} | dqstring WS "," WS dqElements {% data => [data[0], ...data[4]] %}
Now I add RTEXT to the interpreter in the usual way and create a function Draw.rtext that will do the work of drawing the text.# Radial text # RTEXT(start, repeats, font, size, color, style, weight, texts) rtextElement -> "RTEXT"i WS "(" WS decimal WS "," WS decimal WS "," WS dqstring WS "," WS decimal WS "," WS dqstring WS "," WS decimal WS "," WS dqstring WS "," WS dqList WS ")"{% data => ({op: "RTEXT", start: data[4], repeats: data[8], font: data[12], size: data[16], color: data[20], style: data[24], weight: data[28], texts: data[32]}) %}
svg.append('text') .attr('x', x) .attr('y', y) .style('font-family',font) .style('fill', color) .style('font-size', fontSize) .style('font-style', fontStyle) .style('font-weight', fontWeight) .text(text);There are many possible attributes and styles that can be applied to SVG text elements but the above covers the ones we'll be using.
function rtext(svg, center, radius, startAngle, repeats, angle, iteration, op) {
let x = center[0];
let y = center[1]-radius;
svg.append('text')
.attr('x', x)
.attr('y', y)
.style('font-family', op.font)
.style('fill', op.color)
.style('font-size', op.size)
.style('font-style', op.style)
.style('font-weight', op.weight)
.text(op.texts[0]);
};
rotate(degrees, x, y)
This rotates an object by degrees around the point [x, y]. Let me add that to the rtext function:
Notice I needed a helper function “rad2degrees" because my angles are in radians and the SVG command expects degrees.function rtext(svg, center, radius, startAngle, repeats, angle, iteration, op) { let x = center[0]; let y = center[1]-radius; svg.append('text') .attr('x', x) .attr('y', y) .style('font-family', op.font) .style('fill', op.color) .style('font-size', op.size) .style('font-style', op.style) .style('font-weight', op.weight) .style('text-anchor', 'middle') .attr('transform', 'rotate('+rad2degrees(angle-Math.PI/2)+','+center[0]+','+center[1]+')') .text(op.texts[0]); };
function rtext(svg, center, radius, startAngle, repeats, angle, iteration, op) { let x = center[0]; let y = center[1]-radius; svg.append('text') .attr('x', x) .attr('y', y) .style('font-family', op.font) .style('fill', op.color) .style('font-size', op.size) .style('font-style', op.style) .style('font-weight', op.weight) .style('text-anchor', 'middle') .attr('transform', 'rotate('+rad2degrees(angle-Math.PI/2)+','+center[0]+','+center[1]+')') .text(op.texts[iteration]); };
- How would you address the problem of blanking out behind the ordinal labels? One possibility is to have an radial rectangle element (“RRECT"?) and use it to draw white rectangles where the labels will be. Can you use RRECT to draw the black and white ring in this compass?These rectangles aren't really rectangles -- they're arcs of a line instead. How will you draw that?
- In the example compass directly above, the cardinal labels are in the correct positions, but the text has *not* been rotated. Implement a version of RTEXT that puts the text in the correct position but does not rotate the text. For the rotated version of RTEXT, the “anchor" of the text was at the bottom center of the text. Where should the “anchor" of the unrotated text be located?
- In the stress test example above, I used the “Serif" and “Helvetica" fonts. Both these are generally available in the browser. What if you want to use a font that's not normally available? How do you load a font into the browser and use it in SVG? Can you write a CDL command that loads a font and makes it available?