assets/emergence-map-geometry.js
import { invertEqualEarth, equalEarthScale, equalEarthFrame, wrapLongitude } from './emergence-projection.js';
// Decode our vendored M/L/Z paths once; shared mask/hatch paths reuse the result.
export function decodeWorldPath(d) {
return d.split('M').slice(1).map(part => ({
closed: part.endsWith('Z'),
points: part.replace(/Z$/, '').split('L').map(pair => invertEqualEarth(...pair.split(',').map(Number)))
}));
}
export function localPath(parts, focus, view) {
const scaleX = equalEarthScale * focus.cos * Math.PI / 180;
const width = scaleX * 360, paths = [];
for (const part of parts) {
let previous;
const points = part.points.map(([lon, lat]) => {
let delta = wrapLongitude(lon - focus.lon);
if (previous !== undefined) {
while (delta - previous > 180) delta -= 360;
while (delta - previous < -180) delta += 360;
}
previous = delta;
return [equalEarthFrame.W / 2 + delta * scaleX,
equalEarthFrame.H / 2 + (focus.sin - Math.sin(lat * Math.PI / 180)) * equalEarthScale / focus.cos];
});
let left = Infinity, right = -Infinity, top = Infinity, bottom = -Infinity;
for (const [x, y] of points) { left = Math.min(left, x); right = Math.max(right, x); top = Math.min(top, y); bottom = Math.max(bottom, y); }
// Keep a pan buffer; off-screen rings do not need SVG paint work.
if (bottom < view.y - view.h || top > view.y + view.h * 2) continue;
const nearest = Math.round((view.x + view.w / 2 - (left + right) / 2) / width);
for (let shift = nearest - 1; shift <= nearest + 1; shift++) {
const dx = shift * width;
if (right + dx < view.x - view.w || left + dx > view.x + view.w * 2) continue;
paths.push('M' + points.map(([x, y]) => `${(x + dx).toFixed(2)},${y.toFixed(2)}`).join('L') + (part.closed ? 'Z' : ''));
}
}
return paths.join('');
}
export function mapGeometry(map) {
const cache = new Map();
const paths = [...map.querySelectorAll('.em-land,.em-hot,.em-contested-ground,.em-contested-hatch,.em-border,#em-attention-mask path')]
.map(path => ({ path, original: path.getAttribute('d') }));
return {
set(focus, view) {
const projected = new Map();
for (const { path, original } of paths) {
let d = original;
if (focus) {
if (!cache.has(original)) cache.set(original, decodeWorldPath(original));
if (!projected.has(original)) projected.set(original, localPath(cache.get(original), focus, view));
d = projected.get(original);
}
path.setAttribute('d', d);
}
}
};
}