/** * Pregenerates the globe's land dot pattern from Natural Earth coastlines. * * node scripts/generate-land-dots.mjs * * Runs at author time only — world-atlas/topojson stay dev dependencies and the * browser gets a small quantized lat/lon table instead of a 400 KB topology. */ import { readFileSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { feature } from "topojson-client"; const require = createRequire(import.meta.url); const topology = JSON.parse( readFileSync(require.resolve("world-atlas/land-110m.json"), "utf8"), ); const land = feature(topology, topology.objects.land); // Flatten to rings, each with a bounding box so most tests exit immediately. const rings = []; for (const f of land.features) { const polygons = f.geometry.type === "Polygon" ? [f.geometry.coordinates] : f.geometry.coordinates; for (const polygon of polygons) { for (let i = 0; i < polygon.length; i++) { const coords = polygon[i]; let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (const [x, y] of coords) { if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; } rings.push({ coords, hole: i > 0, minX, minY, maxX, maxY }); } } } function inRing(lon, lat, ring) { if (lon < ring.minX || lon > ring.maxX || lat < ring.minY || lat > ring.maxY) { return false; } let inside = false; const c = ring.coords; for (let i = 0, j = c.length - 1; i < c.length; j = i++) { const [xi, yi] = c[i]; const [xj, yj] = c[j]; if (yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi) { inside = !inside; } } return inside; } function isLand(lon, lat) { let hit = false; for (const ring of rings) { if (!inRing(lon, lat, ring)) continue; if (ring.hole) return false; // lake / inner ring hit = true; } return hit; } const SAMPLES = 26000; const golden = Math.PI * (3 - Math.sqrt(5)); const out = []; for (let i = 0; i < SAMPLES; i++) { const y = 1 - (i / (SAMPLES - 1)) * 2; const theta = golden * i; const lat = Math.asin(y) * (180 / Math.PI); const lon = Math.atan2(Math.sin(theta), Math.cos(theta)) * (180 / Math.PI); if (!isLand(lon, lat)) continue; // Quantized to 1/100 degree — far finer than a 2 px dot can show. out.push(Math.round(lat * 100), Math.round(lon * 100)); } const header = `// Generated by scripts/generate-land-dots.mjs — do not edit by hand. // ${out.length / 2} land points sampled from Natural Earth 110m coastlines, // stored as [lat, lon] pairs in hundredths of a degree. export const LAND_LATLON: number[] = [ `; const body = []; for (let i = 0; i < out.length; i += 20) { body.push(" " + out.slice(i, i + 20).join(",") + ","); } writeFileSync( new URL("../src/lib/land-dots.ts", import.meta.url), header + body.join("\n") + "\n];\n", ); console.log(`wrote ${out.length / 2} land points from ${SAMPLES} samples`);