source.fact.ngo a coherence.ngo project

assets/emergence-map-geometry.js

raw ↗ · AGPL-3.0

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); } } }; }