function draw_map_canvas_stroke(
geojson,
size = 600,
stroke_number = 8,
opacity_gradient = false,
stroke_gradient = false,
colour_scale = false,
filled = false,
colour = "teal",
stroke_thickness_const = 1,
ripple_extent = 40,
curve_this = d3.curveCatmullRomClosed,
start = 2,
stroke_gradient_values = [1, 3],
spacing_exponent = 1 / 2
) {
let projection_canvas = d3
.geoMercator()
.fitSize([size - ripple_extent, size - ripple_extent], geojson);
let projection_translate = projection_canvas.translate();
projection_canvas = projection_canvas.translate([
projection_translate[0] + ripple_extent / 2,
projection_translate[1] + ripple_extent / 2
]);
/// Smooth the stroke path /////////////////
/// using D3's curve settings //////////////
/// otherwise the strokes will be spikes ///
////////////////////////////////////////////
// code from @nbremer/simplified-curved-earth-map
function curveContext(curve) {
return {
moveTo(x, y) {
curve.lineStart();
curve.point(x, y);
},
lineTo(x, y) {
curve.point(x, y);
},
closePath() {
curve.lineEnd();
}
};
}
function geoCurvePath(curve, projection, context) {
return object => {
const pathContext = context === undefined ? d3.path() : context;
d3.geoPath(projection_canvas, curveContext(curve(pathContext)))(object);
return context === undefined ? pathContext + "" : undefined;
};
}
const context = DOM.context2d(size, size);
const path_svg = geoCurvePath(curve_this)(geojson);
let path = new Path2D(path_svg);
const path_orig = d3.geoPath(projection_canvas, context);
/////////////////////////
/// Define scales ///////
/////////////////////////
const opacity = d3
.scalePow()
.exponent(0.8)
.domain([0, stroke_number])
.range([0, 1]);
// this is to set the spacing between each waterline. It's called (perhaps confusingly) stroke_width because we're creating the waterlines by redrawing strokes of increasing width
const stroke_width = d3
.scalePow()
.exponent(spacing_exponent)
.domain([0, stroke_number])
.range([ripple_extent, start]); // start = distance from the shoreline the waterlines start
// this is to set the thickness of each waterline
const stroke_thickness = d3
.scalePow()
.exponent(3)
.domain([0, stroke_number])
.range(stroke_gradient_values);
/////////////////////////
/// Draw strokes ///////
/////////////////////////
for (let i = 0; i <= stroke_number; i++) {
// draw colour
context.beginPath();
context.strokeStyle = colour_scale
? d3.interpolateSpectral(1 - i / stroke_number)
: colour;
context.globalAlpha = opacity_gradient ? opacity(i) : 1;
context.lineWidth = stroke_gradient
? stroke_width(i) + stroke_thickness(i)
: stroke_width(i) + stroke_thickness_const;
context.stroke(path);
if (!filled) {
// clip out most of the stroke, so it becomes a line
// Based on @awoodruff/canvas-cartography-nacis-2019#masking-clipping-and-compositing which has a good explanation of context.globalCompositeOperation
context.globalAlpha = 1;
context.beginPath();
context.lineWidth = stroke_width(i);
context.globalCompositeOperation = 'destination-out'; // clipping
context.stroke(path);
context.globalCompositeOperation = 'source-over';
}
}
// draw the original (unsmoothed) geojson over the top
context.beginPath();
path_orig(geojson);
context.lineWidth = 2;
context.fillStyle = colour;
context.fill();
return context.canvas;
}