mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-16 17:31:24 +01:00
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:
parent
778bea15ee
commit
958a2c6ef8
10 changed files with 216 additions and 64 deletions
|
|
@ -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,
|
||||
|
|
|
|||
29
index.html
29
index.html
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue