This post I'll be working on generating graphics to represent plains or swamps that look like these hash marks:
It turns out I've already done something like this before, when I generated something similar to implement
hachures. That word might not be familiar, but you'll recognize hachures easily enough -- they are the curved hashes used to illustrate elevation, as in the two hills on this map excerpt:
If you compare these hachures to the field symbols above, you'll see the similarities. To generate hachures, I created a curve, calculated the normal to the curve at various points, and then drew various length lines along the normals. That worked fine, although I could never really get hachures to look good on the map, so I ended up discarding that code.
With some of the code I've written since hachures I have an easier way to generate these sorts of symbols. I can create two different curves between the same end points, split each curve into an equal number of points, and then draw hash lines between matching points:
That's a mockup, let's work on the real thing. The first step is the two outer curves. In this case I'm going to use a quadratic curve -- this is a type of
Bezier curve with one control point. To create a symmetrical curve with height H, you place the control point above the center of the curve at twice the height H:
Once you have the start and end points and the control point, you can use
the equation provided here to generate points along the curve. This is a parametric equation -- the input is a number from 0 to 1 which represents the percentage along the curve, and the output is the point on the curve at that (percentage) location. So it's very easy to use this to generate some number of points along this curve, as I need to do in this case. All together, the code looks like this:
// See
// https://stackoverflow.com/questions/5634460/quadratic-b%C3%A9zier-curve-calculate-points
// t = [0, 1]
function calcQuadBPoint(start, control, end, t) {
const x = (1 - t) * (1 - t) * start[0] + 2 * (1 - t) * t * control[0] + t * t * end[0];
const y = (1 - t) * (1 - t) * start[1] + 2 * (1 - t) * t * control[1] + t * t * end[1];
return [x, y];
};
// Make a quadratic arc from pt1 to pt2 that rises to a point h above
// the midpoint, and return it as a polyline with num points.
function makeQuadraticArc(pt1, pt2, h, num) {
// Calculate the control point
const x1 = pt1[0];
const x2 = pt2[0];
const y1 = pt1[1];
const y2 = pt2[1];
const cx = (x1 + x2)/2;
const cy = (y1 + y2)/2;
const dx = (x2 - x1)/2;
const dy = (y2 - y1)/2;
const dd = Math.sqrt(dx*dx+dy*dy);
const ex = cx + (dy/dd)*h*2;
const ey = cy - (dx/dd)*h*2;
const cp = [ex, ey];
const result = [];
for(let i=0;i<num;i++) {
result.push(calcQuadBPoint(pt1, cp, pt2, (i/(num-1))));
};
return result;
};
(This code should work for any start and end point; if you just need to do it for an axis-aligned arc like I'm doing here the code can be simplified.)
So I can use that to draw two curves between the same points, one a little higher than the other.
Looks good so far. In this case, I don't actually want the curves, I want points along the curve. So let me draw points corresponding to where the lines would go:
This shows a problem -- if I draw lines between the points, the lines will all be vertical! How did that happen? If the curves are split into equal-sized pieces, the points on the top curve should be spread further apart (because that curve is longer).
Well, it turns out that the parametric function above is not linear. The points it generates will not be equally spaced along the curve -- basically the points are farther apart where the curve is steep and closer together where the curve is shallow. The result is that the points line up even when they're taken from different curves, as I'm doing.
So how do you get equally spaced points? That turns out to be
hard. The most practical approach is to sample a bunch of points along the curve (say, 100), calculate the distance between the points, and then resample to find points that are equally spaced. But that's a lot of work to draw a clump of grass.
What I need to do is either spread out the intervals around the center on the top curve or compact them on the bottom curve. As it turns out,
d3 has functions that do just this -- they take numbers on a range of 0 to 1 and remap them to the same range but with different intervals. They're called
easing functions. Maybe I can pick an easing function that will do what I need. The idea is to put the input for the parametric function through the easing function before using it for the upper curve. So for example, if I'm generating the point for 0.25 on the bottom curve, I'll use the easing function on 0.25 to get a new value -- say, 0.17 -- and then use that for the upper curve.
For this to work, I need an easing function that “slows down" values before 0.5, and “speeds up" values after 0.5. After some playing around trying out various easing functions, here is
d3.easePolyInOut with an exponent of 1.5:
This does what I want -- the points on the top curve are now spread out. And now the lines will fall away from the center outward. Higher exponents to this easing function make the lines fall away faster.
Now I will stop drawing the top and bottom line and the points and just draw lines between the points.
And that's the basic shape. It even looks pretty good just like this at map scale.
Before I go any further, I want to add the code to display the symbol on the map to get a sense of how it looks and refine the size. It's always pretty challenging to get a nice placement of symbols in a case like this. Obviously you want to spread out the symbols a bit so they don't overlap, and you want to scatter them in a more-or-less random way. But you also have to take care to avoid a lot of the other map symbols.
The top image just places symbols into the grasslands and you can see this results in symbols clashing with the forest, the coast, rivers, cities and the mountains. In the lower image I try to avoid all those things. Before I place a symbol, I check it's distance to the nearest forest, mountain, etc., and skip it if that's too small.
Taking a look at this initial version, I'm not sure I like using this to denote all grasslands; there are a lot of grasslands on the typical map and this makes the map pretty busy. We'll see. Next time I'll work on making the symbol a bit more varied.
(N.B.: A few weeks after writing this post, I realized I needed a little more control over the geometry of the hachures, and ended up changing the way I generate them. The new method still has the same upper and lower curves to find the endpoints of the individual blades, but now the curves can be separated with some height (meaning the ends of the curves don't always touch) and the curves can be different lengths -- which eliminates the need to use an easing function.)