diff --git a/index.css b/index.css index 933434cf..8e8f3f2f 100644 --- a/index.css +++ b/index.css @@ -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, diff --git a/index.html b/index.html index ff086788..2c7b6204 100644 --- a/index.html +++ b/index.html @@ -138,7 +138,7 @@ } - + @@ -1275,13 +1275,6 @@ - - Color scheme - - - - - Terracing @@ -1289,6 +1282,7 @@ 0 + Reduce layers @@ -1315,6 +1309,19 @@ + + + Color scheme + + + + + @@ -7947,7 +7954,7 @@ - + @@ -7983,11 +7990,11 @@ - + - + diff --git a/modules/dynamic/heightmap-selection.js b/modules/dynamic/heightmap-selection.js index 1fa65e5e..c81574bf 100644 --- a/modules/dynamic/heightmap-selection.js +++ b/modules/dynamic/heightmap-selection.js @@ -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 */ `
- ${name} + ${name}
${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 + heightmap preview +
+
+
`; + + 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