Custom heightmap color scheme (#1013)

* feat: custom heightmap color scheme

* feat: custom heightmap color scheme - add shceme on load

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
This commit is contained in:
Azgaar 2023-11-18 16:34:41 +04:00 committed by GitHub
parent 778bea15ee
commit 958a2c6ef8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 216 additions and 64 deletions

View file

@ -625,7 +625,7 @@ input[type="color"]::-webkit-color-swatch-wrapper {
.tabcontent button.sideButton {
border-radius: 15%;
font-size: 0.8em;
margin-bottom: -1em;
margin-block: -1em;
}
#layersContent button.active,

View file

@ -138,7 +138,7 @@
}
</style>
<link rel="preload" href="index.css?v=1.93.10" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="index.css?v=1.93.12" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="icons.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="libs/jquery-ui.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
</head>
@ -1275,13 +1275,6 @@
</tbody>
<tbody id="styleHeightmap">
<tr data-tip="Select color scheme for the element">
<td>Color scheme</td>
<td>
<select id="styleHeightmapScheme"></select>
</td>
</tr>
<tr data-tip="Terracing rate. Set to 0 (toggle off) to improve performance">
<td>Terracing</td>
<td>
@ -1289,6 +1282,7 @@
<output id="styleHeightmapTerracingOutput">0</output>
</td>
</tr>
<tr data-tip="Layers reduction rate. Increase to improve performance">
<td>Reduce layers</td>
<td>
@ -1315,6 +1309,19 @@
</select>
</td>
</tr>
<tr data-tip="Select color scheme for the element">
<td>Color scheme</td>
<td>
<select id="styleHeightmapScheme"></select>
<button
id="openCreateHeightmapSchemeButton"
data-tip="Click to add a custom heightmap color scheme"
data-stops="#ffffff,#EEEECC,#D2B48C,#008000,#008080"
class="icon-plus sideButton"
></button>
</td>
</tr>
</tbody>
<tbody id="styleArmies">
@ -7947,7 +7954,7 @@
<script src="utils/commonUtils.js?v=1.89.29"></script>
<script src="utils/arrayUtils.js"></script>
<script src="utils/colorUtils.js"></script>
<script src="utils/graphUtils.js?v=1.90.01"></script>
<script src="utils/graphUtils.js?v=1.93.12"></script>
<script src="utils/nodeUtils.js"></script>
<script src="utils/numberUtils.js?v=1.89.08"></script>
<script src="utils/polyfills.js?v=1.93.00"></script>
@ -7983,11 +7990,11 @@
<script src="modules/ui/stylePresets.js?v=1.93.07"></script>
<script src="modules/ui/general.js?v=1.93.04"></script>
<script src="modules/ui/options.js?v=1.93.11"></script>
<script src="modules/ui/options.js?v=1.93.12"></script>
<script src="main.js?v=1.93.02"></script>
<script defer src="modules/relief-icons.js"></script>
<script defer src="modules/ui/style.js?v=1.93.07"></script>
<script defer src="modules/ui/style.js?v=1.93.12"></script>
<script defer src="modules/ui/editors.js?v=1.93.10"></script>
<script defer src="modules/ui/tools.js?v=1.92.00"></script>
<script defer src="modules/ui/world-configurator.js?v=1.91.05"></script>

View file

@ -199,10 +199,9 @@ function insertHtml() {
const name = heightmapTemplates[key].name;
Math.random = aleaPRNG(initialSeed);
const heights = HeightmapGenerator.fromTemplate(graph, key);
const dataUrl = drawHeights(heights);
return /* html */ `<article data-id="${key}" data-seed="${initialSeed}">
<img src="${dataUrl}" alt="${name}" />
<img src="${getHeightmapPreview(heights)}" alt="${name}" />
<div>
${name}
<span data-tip="Regenerate preview" class="icon-cw regeneratePreview"></span>
@ -266,42 +265,16 @@ function getGraph(currentGraph) {
return newGraph;
}
function drawHeights(heights) {
const canvas = document.createElement("canvas");
canvas.width = graph.cellsX;
canvas.height = graph.cellsY;
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(graph.cellsX, graph.cellsY);
const scheme = getColorScheme(byId("heightmapSelectionColorScheme").value);
const renderOcean = byId("heightmapSelectionRenderOcean").checked;
const getHeight = height => (height < 20 ? (renderOcean ? height : 0) : height);
for (let i = 0; i < heights.length; i++) {
const color = scheme(1 - getHeight(heights[i]) / 100);
const {r, g, b} = d3.color(color);
const n = i * 4;
imageData.data[n] = r;
imageData.data[n + 1] = g;
imageData.data[n + 2] = b;
imageData.data[n + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
return canvas.toDataURL("image/png");
}
function drawTemplatePreview(id) {
const heights = HeightmapGenerator.fromTemplate(graph, id);
const dataUrl = drawHeights(heights);
const dataUrl = getHeightmapPreview(heights);
const article = byId("heightmapSelection").querySelector(`[data-id="${id}"]`);
article.querySelector("img").src = dataUrl;
}
async function drawPrecreatedHeightmap(id) {
const heights = await HeightmapGenerator.fromPrecreated(graph, id);
const dataUrl = drawHeights(heights);
const dataUrl = getHeightmapPreview(heights);
const article = byId("heightmapSelection").querySelector(`[data-id="${id}"]`);
article.querySelector("img").src = dataUrl;
}
@ -337,3 +310,10 @@ function confirmHeightmapEdit() {
onConfirm: () => editHeightmap({mode: "erase", tool})
});
}
function getHeightmapPreview(heights) {
const scheme = getColorScheme(byId("heightmapSelectionColorScheme").value);
const renderOcean = byId("heightmapSelectionRenderOcean").checked;
const dataUrl = drawHeights({heights, width: grid.cellsX, height: grid.cellsY, scheme, renderOcean});
return dataUrl;
}

View file

@ -454,12 +454,20 @@ async function parseLoadedData(data) {
})();
{
// dynamically import and run auto-udpdate script
// dynamically import and run auto-update script
const versionNumber = parseFloat(params[0]);
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.93.00");
resolveVersionConflicts(versionNumber);
}
{
// add custom heightmap color scheme if any
const scheme = terrs.attr("scheme");
if (!(scheme in heightmapColorSchemes)) {
addCustomColorScheme(scheme);
}
}
void (function checkDataIntegrity() {
const cells = pack.cells;

View file

@ -297,11 +297,6 @@ function drawHeightmap() {
TIME && console.timeEnd("drawHeightmap");
}
function getColorScheme(scheme = "bright") {
if (scheme in heightmapColorSchemes) return heightmapColorSchemes[scheme];
throw new Error(`Unsupported color scheme: ${scheme}`);
}
function getColor(value, scheme = getColorScheme("bright")) {
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
}

View file

@ -702,7 +702,7 @@ function changeEra() {
}
async function openTemplateSelectionDialog() {
const HeightmapSelectionDialog = await import("../dynamic/heightmap-selection.js?v=1.93.07");
const HeightmapSelectionDialog = await import("../dynamic/heightmap-selection.js?v=1.93.12");
HeightmapSelectionDialog.open();
}

View file

@ -3,7 +3,7 @@
// add available filters to lists
{
const filters = Array.from(document.getElementById("filters").querySelectorAll("filter"));
const filters = Array.from(byId("filters").querySelectorAll("filter"));
const emptyOption = '<option value="" selected>None</option>';
const options = filters.map(filter => {
const id = filter.getAttribute("id");
@ -12,8 +12,8 @@
});
const allOptions = emptyOption + options.join("");
document.getElementById("styleFilterInput").innerHTML = allOptions;
document.getElementById("styleStatesBodyFilter").innerHTML = allOptions;
byId("styleFilterInput").innerHTML = allOptions;
byId("styleStatesBodyFilter").innerHTML = allOptions;
}
// store some style inputs as options
@ -37,20 +37,37 @@ function editStyle(element, group) {
}, 1500);
}
// Color schemes
const heightmapColorSchemes = {
bright: d3.scaleSequential(d3.interpolateSpectral),
light: d3.scaleSequential(d3.interpolateRdYlGn),
natural: d3.scaleSequential(d3.interpolateRgbBasis(["white", "#EEEECC", "tan", "green", "teal"])),
green: d3.scaleSequential(d3.interpolateGreens),
olive: d3.scaleSequential(d3.interpolateRgbBasis(["#ffffff", "#cea48d", "#d5b085", "#0c2c19", "#151320"])),
livid: d3.scaleSequential(d3.interpolateRgbBasis(["#BBBBDD", "#2A3440", "#17343B", "#0A1E24"])),
monochrome: d3.scaleSequential(d3.interpolateGreys)
};
// add color schemes to the lists
document.getElementById("styleHeightmapScheme").innerHTML = Object.keys(heightmapColorSchemes)
// add default color schemes to the list of options
byId("styleHeightmapScheme").innerHTML = Object.keys(heightmapColorSchemes)
.map(scheme => `<option value="${scheme}">${scheme}</option>`)
.join("");
function addCustomColorScheme(scheme) {
const stops = scheme.split(",");
heightmapColorSchemes[scheme] = d3.scaleSequential(d3.interpolateRgbBasis(stops));
byId("styleHeightmapScheme").options.add(new Option(scheme, scheme, false, true));
}
function getColorScheme(scheme = "bright") {
if (!(scheme in heightmapColorSchemes)) {
const colors = scheme.split(",");
heightmapColorSchemes[scheme] = d3.scaleSequential(d3.interpolateRgbBasis(colors));
}
return heightmapColorSchemes[scheme];
}
// Toggle style sections on element select
styleElementSelect.addEventListener("change", selectStyleElement);
function selectStyleElement() {
@ -278,9 +295,9 @@ function selectStyleElement() {
if (sel === "ocean") {
styleOcean.style.display = "block";
styleOceanFill.value = styleOceanFillOutput.value = oceanLayers.select("#oceanBase").attr("fill");
styleOceanPattern.value = document.getElementById("oceanicPattern")?.getAttribute("href");
styleOceanPattern.value = byId("oceanicPattern")?.getAttribute("href");
styleOceanPatternOpacity.value = styleOceanPatternOpacityOutput.value =
document.getElementById("oceanicPattern").getAttribute("opacity") || 1;
byId("oceanicPattern").getAttribute("opacity") || 1;
outlineLayers.value = oceanLayers.attr("layers");
}
@ -313,7 +330,7 @@ function selectStyleElement() {
// update group options
styleGroupSelect.options.length = 0; // remove all options
if (["routes", "labels", "coastline", "lakes", "anchors", "burgIcons", "borders"].includes(sel)) {
const groups = document.getElementById(sel).querySelectorAll("g");
const groups = byId(sel).querySelectorAll("g");
groups.forEach(el => {
if (el.id === "burgLabels") return;
const option = new Option(`${el.id} (${el.childElementCount})`, el.id, false, false);
@ -458,11 +475,11 @@ styleOceanFill.addEventListener("input", function () {
});
styleOceanPattern.addEventListener("change", function () {
document.getElementById("oceanicPattern")?.setAttribute("href", this.value);
byId("oceanicPattern")?.setAttribute("href", this.value);
});
styleOceanPatternOpacity.addEventListener("input", function () {
document.getElementById("oceanicPattern").setAttribute("opacity", this.value);
byId("oceanicPattern").setAttribute("opacity", this.value);
styleOceanPatternOpacityOutput.value = this.value;
});
@ -477,6 +494,127 @@ styleHeightmapScheme.addEventListener("change", function () {
drawHeightmap();
});
openCreateHeightmapSchemeButton.addEventListener("click", function () {
// start with current scheme
this.dataset.stops = terrs.attr("scheme").startsWith("#")
? terrs.attr("scheme")
: (function () {
const scheme = heightmapColorSchemes[terrs.attr("scheme")];
return [0, 0.25, 0.5, 0.75, 1].map(scheme).map(toHEX).join(",");
})();
// render dialog base structure
alertMessage.innerHTML = /* html */ `<div>
<i>Define heightmap gradient colors from high to low altitude</i>
<img id="heightmapSchemePreview" alt="heightmap preview" style="margin-top: 0.5em; width: 100%;" />
<div id="heightmapSchemeStops" style="margin-block: 0.5em; display: flex; flex-wrap: wrap;"></div>
<div id="heightmapSchemeGradient" style="height: 1.9em; border: 1px solid #767676;"></div>
</div>`;
renderPreview();
renderStops();
renderGradient();
function renderPreview() {
const stops = openCreateHeightmapSchemeButton.dataset.stops.split(",");
const scheme = d3.scaleSequential(d3.interpolateRgbBasis(stops));
const preview = drawHeights({
heights: grid.cells.h,
width: grid.cellsX,
height: grid.cellsY,
scheme,
renderOcean: false
});
byId("heightmapSchemePreview").src = preview;
}
function renderStops() {
const stops = openCreateHeightmapSchemeButton.dataset.stops.split(",");
const colorInput = color =>
`<input type="color" class="stop" value="${color}" data-tip="Click to set the color" style="width: 2.5em; border: none;" />`;
const removeStopButton = index =>
`<button class="remove" data-index="${index}" data-tip="Remove color stop" style="margin-top: 0.3em; height: max-content;">x</button>`;
const addStopButton = () =>
`<button class="add" data-tip="Add color stop in between" style="margin-top: 0.3em; height: max-content;">+</button>`;
const container = byId("heightmapSchemeStops");
container.innerHTML = stops
.map(
(stop, index) => `${colorInput(stop)}
${index && index < stops.length - 1 ? removeStopButton(index) : ""}`
)
.join(addStopButton());
Array.from(container.querySelectorAll("input.stop")).forEach(
(input, index) =>
(input.oninput = function () {
stops[index] = this.value;
openCreateHeightmapSchemeButton.dataset.stops = stops.join(",");
renderPreview();
renderGradient();
})
);
Array.from(container.querySelectorAll("button.remove")).forEach(
button =>
(button.onclick = function () {
const index = +this.dataset.index;
stops.splice(index, 1);
openCreateHeightmapSchemeButton.dataset.stops = stops.join(",");
renderPreview();
renderStops();
renderGradient();
})
);
Array.from(container.querySelectorAll("button.add")).forEach(
(button, index) =>
(button.onclick = function () {
const middleColor = d3.interpolateRgb(stops[index], stops[index + 1])(0.5);
stops.splice(index + 1, 0, toHEX(middleColor));
openCreateHeightmapSchemeButton.dataset.stops = stops.join(",");
renderPreview();
renderStops();
renderGradient();
})
);
}
function renderGradient() {
const stops = openCreateHeightmapSchemeButton.dataset.stops;
byId("heightmapSchemeGradient").style.background = `linear-gradient(to right, ${stops})`;
}
function handleCreate() {
const stops = openCreateHeightmapSchemeButton.dataset.stops;
if (stops in heightmapColorSchemes) return tip("This scheme already exists", false, "error");
addCustomColorScheme(stops);
terrs.attr("scheme", stops);
drawHeightmap();
handleClose();
}
function handleClose() {
$("#alert").dialog("close");
}
$("#alert").dialog({
resizable: false,
title: "Create heightmap color scheme",
width: "28em",
buttons: {
Create: handleCreate,
Cancel: handleClose
},
position: {my: "center top+150", at: "center top", of: "svg"}
});
});
styleHeightmapTerracingInput.addEventListener("input", function () {
terrs.attr("terracing", this.value);
drawHeightmap();
@ -801,7 +939,7 @@ function fetchTextureURL(url) {
INFO && console.log("Provided URL is", url);
const img = new Image();
img.onload = function () {
const canvas = document.getElementById("texturePreview");
const canvas = byId("texturePreview");
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

View file

@ -97,9 +97,7 @@ function applyStyle(style) {
// add custom heightmap color scheme
if (selector === "#terrs" && attribute === "scheme" && !(value in heightmapColorSchemes)) {
const colors = value.split(",");
heightmapColorSchemes[value] = d3.scaleSequential(d3.interpolateRgbBasis(colors));
document.getElementById("styleHeightmapScheme").options.add(new Option(value, value));
addCustomColorScheme(value);
}
}
}

View file

@ -325,7 +325,7 @@ function drawCellsValue(data) {
.text(d => d);
}
// helper function non-used for the generation
// helper function non-used for the main generation
function drawPolygons(data) {
const max = d3.max(data),
min = d3.min(data),
@ -342,3 +342,28 @@ function drawPolygons(data) {
.attr("fill", d => scheme(d))
.attr("stroke", d => scheme(d));
}
// draw raster heightmap preview (not used in main generation)
function drawHeights({heights, width, height, scheme, renderOcean}) {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(width, height);
const getHeight = height => (height < 20 ? (renderOcean ? height : 0) : height);
for (let i = 0; i < heights.length; i++) {
const color = scheme(1 - getHeight(heights[i]) / 100);
const {r, g, b} = d3.color(color);
const n = i * 4;
imageData.data[n] = r;
imageData.data[n + 1] = g;
imageData.data[n + 2] = b;
imageData.data[n + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
return canvas.toDataURL("image/png");
}

View file

@ -28,6 +28,7 @@ const version = "1.93.12"; // generator version, update each time
<ul>
<strong>Latest changes:</strong>
<li>Ability to define custom heightmap color scheme</li>
<li>New style preset Night and new heightmap color schemes</li>
<li>Random encounter markers (integration with <a href="https://deorum.vercel.app/" target="_blank">Deorum</a>)</li>
<li>Auto-load of the last saved map is now optional (see <i>Onload behavior</i> in Options)</li>