// 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 `
` + `
${labels.map(l => `${l}`).join('')}
`; } /** 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 = '
'; 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 += `
${c.name} ${what}
`; } return html + '
'; } 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 '
One colour per drainage basin, by river mouth
'; case 'paintOverlay': { if (!overlay || !overlay.legend) return '
No overlay loaded
'; const rep = overlay.report; let html = '
'; 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 += `
${m.name} ${what}
`; } return html + '
Marks over the dimmed class map
'; } default: return ''; } }