Direction to shore d3-geo-voronoi can help deriving topologies such as basins of attraction. See also Distance to shore.
{ const projection = d3.geoEqualEarth().rotate([-12, 0]); const context = DOM.context2d(width, width * 0.5), path = d3.geoPath(projection).context(context); projection.fitExtent([[10, 10], [width - 10, width * 0.5 - 10]], { type: "Sphere" }); context.lineWidth = 1; (context.lineWidth = 0.8), points.forEach(p => { context.beginPath(), path({ type: "LineString", coordinates: [p.p, p.dir] }); (context.strokeStyle = color(p.dist * KM)), context.stroke(); }); (context.lineWidth = 1), context.beginPath(), path(spiderweb), context.setLineDash([1, 2]), (context.strokeStyle = "#000"), context.stroke(); context.setLineDash([]); (context.lineWidth = 0.5), context.beginPath(), //path(d3.geoVoronoi(vertices).cellMesh()), (context.strokeStyle = "#fefef2"), context.stroke(); (context.lineWidth = 0.5), (context.strokeStyle = "#000"), context.beginPath(), path(land), context.stroke(); path.pointRadius(2), (context.lineWidth = 2), (context.strokeStyle = color(0.6 * KM)), context.beginPath(), path({ type: "Point", coordinates: delaunay.centers[minpoint] }), context.stroke(); (context.lineWidth = 2), (context.strokeStyle = color(-0.6 * KM)), context.beginPath(), path({ type: "Point", coordinates: delaunay.centers[maxpoint] }), context.stroke(); const preclip = projection.preclip(); //projection.preclip(s => s); (context.lineWidth = 1.2), (context.strokeStyle = "#555"), context.beginPath(), path({ type: "Sphere" }), context.stroke(); //projection.preclip(preclip); return context.canvas; }
Adjust the filtration level of the medial axis:
geoVertices = function(feature) { const vertices = []; const stream = { point: function(lambda, phi) { vertices.push([lambda, phi]); }, lineStart: function() {}, lineEnd: function() {}, polygonStart: function() {}, polygonEnd: function() {} }; d3.geoStream(feature, stream); return vertices; }
vertices = geoVertices(land)
voro = d3.geoDelaunay(vertices)
function dist2shore(a) { return ( d3.geoDistance(a, vertices[voro.find(a[0], a[1])]) * (contains(a) ? 1 : -1) ); }
contains = geoContainsCache(land)
N = 12000
delaunay = d3.geoDelaunay(vertices)
edges = delaunay.edges.filter( e => d3.geoDistance(vertices[e[0]], vertices[e[1]]) > 1 - s )
spider = edges.map(e => { let p0 = delaunay.polygons[e[0]], p1 = delaunay.polygons[e[1]]; for (let i = 0; i < p0.length; i++) { for (let j = 0; j < p1.length; j++) { if ( p0[(i + 1) % p0.length] === p1[j] && p1[(j + 1) % p1.length] === p0[i] ) return [p0[i], p1[j]]; } } return null; })
spiderweb = ({ type: "MultiLineString", coordinates: spider.map(e => [delaunay.centers[e[0]], delaunay.centers[e[1]]]) })
minpoint = d3.scan(delaunay.centers.map(dist2shore))
maxpoint = d3.scan(delaunay.centers.map(d => -dist2shore(d)))
spots = { const l = 2 / Math.sqrt(N), points = [...Array.from({ length: N }).map(randompoint)]; function randompoint(delaunay) { let p, OK, tests = 10; do { p = [ 360 * Math.random() - 180, (180 / Math.PI) * Math.acos(2 * Math.random() - 1) - 90 ]; OK = !delaunay || d3.geoDistance(p, points[delaunay.find(...p)]) > l; } while (!OK && tests-- > 0); return p; } if (bluenoise) { let A, runs = 10; do { A = 0; let delaunay = d3.geoDelaunay(points); delaunay.edges.forEach(e => { if (d3.geoDistance(points[e[0]], points[e[1]]) < l) { points[e[0]] = randompoint(delaunay); A++; } }); } while (A > 0 && runs-- > 0); } return points }
points = spots.map(p => { const dist = dist2shore(p), f = vertices[voro.find(...p)], dir = d3.geoInterpolate(p, f)(0.05 / d3.geoDistance(p, f)); return { p, dist, dir }; })
color = d3 .scaleLinear() .range(["#0a0a77", "white", "brown"]) .domain([-6000, 0, 4000])
land = d3.json( "https://unpkg.com/visionscarto-world-atlas/world/110m_land.geojson" )
d3 = require("d3-interpolate", "d3-array", "d3-scale", "d3-geo", "d3-fetch", "d3-geo-voronoi", "d3-geo-projection", "d3-geo-polygon")
import {geoContainsCache} from "@fil/another-hex-map"
KM = 20000 / Math.PI