assets/emergence-projection.js
// Spherical Equal Earth: Šavrič, Patterson & Jenny (2018).
// Shared by the geometry generator and the build-time attention-dot renderer.
// https://shadedrelief.com/ee_proj/ — areas are preserved, not shapes or distances.
const A1 = 1.340264, A2 = -0.081106, A3 = 0.000893, A4 = 0.003796;
const rad = Math.PI / 180, root3 = Math.sqrt(3);
const extentX = 2 * root3 * Math.PI / (3 * A1);
export const equalEarthFrame = { name: 'Equal Earth', W: 720, H: 360, padding: 8 };
export const equalEarthScale = (equalEarthFrame.W - 2 * equalEarthFrame.padding) / (2 * extentX);
export function projectEqualEarth(lon, lat, frame = equalEarthFrame) {
const t = Math.asin(root3 / 2 * Math.sin(Math.max(-90, Math.min(90, lat)) * rad));
const t2 = t * t, t6 = t2 * t2 * t2;
const x = 2 * root3 * lon * rad * Math.cos(t) / (3 * (A1 + 3 * A2 * t2 + t6 * (7 * A3 + 9 * A4 * t2)));
const y = t * (A1 + A2 * t2 + t6 * (A3 + A4 * t2));
const scale = (frame.W - 2 * frame.padding) / (2 * extentX);
return [frame.W / 2 + x * scale, frame.H / 2 - y * scale];
}
export function invertEqualEarth(x, y, frame = equalEarthFrame) {
const scale = (frame.W - 2 * frame.padding) / (2 * extentX);
const target = (frame.H / 2 - y) / scale;
let t = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, target / A1));
for (let i = 0; i < 8; i++) {
const t2 = t * t, t6 = t2 * t2 * t2;
t -= (t * (A1 + A2 * t2 + t6 * (A3 + A4 * t2)) - target)
/ (A1 + 3 * A2 * t2 + t6 * (7 * A3 + 9 * A4 * t2));
t = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, t));
}
const t2 = t * t, t6 = t2 * t2 * t2;
const lon = (x - frame.W / 2) / scale * 3 * (A1 + 3 * A2 * t2 + t6 * (7 * A3 + 9 * A4 * t2)) / (2 * root3 * Math.cos(t));
return [lon / rad, Math.asin(Math.max(-1, Math.min(1, 2 * Math.sin(t) / root3))) / rad];
}
export const wrapLongitude = lon => ((lon + 180) % 360 + 360) % 360 - 180;
// Cylindrical equal-area, with true scale at the focus latitude.
// https://proj.org/en/stable/operations/projections/cea.html
// Intended for close views; the world view stays Equal Earth.
export function localAreaFrame(lon, lat) {
const focusLat = Math.max(-75, Math.min(75, lat));
return { lon: wrapLongitude(lon), lat: focusLat, cos: Math.cos(focusLat * rad), sin: Math.sin(focusLat * rad) };
}
export function projectLocalArea(lon, lat, focus) {
return [equalEarthFrame.W / 2 + wrapLongitude(lon - focus.lon) * rad * equalEarthScale * focus.cos,
equalEarthFrame.H / 2 + (focus.sin - Math.sin(lat * rad)) * equalEarthScale / focus.cos];
}
export function invertLocalArea(x, y, focus) {
return [wrapLongitude(focus.lon + (x - equalEarthFrame.W / 2) / (equalEarthScale * focus.cos * rad)),
Math.asin(Math.max(-1, Math.min(1, focus.sin - (y - equalEarthFrame.H / 2) * focus.cos / equalEarthScale))) / rad];
}