${name}
@@ -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;
+}
diff --git a/modules/io/load.js b/modules/io/load.js
index 6208b99b..61788af9 100644
--- a/modules/io/load.js
+++ b/modules/io/load.js
@@ -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;
diff --git a/modules/ui/layers.js b/modules/ui/layers.js
index 09d7a66a..fc1943d6 100644
--- a/modules/ui/layers.js
+++ b/modules/ui/layers.js
@@ -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);
}
diff --git a/modules/ui/options.js b/modules/ui/options.js
index 863241f7..8149bc8e 100644
--- a/modules/ui/options.js
+++ b/modules/ui/options.js
@@ -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();
}
diff --git a/modules/ui/style.js b/modules/ui/style.js
index 5217d9cb..90fa7bbd 100644
--- a/modules/ui/style.js
+++ b/modules/ui/style.js
@@ -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 = '';
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 => ``)
.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 */ `
+ Define heightmap gradient colors from high to low altitude
+
+
+
+
`;
+
+ 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 =>
+ ``;
+ const removeStopButton = index =>
+ ``;
+ const addStopButton = () =>
+ ``;
+
+ 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);
diff --git a/modules/ui/stylePresets.js b/modules/ui/stylePresets.js
index 0ba995d4..c4a01dee 100644
--- a/modules/ui/stylePresets.js
+++ b/modules/ui/stylePresets.js
@@ -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);
}
}
}
diff --git a/utils/graphUtils.js b/utils/graphUtils.js
index 780dc931..fdbe7bcb 100644
--- a/utils/graphUtils.js
+++ b/utils/graphUtils.js
@@ -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");
+}
diff --git a/versioning.js b/versioning.js
index 5271014f..ec17f078 100644
--- a/versioning.js
+++ b/versioning.js
@@ -28,6 +28,7 @@ const version = "1.93.12"; // generator version, update each time
-
Latest changes:
+
- Ability to define custom heightmap color scheme
- New style preset Night and new heightmap color schemes
- Random encounter markers (integration with Deorum)
- Auto-load of the last saved map is now optional (see Onload behavior in Options)