diff --git a/index.html b/index.html index eb586055..cba2c7d9 100644 --- a/index.html +++ b/index.html @@ -494,6 +494,14 @@ > Heightmap +
  • + Topography +
  • + diff --git a/main.js b/main.js index 5f937ca8..ea483865 100644 --- a/main.js +++ b/main.js @@ -50,6 +50,7 @@ let lakes = viewbox.append("g").attr("id", "lakes"); let landmass = viewbox.append("g").attr("id", "landmass"); let texture = viewbox.append("g").attr("id", "texture"); let terrs = viewbox.append("g").attr("id", "terrs"); +let topography = viewbox.append("g").attr("id", "topography"); let biomes = viewbox.append("g").attr("id", "biomes"); let cells = viewbox.append("g").attr("id", "cells"); let gridOverlay = viewbox.append("g").attr("id", "gridOverlay"); @@ -249,7 +250,7 @@ document.addEventListener("DOMContentLoaded", async () => { hideLoading(); await checkLoadParameters(); } - restoreDefaultEvents; // apply default viewbox events + restoreDefaultEvents(); // apply default viewbox events initiateAutosave(); }); diff --git a/modules/renderers/draw-topography.js b/modules/renderers/draw-topography.js new file mode 100644 index 00000000..70e675dd --- /dev/null +++ b/modules/renderers/draw-topography.js @@ -0,0 +1,125 @@ +"use strict"; + +// Draw topographic hillshade-style fill per grid cell +function drawTopography() { + TIME && console.time("drawTopography"); + + const group = d3.select("#topography"); + group.selectAll("*").remove(); + + const {cells, points, features: gridFeatures} = grid; + + // Colorimetry from user (positive altitudes, depressions, negative altitudes) + // Start positive ramp at #BAAE9A as requested + const POSITIVE_COLORS = [ + "#BAAE9A", + "#AC9A7C", + "#AA8753", + "#B9985A", + "#C3A76B", + "#CAB982", + "#D3CA9D", + "#DED6A3", + "#E8E1B6", + "#EFEBC0", + "#E1E4B5", + "#D1D7AB", + "#BDCC96", + "#A8C68F", + "#94BF8B", + "#ACD0A5" + ]; + const NEGATIVE_COLORS = [ + "#D8F2FE", + "#C6ECFF", + "#B9E3FF", + "#ACDBFB", + "#A1D2F7", + "#96C9F0", + "#8DC1EA", + "#84B9E3", + "#79B2DE", + "#71ABD8" + ]; + const DEPRESSIONS_COLOR = "#0978AB"; // lakes / inland depressions: Rivers, coasts, hydronyms color + + const posInterp = d3.interpolateRgbBasis(POSITIVE_COLORS); + const negInterp = d3.interpolateRgbBasis(NEGATIVE_COLORS); + + // Base color scheme mirrors heightmap scheme + // not used now; we fully replace with provided palette + + // Lighting settings (can be overridden by attributes) + const azimuth = (+group.attr("azimuth") || 315) * (Math.PI / 180); // degrees → radians + const altitude = (+group.attr("altitude") || 45) * (Math.PI / 180); + const light = [Math.cos(azimuth) * Math.cos(altitude), Math.sin(azimuth) * Math.cos(altitude), Math.sin(altitude)]; + const zScale = +group.attr("zscale") || 0.8; // vertical exaggeration for normals + + let html = ""; + + for (const i of cells.i) { + const h = cells.h[i]; + const isWater = h < 20; + const featureType = isWater && gridFeatures ? gridFeatures[cells.f[i]]?.type : null; + const isLake = isWater && featureType === "lake"; + + // Estimate gradient from neighbors + const [cx, cy] = points[i]; + let gx = 0, + gy = 0, + wsum = 0; + for (const n of cells.c[i]) { + if (n < 0) continue; + const [nx, ny] = points[n]; + const dx = nx - cx; + const dy = ny - cy; + const dist = Math.hypot(dx, dy) || 1; + const dh = (cells.h[n] - h) / 100; // normalize height difference + const w = 1 / dist; // weight close neighbors higher + gx += w * dh * (dx / dist); + gy += w * dh * (dy / dist); + wsum += w; + } + if (wsum) { + gx /= wsum; + gy /= wsum; + } + + // Build surface normal and compute light intensity + const nx = -gx * zScale; + const ny = -gy * zScale; + const nz = 1; + const invLen = 1 / Math.hypot(nx, ny, nz); + const n0 = [nx * invLen, ny * invLen, nz * invLen]; + const intensity = minmax(n0[0] * light[0] + n0[1] * light[1] + n0[2] * light[2], 0, 1); + + // Base color from provided palettes + let baseColor; + if (isWater) { + if (isLake) baseColor = d3.color(DEPRESSIONS_COLOR); + else { + const t = minmax((20 - h) / 20, 0, 1); // shallow 0 → deep 1 + baseColor = d3.color(negInterp(t)); + } + } else { + const t = minmax((h - 20) / 80, 0, 1); // lowland 0 → highland 1 + baseColor = d3.color(posInterp(t)); + } + + // Modulate lightness by intensity (weaker effect on water) + const hsl = d3.hsl(baseColor); + const k = isWater ? 0.4 : 0.8; + hsl.l = minmax(hsl.l * (0.6 + k * intensity), 0, 1); + const color = hsl.toString(); + + // Cell polygon + const poly = getGridPolygon(i) + .map(p => `${rn(p[0], 2)},${rn(p[1], 2)}`) + .join(" "); + html += ``; + } + + group.html(html); + + TIME && console.timeEnd("drawTopography"); +} diff --git a/modules/ui/hotkeys.js b/modules/ui/hotkeys.js index 4b1e626d..1c85d81e 100644 --- a/modules/ui/hotkeys.js +++ b/modules/ui/hotkeys.js @@ -61,6 +61,7 @@ function handleKeyup(event) { else if (key === "%") toggleAddMarker(); else if (code === "KeyX") toggleTexture(); else if (code === "KeyH") toggleHeight(); + else if (code === "KeyQ") toggleTopography(); else if (code === "KeyB") toggleBiomes(); else if (code === "KeyE") toggleCells(); else if (code === "KeyG") toggleGrid(); diff --git a/modules/ui/layers.js b/modules/ui/layers.js index a1e3b503..cbf8429f 100644 --- a/modules/ui/layers.js +++ b/modules/ui/layers.js @@ -186,6 +186,7 @@ function drawLayers() { drawFeatures(); if (layerIsOn("toggleTexture")) drawTexture(); if (layerIsOn("toggleHeight")) drawHeightmap(); + if (layerIsOn("toggleTopography")) drawTopography(); if (layerIsOn("toggleBiomes")) drawBiomes(); if (layerIsOn("toggleCells")) drawCells(); if (layerIsOn("toggleGrid")) drawGrid(); @@ -229,6 +230,18 @@ function toggleHeight(event) { } } +function toggleTopography(event) { + const group = d3.select('#topography'); + const hasContent = group.selectAll('*').size() > 0; + if (!hasContent) { + turnButtonOn('toggleTopography'); + drawTopography(); + } else { + turnButtonOff('toggleTopography'); + group.selectAll('*').remove(); + } +} + function toggleTemperature(event) { if (!temperature.selectAll("*").size()) { turnButtonOn("toggleTemperature"); @@ -1015,6 +1028,7 @@ function moveLayer(event, ui) { // define connection between option layer buttons and actual svg groups to move the element function getLayer(id) { if (id === "toggleHeight") return $("#terrs"); + if (id === "toggleTopography") return $("#topography"); if (id === "toggleBiomes") return $("#biomes"); if (id === "toggleCells") return $("#cells"); if (id === "toggleGrid") return $("#gridOverlay");