diff --git a/index.css b/index.css index ac5669c5..b49a0558 100644 --- a/index.css +++ b/index.css @@ -1028,10 +1028,10 @@ div#regimentSelectorBody > div > div { } .color-div { - width: 2.5em; + width: 3em; height: 1em; display: inline-block; - margin: .1em .2em; + margin: 0 .16em; border: 1px #c5c5c5 groove; cursor: pointer; } diff --git a/index.html b/index.html index 4a64ad88..ad3f4e32 100644 --- a/index.html +++ b/index.html @@ -1883,8 +1883,8 @@ - - + +
You can also keep all the data, but you won't be able to change the coastline.
-If you need to change the coastline and keep the data, you may try the risk edit option. - The data will be restored as much as possible, but the coastline change can cause unexpected fluctuations and errors.
-Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.
-Please save the map before editing the heightmap!
`; + alertMessage.innerHTML = `Heightmap is a core element on which all other data (rivers, burgs, states etc) is based. + So the best edit approach is to erase the secondary data and let the system automatically regenerate it on edit completion. +Erase mode also allows you Convert an Image into a heightmap or use Template Editor.
+You can keep the data, but you won't be able to change the coastline.
+Try risk mode to change the coastline and keep the data. The data will be restored as much as possible, but it can cause unpredictable errors.
+Please save the map before editing the heightmap!
+Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.
`; $("#alert").dialog({resizable: false, title: "Edit Heightmap", width: "28em", buttons: { @@ -61,9 +61,9 @@ function editHeightmap() { changeOnlyLand.checked = false; } - // hide convert and template buttons for the Keep mode - applyTemplate.style.display = type === "keep" ? "none" : "inline-block"; - convertImage.style.display = type === "keep" ? "none" : "inline-block"; + // show convert and template buttons for Erase mode only + applyTemplate.style.display = type === "erase" ? "inline-block" : "none"; + convertImage.style.display = type === "erase" ? "inline-block" : "none"; // hide erosion checkbox if mode is Keep changeHeightsBox.style.display = type === "keep" ? "none" : "inline-block"; @@ -963,10 +963,11 @@ function editHeightmap() { function openImageConverter() { if ($("#imageConverter").is(":visible")) return; + imageToLoad.click(); closeDialogs("#imageConverter"); $("#imageConverter").dialog({ - title: "Image Converter", maxHeight: svgHeight*.75, minHeight: "auto", width: "19.5em", resizable: false, + title: "Image Converter", maxHeight: svgHeight*.8, minHeight: "auto", width: "20em", position: {my: "right top", at: "right-10 top+10", of: "svg"}, beforeClose: closeImageConverter }); @@ -978,15 +979,9 @@ function editHeightmap() { canvas.height = graphHeight; document.body.insertBefore(canvas, optionsContainer); - const img = new Image; - img.id = "image"; - img.style.display = "none"; - document.body.appendChild(img); - setOverlayOpacity(0); - - document.getElementById("convertImageLoad").classList.add("glow"); // add glow effect - tip('Image Converter is opened. Upload the image and assign height value for each of the colors', true, "warn"); // main tip + clearMainTip(); + tip('Image Converter is opened. Upload image and assign height value for each color', false, "warn"); // main tip // remove all heights grid.cells.h = new Uint8Array(grid.cells.i.length); @@ -1001,7 +996,7 @@ function editHeightmap() { d3.select("#imageConverterPalette").selectAll("div").data(d3.range(101)) .enter().append("div").attr("data-color", i => i) .style("background-color", i => color(1-(i < 20 ? i-5 : i) / 100)) - .style("width", i => i < 20 || i > 70 ? ".2em" : ".1em") + .style("width", i => i < 40 || i > 68 ? ".2em" : ".1em") .on("touchmove mousemove", showPalleteHeight).on("click", assignHeight); }() @@ -1010,6 +1005,7 @@ function editHeightmap() { document.getElementById("imageToLoad").addEventListener("change", loadImage); document.getElementById("convertAutoLum").addEventListener("click", () => autoAssing("lum")); document.getElementById("convertAutoHue").addEventListener("click", () => autoAssing("hue")); + document.getElementById("convertAutoFMG").addEventListener("click", () => autoAssing("scheme")); document.getElementById("convertColorsButton").addEventListener("click", setConvertColorsNumber); document.getElementById("convertComplete").addEventListener("click", applyConversion); document.getElementById("convertCancel").addEventListener("click", cancelConversion); @@ -1030,12 +1026,12 @@ function editHeightmap() { this.value = ""; // reset input value to get triggered if the file is re-uploaded const reader = new FileReader(); + const img = new Image; img.onload = function() { const ctx = document.getElementById("canvas").getContext("2d"); ctx.drawImage(img, 0, 0, graphWidth, graphHeight); heightsFromImage(+convertColors.value); resetZoom(); - convertImageLoad.classList.remove("glow"); }; reader.onloadend = () => img.src = reader.result; @@ -1043,37 +1039,35 @@ function editHeightmap() { } function heightsFromImage(count) { - const ctx = document.getElementById("canvas").getContext("2d"); + const sourceImage = document.getElementById("canvas"); + const sampleCanvas = document.createElement("canvas"); + sampleCanvas.width = grid.cellsX; + sampleCanvas.height = grid.cellsY; + sampleCanvas.getContext('2d').drawImage(sourceImage, 0, 0, grid.cellsX, grid.cellsY); + const q = new RgbQuant({colors:count}); - q.sample(ctx); - const data = q.reduce(ctx); + q.sample(sampleCanvas); + const data = q.reduce(sampleCanvas); + const pallete = q.palette(true); viewbox.select("#heights").selectAll("*").remove(); d3.select("#imageConverter").selectAll("div.color-div").remove(); colorsSelect.style.display = "block"; colorsUnassigned.style.display = "block"; colorsAssigned.style.display = "none"; - - let usedColors = new Set(); - let gridColors = grid.points.map(p => { - const x = Math.floor(p[0]-.01), y = Math.floor(p[1]-.01); - const i = (x + y * graphWidth) * 4; - const r = data[i], g = data[i+1], b = data[i+2]; - usedColors.add(`rgb(${r},${g},${b})`); - return [r, g, b]; - }); + sampleCanvas.remove(); // no need to keep viewbox.select("#heights").selectAll("polygon").data(grid.cells.i).join("polygon") - .attr("points", d => getGridPolygon(d)) - .attr("id", d => "cell"+d).attr("fill", d => `rgb(${gridColors[d].join(",")})`) + .attr("points", d => getGridPolygon(d)).attr("id", d => "cell"+d) + .attr("fill", d => `rgb(${data[d*4]}, ${data[d*4+1]}, ${data[d*4+2]})`) .on("click", mapClicked); - const unassigned = [...usedColors].sort((a, b) => d3.lab(a).l - d3.lab(b).l); - d3.select("#colorsUnassigned").selectAll("div").data(unassigned).enter().append("div") + const colors = pallete.map(p => `rgb(${p[0]}, ${p[1]}, ${p[2]})`); + d3.select("#colorsUnassigned").selectAll("div").data(colors).enter().append("div") .attr("data-color", i => i).style("background-color", i => i) .attr("class", "color-div").on("click", colorClicked); - convertColors.value = unassigned.length; + document.getElementById("colorsUnassignedNumber").innerHTML = colors.length; } function mapClicked() { @@ -1123,34 +1117,60 @@ function editHeightmap() { if (selectedColor.parentNode.id === "colorsUnassigned") { colorsAssigned.appendChild(selectedColor); colorsAssigned.style.display = "block"; + + document.getElementById("colorsUnassignedNumber").innerHTML = colorsUnassigned.childElementCount - 2; + document.getElementById("colorsAssignedNumber").innerHTML = colorsAssigned.childElementCount - 2; } } // auto assign color based on luminosity or hue function autoAssing(type) { - const unassigned = colorsUnassigned.querySelectorAll("div"); - if (!unassigned.length) {tip("No unassigned colors. Please load an image and click the button again", false, "error"); return;} + let unassigned = colorsUnassigned.querySelectorAll("div"); + if (!unassigned.length) { + heightsFromImage(+convertColors.value); + unassigned = colorsUnassigned.querySelectorAll("div"); + if (!unassigned.length) { + tip("No unassigned colors. Please load an image and click the button again", false, "error"); + return; + } + } - const assinged = []; // assigned heights + const getHeightByHue = function(color) { + let hue = d3.hsl(color).h; + if (hue > 300) hue -= 360; + if (hue > 170) return Math.abs(hue-250) / 3 |0; // water + return Math.abs(hue-250+20) / 3 |0; // land + } + + const getHeightByLum = function(color) { + let lum = d3.lab(color).l; + if (lum < 13) return lum / 13 * 20 |0; // water + return lum|0; // land + } + + const scheme = d3.range(101).map(i => getColor(i, color())); + const hues = scheme.map(rgb => d3.hsl(rgb).h|0); + const getHeightByScheme = function(color) { + let height = scheme.indexOf(color); + if (height !== -1) return height; // exact match + const hue = d3.hsl(color).h; + const closest = hues.reduce((prev, curr) => (Math.abs(curr - hue) < Math.abs(prev - hue) ? curr : prev)); + return hues.indexOf(closest); + } + + const assinged = []; // store assigned heights unassigned.forEach(el => { - const colorFrom = el.dataset.color; - const lab = d3.lab(colorFrom); - const normalized = type === "hue" ? rn(normalize(lab.b + lab.a / 2, -50, 200), 2) : rn(normalize(lab.l, -15, 100), 2); - let heightTo = rn(normalized * 100); - if (assinged[heightTo] && heightTo < 100) heightTo += 1; // if height is already added, try increased one - if (assinged[heightTo] && heightTo < 100) heightTo += 1; // if height is already added, try increased one - if (assinged[heightTo] && heightTo > 3) heightTo -= 3; // if increased one is also added, try decreased one - if (assinged[heightTo] && heightTo > 1) heightTo -= 1; // if increased one is also added, try decreased one + const clr = el.dataset.color; + const height = type === "hue" ? getHeightByHue(clr) : type === "lum" ? getHeightByLum(clr) : getHeightByScheme(clr); + const colorTo = color(1 - (height < 20 ? (height-5) / 100 : height / 100)); + viewbox.select("#heights").selectAll("polygon[fill='" + clr + "']").attr("fill", colorTo).attr("data-height", height); - const colorTo = color(1 - (heightTo < 20 ? (heightTo-5)/100 : heightTo/100)); - viewbox.select("#heights").selectAll("polygon[fill='" + colorFrom + "']").attr("fill", colorTo).attr("data-height", heightTo); - - if (assinged[heightTo]) {el.remove(); return;} // if color is already added, remove it + if (assinged[height]) {el.remove(); return;} // if color is already added, remove it el.style.backgroundColor = el.dataset.color = colorTo; - el.dataset.height = heightTo; + el.dataset.height = height; colorsAssigned.appendChild(el); - assinged[heightTo] = true; + assinged[height] = true; }); // sort assigned colors by height @@ -1160,10 +1180,11 @@ function editHeightmap() { colorsAssigned.style.display = "block"; colorsUnassigned.style.display = "none"; + document.getElementById("colorsAssignedNumber").innerHTML = colorsAssigned.childElementCount - 2; } function setConvertColorsNumber() { - prompt(`Please set maximum number of colors.