177 lines
8.2 KiB
JavaScript
177 lines
8.2 KiB
JavaScript
// Colours and legends for the painted-map layers: the class map, the uplift rate, the
|
|
// erodibility, the drainage (log area), the slope and the drainage basins.
|
|
//
|
|
// Shared by planet-mesh.js (globe, map and export colouring) and import-main.js (the sidebar
|
|
// legend), so the picture on the globe and the one in the exported PNG are the same picture.
|
|
|
|
export const PAINTED_LAYERS = new Set(['paintClass', 'paintUplift', 'paintK', 'flow', 'slope', 'basins', 'paintOverlay']);
|
|
|
|
// Export type → debug layer it draws.
|
|
export const PAINTED_EXPORT_TYPES = {
|
|
paintclass: 'paintClass',
|
|
uplift: 'paintUplift',
|
|
erodibility: 'paintK',
|
|
flow: 'flow',
|
|
slope: 'slope',
|
|
basins: 'basins',
|
|
overlay: 'paintOverlay',
|
|
};
|
|
|
|
export const PAINTED_EXPORT_LABELS = {
|
|
paintclass: 'Class Map',
|
|
uplift: 'Uplift Rate',
|
|
erodibility: 'Erodibility',
|
|
flow: 'Drainage',
|
|
slope: 'Slope',
|
|
basins: 'Basins',
|
|
overlay: 'Overlay',
|
|
};
|
|
|
|
const SEA_DARK = [0.05, 0.07, 0.12];
|
|
const SEA_GREY = [0.16, 0.18, 0.24];
|
|
|
|
function lerp3(a, b, t) {
|
|
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
}
|
|
|
|
function ramp(stops, t) {
|
|
if (t <= 0) return stops[0];
|
|
if (t >= 1) return stops[stops.length - 1];
|
|
const x = t * (stops.length - 1);
|
|
const i = Math.floor(x);
|
|
return lerp3(stops[i], stops[Math.min(stops.length - 1, i + 1)], x - i);
|
|
}
|
|
|
|
// Uplift: dark violet → wine → orange → pale yellow. Sea is dark.
|
|
const UPLIFT_STOPS = [[0.09, 0.04, 0.20], [0.42, 0.08, 0.36], [0.78, 0.25, 0.22], [0.98, 0.62, 0.12], [1.00, 0.95, 0.65]];
|
|
// Erodibility: hard rock blue → neutral grey → soft rock orange.
|
|
const K_STOPS = [[0.20, 0.35, 0.80], [0.55, 0.57, 0.62], [0.95, 0.55, 0.15]];
|
|
// Drainage: dry ground olive → light blue → white at the trunk rivers.
|
|
const FLOW_STOPS = [[0.26, 0.28, 0.16], [0.30, 0.40, 0.30], [0.35, 0.62, 0.85], [0.75, 0.90, 1.00], [1.00, 1.00, 1.00]];
|
|
// Slope: white → yellow → red → near-black.
|
|
const SLOPE_STOPS = [[0.96, 0.96, 0.94], [0.98, 0.85, 0.30], [0.90, 0.30, 0.10], [0.25, 0.05, 0.05]];
|
|
|
|
function basinColor(id) {
|
|
// Golden-ratio hue per basin, moderate saturation so neighbours differ without shouting.
|
|
const hue = ((id * 0.6180339887) % 1 + 1) % 1;
|
|
const s = 0.55, l = 0.55;
|
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
const p = 2 * l - q;
|
|
const f = (t) => {
|
|
t = ((t % 1) + 1) % 1;
|
|
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
if (t < 1 / 2) return q;
|
|
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
return p;
|
|
};
|
|
return [f(hue + 1 / 3), f(hue), f(hue - 1 / 3)];
|
|
}
|
|
|
|
/**
|
|
* Prepare a colour function for one painted layer. Returns null when the data is missing.
|
|
* The returned object has `color(regionIndex)` and the numbers the sidebar legend prints.
|
|
*/
|
|
export function preparePaintedLayer(layer, arr, curData) {
|
|
if (!arr) return null;
|
|
const painted = curData && curData.painted;
|
|
const N = arr.length;
|
|
switch (layer) {
|
|
case 'paintClass': {
|
|
const cls = painted && painted.legend && painted.legend.classes;
|
|
if (!cls) return null;
|
|
const table = cls.map(c => [c.rgb[0] / 255, c.rgb[1] / 255, c.rgb[2] / 255]);
|
|
return { color: r => table[arr[r]] || SEA_GREY, classes: cls };
|
|
}
|
|
case 'paintUplift': {
|
|
let max = 0;
|
|
for (let r = 0; r < N; r++) if (arr[r] > max) max = arr[r];
|
|
const inv = max > 0 ? 1 / max : 0;
|
|
return { color: r => arr[r] < 0 ? SEA_DARK : ramp(UPLIFT_STOPS, arr[r] * inv), max };
|
|
}
|
|
case 'paintK': {
|
|
let lo = Infinity, hi = -Infinity;
|
|
for (let r = 0; r < N; r++) { const v = arr[r]; if (v < 0) continue; if (v < lo) lo = v; if (v > hi) hi = v; }
|
|
if (!(hi > lo)) { lo = 0.5; hi = 1.5; }
|
|
// Centre the ramp on k = 1 so "harder than average" and "softer" read as colours.
|
|
const span = Math.max(hi - 1, 1 - lo, 1e-6);
|
|
return { color: r => arr[r] < 0 ? SEA_GREY : ramp(K_STOPS, 0.5 + (arr[r] - 1) / (2 * span)), lo, hi };
|
|
}
|
|
case 'flow': {
|
|
let max = 0;
|
|
for (let r = 0; r < N; r++) if (arr[r] > max) max = arr[r];
|
|
const inv = max > 0 ? 1 / max : 0;
|
|
return { color: r => { const v = arr[r]; if (v < 0) return SEA_DARK; const t = v * inv; return ramp(FLOW_STOPS, t * t); }, max };
|
|
}
|
|
case 'slope': {
|
|
const cap = 30;
|
|
return { color: r => arr[r] < 0 ? SEA_GREY : ramp(SLOPE_STOPS, Math.min(1, arr[r] / cap)), cap };
|
|
}
|
|
case 'basins':
|
|
return { color: r => arr[r] < 0 ? SEA_DARK : basinColor(arr[r]) };
|
|
case 'paintOverlay': {
|
|
// The class map halved towards black, so a full-strength mark drawn over it cannot be mistaken
|
|
// for the ground - the Go tool's map_overlay.png, drawn the same way. The sheet itself is a
|
|
// texture (painted-overlay-view.js), not a region colour.
|
|
const cls = painted && painted.legend && painted.legend.classes;
|
|
if (!cls) return null;
|
|
const table = cls.map(c => [c.rgb[0] / 510, c.rgb[1] / 510, c.rgb[2] / 510]);
|
|
return { color: r => table[arr[r]] || SEA_DARK, classes: cls };
|
|
}
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function css(c) { return `rgb(${Math.round(c[0] * 255)},${Math.round(c[1] * 255)},${Math.round(c[2] * 255)})`; }
|
|
|
|
function gradientHTML(stops, labels) {
|
|
const pcts = stops.map((_, i) => Math.round(i / (stops.length - 1) * 100));
|
|
const grad = stops.map((c, i) => `${css(c)} ${pcts[i]}%`).join(', ');
|
|
return `<div class="legend-gradient" style="background:linear-gradient(to right,${grad})"></div>` +
|
|
`<div class="legend-labels">${labels.map(l => `<span>${l}</span>`).join('')}</div>`;
|
|
}
|
|
|
|
/** Sidebar legend HTML for a painted layer, or '' when the layer has no data. `overlay` is state.overlay. */
|
|
export function paintedLegendHTML(layer, curData, overlay) {
|
|
const d = curData;
|
|
if (!d || !d.debugLayers) return '';
|
|
const prep = preparePaintedLayer(layer, d.debugLayers[layer], d);
|
|
if (!prep) return '';
|
|
switch (layer) {
|
|
case 'paintClass': {
|
|
let html = '<div class="legend-classes">';
|
|
for (const c of prep.classes) {
|
|
if (c.derived && !c.used) continue;
|
|
const what = c.sea ? `${c.depthM} m deep` : `${c.upliftMmYr} mm/yr`;
|
|
html += `<div class="legend-class"><span class="legend-koppen-swatch" style="background:rgb(${c.rgb.join(',')})"></span>${c.name} <span class="legend-class-num">${what}</span></div>`;
|
|
}
|
|
return html + '</div>';
|
|
}
|
|
case 'paintUplift':
|
|
return gradientHTML(UPLIFT_STOPS, ['0', 'Uplift, mm/yr', prep.max.toFixed(2)]);
|
|
case 'paintK':
|
|
return gradientHTML(K_STOPS, ['Hard rock', 'k = 1', 'Soft rock']);
|
|
case 'flow':
|
|
return gradientHTML(FLOW_STOPS, ['Hillslope', 'Drainage area', 'Trunk river']);
|
|
case 'slope':
|
|
return gradientHTML(SLOPE_STOPS, ['0°', 'Slope', `${prep.cap}°+`]);
|
|
case 'basins':
|
|
return '<div class="legend-labels"><span>One colour per drainage basin, by river mouth</span></div>';
|
|
case 'paintOverlay': {
|
|
if (!overlay || !overlay.legend) return '<div class="legend-labels"><span>No overlay loaded</span></div>';
|
|
const rep = overlay.report;
|
|
let html = '<div class="legend-classes">';
|
|
for (const m of overlay.legend.marks) {
|
|
const share = rep ? 100 * rep.counts[m.index] / rep.total : 0;
|
|
let what = share > 0 ? share.toFixed(2) + ' %' : 'none';
|
|
if (m.coastJitter !== null) what += m.coastJitter === 0 ? ', coast pinned' : `, coast \u00d7${m.coastJitter}`;
|
|
else if (m.kind === 'path') what += ', path';
|
|
html += `<div class="legend-class"><span class="legend-koppen-swatch" style="background:rgb(${m.rgb.join(',')})"></span>${m.name} <span class="legend-class-num">${what}</span></div>`;
|
|
}
|
|
return html + '</div><div class="legend-labels"><span>Marks over the dimmed class map</span></div>';
|
|
}
|
|
default:
|
|
return '';
|
|
}
|
|
}
|