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 150f4b52..b623ad59 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/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..77284913 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 @@ -47,7 +47,7 @@ const heightmapColorSchemes = { }; // add color schemes to the lists -document.getElementById("styleHeightmapScheme").innerHTML = Object.keys(heightmapColorSchemes) +byId("styleHeightmapScheme").innerHTML = Object.keys(heightmapColorSchemes) .map(scheme => ``) .join(""); @@ -278,9 +278,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 +313,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 +458,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 +477,129 @@ 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"); + + heightmapColorSchemes[stops] = d3.scaleSequential(d3.interpolateRgbBasis(stops.split(","))); + byId("styleHeightmapScheme").options.add(new Option(stops, stops, false, true)); + + 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 +924,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/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 57ad9593..ec17f078 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.93.11"; // generator version, update each time +const version = "1.93.12"; // generator version, update each time { document.title += " v" + version; @@ -28,6 +28,7 @@ const version = "1.93.11"; // generator version, update each time