Fantasy-Map-Generator/modules/ui/heightmap-editor.js
2020-04-11 15:18:35 +03:00

1293 lines
54 KiB
JavaScript

// heightmap-editor module. To be added to window as for now
"use strict";
function editHeightmap() {
void function selectEditMode() {
alertMessage.innerHTML = `<span>Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
So the best edit approach is to <i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.</span>
<p>You can also <i>keep</i> all the data, but you won't be able to change the coastline.</p>
<p>If you need to change the coastline and keep the data, you may try the <i>risk</i> edit option.
The data will be restored as much as possible, but the coastline change can cause unexpected fluctuations and errors.</p>
<p>Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.</p>
<p>Please <span class="pseudoLink" onclick=saveMap(); editHeightmap();>save the map</span> before editing the heightmap!</p>`;
$("#alert").dialog({resizable: false, title: "Edit Heightmap", width: "28em",
buttons: {
Erase: function() {enterHeightmapEditMode("erase");},
Keep: function() {enterHeightmapEditMode("keep");},
Risk: function() {enterHeightmapEditMode("risk");},
Cancel: function() {$(this).dialog("close");}
}
});
}()
let edits = [];
restartHistory();
viewbox.insert("g", "#terrs").attr("id", "heights");
if (modules.editHeightmap) return;
modules.editHeightmap = true;
// add listeners
document.getElementById("paintBrushes").addEventListener("click", openBrushesPanel);
document.getElementById("applyTemplate").addEventListener("click", openTemplateEditor);
document.getElementById("convertImage").addEventListener("click", openImageConverter);
document.getElementById("heightmapPreview").addEventListener("click", toggleHeightmapPreview);
document.getElementById("heightmap3DView").addEventListener("click", changeViewMode);
document.getElementById("finalizeHeightmap").addEventListener("click", finalizeHeightmap);
document.getElementById("renderOcean").addEventListener("click", mockHeightmap);
document.getElementById("templateUndo").addEventListener("click", () => restoreHistory(edits.n-1));
document.getElementById("templateRedo").addEventListener("click", () => restoreHistory(edits.n+1));
function enterHeightmapEditMode(type) {
editHeightmap.layers = Array.from(mapLayers.querySelectorAll("li:not(.buttonoff)")).map(node => node.id); // store layers preset
editHeightmap.layers.forEach(l => document.getElementById(l).click()); // turn off all layers
customization = 1;
closeDialogs();
tip('Heightmap edit mode is active. Click on "Exit Customization" to finalize the heightmap', true);
customizationMenu.style.display = "block";
toolsContent.style.display = "none";
heightmapEditMode.innerHTML = type;
if (type === "erase") {
undraw();
changeOnlyLand.checked = false;
} else if (type === "keep") {
viewbox.selectAll("#landmass, #lakes").style("display", "none");
changeOnlyLand.checked = true;
} else if (type === "risk") {
defs.selectAll("#land, #water").selectAll("path").remove();
viewbox.selectAll("#coastline path, #lakes path, #oceanLayers path").remove();
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";
// hide erosion checkbox if mode is Keep
changeHeightsBox.style.display = type === "keep" ? "none" : "inline-block";
// show finalize button
if (!sessionStorage.getItem("noExitButtonAnimation")) {
sessionStorage.setItem("noExitButtonAnimation", true);
exitCustomization.style.opacity = 0;
const width = 12 * uiSizeOutput.value * 11;
exitCustomization.style.right = (svgWidth - width) / 2 + "px";
exitCustomization.style.bottom = svgHeight / 2 + "px";
exitCustomization.style.transform = "scale(2)";
exitCustomization.style.display = "block";
d3.select("#exitCustomization")
.transition().duration(1000).style("opacity", 1)
.transition().duration(2000).ease(d3.easeSinInOut).style("right", "10px").style("bottom", "10px").style("transform", "scale(1)");
} else exitCustomization.style.display = "block";
openBrushesPanel();
turnButtonOn("toggleHeight");
layersPreset.value = "heightmap";
layersPreset.disabled = true;
mockHeightmap();
viewbox.on("touchmove mousemove", moveCursor);
}
function moveCursor() {
const p = d3.mouse(this), cell = findGridCell(p[0], p[1]);
heightmapInfoX.innerHTML = rn(p[0]);
heightmapInfoY.innerHTML = rn(p[1]);
heightmapInfoCell.innerHTML = cell;
heightmapInfoHeight.innerHTML = `${grid.cells.h[cell]} (${getHeight(grid.cells.h[cell])})`;
if (tooltip.dataset.main) showMainTip();
// move radius circle if drag mode is active
const pressed = document.getElementById("brushesButtons").querySelector("button.pressed");
if (!pressed) return;
moveCircle(p[0], p[1], brushRadius.valueAsNumber, "#333");
}
// get user-friendly (real-world) height value from map data
function getHeight(h) {
const unit = heightUnit.value;
let unitRatio = 3.281; // default calculations are in feet
if (unit === "m") unitRatio = 1; // if meter
else if (unit === "f") unitRatio = 0.5468; // if fathom
let height = -990;
if (h >= 20) height = Math.pow(h - 18, +heightExponentInput.value);
else if (h < 20 && h > 0) height = (h - 20) / h * 50;
return rn(height * unitRatio) + " " + unit;
}
// Exit customization mode
function finalizeHeightmap() {
if (viewbox.select("#heights").selectAll("*").size() < 200) {
tip("Insufficient land area! There should be at least 200 land cells to finalize the heightmap", null, "error");
return;
}
if (document.getElementById("imageConverter").offsetParent) {
tip("Please exit the Image Conversion mode first", null, "error");
return;
}
customization = 0;
customizationMenu.style.display = "none";
if (document.getElementById("options").querySelector(".tab > button.active").id === "toolsTab") toolsContent.style.display = "block";
layersPreset.disabled = false;
exitCustomization.style.display = "none"; // hide finalize button
restoreDefaultEvents();
clearMainTip();
closeDialogs();
resetZoom();
restartHistory();
if (document.getElementById("preview")) document.getElementById("preview").remove();
if (document.getElementById("canvas3d")) enterStandardView();
const mode = heightmapEditMode.innerHTML;
if (mode === "erase") regenerateErasedData();
else if (mode === "keep") restoreKeptData();
else if (mode === "risk") restoreRiskedData();
// restore initial layers
//viewbox.select("#heights").remove();
document.getElementById("heights").remove();
turnButtonOff("toggleHeight");
document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) {
if (editHeightmap.layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on
else if (!editHeightmap.layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
});
getCurrentPreset();
}
function regenerateErasedData() {
console.group("Edit Heightmap");
console.time("regenerateErasedData");
const change = changeHeights.checked;
markFeatures();
if (change) openNearSeaLakes();
OceanLayers();
calculateTemperatures();
generatePrecipitation();
reGraph();
drawCoastline();
elevateLakes();
Rivers.generate(change);
if (!change) {
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
if (pack.cells.h[i] !== grid.cells.h[g] && pack.cells.h[i] >= 20 === grid.cells.h[g] >= 20) pack.cells.h[i] = grid.cells.h[g];
}
}
defineBiomes();
rankCells();
Cultures.generate();
Cultures.expand();
BurgsAndStates.generate();
Religions.generate();
BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces();
BurgsAndStates.defineBurgFeatures();
drawStates();
drawBorders();
BurgsAndStates.drawStateLabels();
Rivers.specify();
Military.generate();
addMarkers();
addZones();
console.timeEnd("regenerateErasedData");
console.groupEnd("Edit Heightmap");
}
function restoreKeptData() {
viewbox.selectAll("#landmass, #lakes").style("display", null);
for (const i of pack.cells.i) {
pack.cells.h[i] = grid.cells.h[pack.cells.g[i]];
}
}
function restoreRiskedData() {
console.group("Edit Heightmap");
console.time("restoreRiskedData");
// assign pack data to grid cells
const l = grid.cells.i.length;
const biome = new Uint8Array(l);
const pop = new Uint16Array(l);
const road = new Uint16Array(l);
const crossroad = new Uint16Array(l);
const s = new Uint16Array(l);
const burg = new Uint16Array(l);
const state = new Uint16Array(l);
const province = new Uint16Array(l);
const culture = new Uint16Array(l);
const religion = new Uint16Array(l);
// rivers data, stored only if changeHeights is unchecked
const fl = new Uint16Array(l);
const r = new Uint16Array(l);
const conf = new Uint8Array(l);
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
biome[g] = pack.cells.biome[i];
culture[g] = pack.cells.culture[i];
pop[g] = pack.cells.pop[i];
road[g] = pack.cells.road[i];
crossroad[g] = pack.cells.crossroad[i];
s[g] = pack.cells.s[i];
state[g] = pack.cells.state[i];
province[g] = pack.cells.province[i];
burg[g] = pack.cells.burg[i];
religion[g] = pack.cells.religion[i];
if (!changeHeights.checked) {
fl[g] = pack.cells.fl[i];
r[g] = pack.cells.r[i];
conf[g] = pack.cells.conf[i];
}
}
// do not allow to remove land with burgs
for (const i of grid.cells.i) {
if (!burg[i]) continue;
if (grid.cells.h[i] < 20) grid.cells.h[i] = 20;
}
// save culture centers x and y to restore center cell id after re-graph
for (const c of pack.cultures) {
if (!c.i || c.removed) continue;
const p = pack.cells.p[c.center];
c.x = p[0];
c.y = p[1];
}
// recalculate zones to grid
zones.selectAll("g").each(function() {
const zone = d3.select(this);
const dataCells = zone.attr("data-cells");
const cells = dataCells ? dataCells.split(",").map(i => +i) : [];
const g = cells.map(i => pack.cells.g[i]);
zone.attr("data-cells", g);
zone.selectAll("*").remove();
});
markFeatures();
OceanLayers();
calculateTemperatures();
generatePrecipitation();
reGraph();
drawCoastline();
if (changeHeights.checked) {
elevateLakes();
Rivers.generate(changeHeights.checked);
}
// assign saved pack data from grid back to pack
const n = pack.cells.i.length;
pack.cells.pop = new Float32Array(n);
pack.cells.road = new Uint16Array(n);
pack.cells.crossroad = new Uint16Array(n);
pack.cells.s = new Uint16Array(n);
pack.cells.burg = new Uint16Array(n);
pack.cells.state = new Uint16Array(n);
pack.cells.province = new Uint16Array(n);
pack.cells.culture = new Uint16Array(n);
pack.cells.religion = new Uint16Array(n);
pack.cells.biome = new Uint8Array(n);
if (!changeHeights.checked) {
pack.cells.r = new Uint16Array(n);
pack.cells.conf = new Uint8Array(n);
pack.cells.fl = new Uint16Array(n);
}
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
if (pack.features[pack.cells.f[i]].group === "freshwater") pack.cells.h[i] = 19; // de-elevate lakes
const land = pack.cells.h[i] >= 20;
// check biome
if (!biome[g]) pack.cells.biome[i] = getBiomeId(grid.cells.prec[g], grid.cells.temp[g]);
else if (!land && biome[g]) pack.cells.biome[i] = 0;
else pack.cells.biome[i] = biome[g];
// rivers data
if (!changeHeights.checked) {
pack.cells.r[i] = r[g];
pack.cells.conf[i] = conf[g];
pack.cells.fl[i] = fl[g];
}
if (!land) continue;
pack.cells.culture[i] = culture[g];
pack.cells.pop[i] = pop[g];
pack.cells.road[i] = road[g];
pack.cells.crossroad[i] = crossroad[g];
pack.cells.s[i] = s[g];
pack.cells.state[i] = state[g];
pack.cells.province[i] = province[g];
pack.cells.religion[i] = religion[g];
}
// find closest land cell to burg
const findBurgCell = function(x, y) {
let i = findCell(x, y);
if (pack.cells.h[i] >= 20) return i;
const dist = pack.cells.c[i].map(c =>
pack.cells.h[c] < 20 ? Infinity : (pack.cells.p[c][0] - x) ** 2 + (pack.cells.p[c][1] - y) ** 2
);
return pack.cells.c[i][d3.scan(dist)];
}
// find best cell for burgs
for (const b of pack.burgs) {
if (!b.i || b.removed) continue;
b.cell = findBurgCell(b.x, b.y);
b.feature = pack.cells.f[b.cell];
pack.cells.burg[b.cell] = b.i;
if (!b.capital && pack.cells.h[b.cell] < 20) removeBurg(b.i);
if (b.capital) pack.states[b.state].center = b.cell;
}
for (const p of pack.provinces) {
if (!p.i || p.removed) continue;
const provCells = pack.cells.i.filter(i => pack.cells.province[i] === p.i);
if (!provCells.length) {
const state = p.state;
const stateProvs = pack.states[state].provinces;
if (stateProvs.includes(p.i)) pack.states[state].provinces.splice(stateProvs.indexOf(p), 1);
p.removed = true;
continue;
}
if (p.burg && !pack.burgs[p.burg].removed) p.center = pack.burgs[p.burg].cell;
else {p.center = provCells[0]; p.burg = pack.cells.burg[p.center];}
}
for (const c of pack.cultures) {
if (!c.i || c.removed) continue;
c.center = findCell(c.x, c.y);
}
BurgsAndStates.drawStateLabels();
drawStates();
drawBorders();
if (changeHeights.checked) Rivers.specify();
// restore zones from grid
zones.selectAll("g").each(function() {
const zone = d3.select(this);
const g = zone.attr("data-cells");
const gCells = g ? g.split(",").map(i => +i) : [];
const cells = pack.cells.i.filter(i => gCells.includes(pack.cells.g[i]));
zone.attr("data-cells", cells);
zone.selectAll("*").remove();
const base = zone.attr("id") + "_"; // id generic part
zone.selectAll("*").data(cells).enter().append("polygon")
.attr("points", d => getPackPolygon(d)).attr("id", d => base + d);
});
console.timeEnd("restoreRiskedData");
console.groupEnd("Edit Heightmap");
}
// trigger heightmap redraw and history update if at least 1 cell is changed
function updateHeightmap() {
const prev = last(edits);
const changed = grid.cells.h.reduce((s, h, i) => h !== prev[i] ? s+1 : s, 0);
tip("Cells changed: " + changed);
if (!changed) return;
// check ocean cells are not checged if olny land edit is allowed
if (changeOnlyLand.checked) {
for (const i of grid.cells.i) {
if (prev[i] < 20 || grid.cells.h[i] < 20) grid.cells.h[i] = prev[i];
}
}
mockHeightmap();
updateHistory();
}
// draw or update heightmap
function mockHeightmap() {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20);
const scheme = getColorScheme();
viewbox.select("#heights").selectAll("polygon").data(data).join("polygon")
.attr("points", d => getGridPolygon(d)).attr("id", d => "cell"+d)
.attr("fill", d => getColor(grid.cells.h[d], scheme));
}
// draw or update heightmap for a selection of cells
function mockHeightmapSelection(selection) {
const ocean = renderOcean.checked;
const scheme = getColorScheme();
selection.forEach(function(i) {
let cell = viewbox.select("#heights").select("#cell"+i);
if (!ocean && grid.cells.h[i] < 20) {cell.remove(); return;}
if (!cell.size()) cell = viewbox.select("#heights").append("polygon").attr("points", getGridPolygon(i)).attr("id", "cell"+i);
cell.attr("fill", getColor(grid.cells.h[i], scheme));
});
}
function updateStatistics() {
const landCells = grid.cells.h.reduce((s, h) => h >= 20 ? s+1 : s);
landmassCounter.innerHTML = `${landCells} (${rn(landCells/grid.cells.i.length*100)}%)`;
landmassAverage.innerHTML = rn(d3.mean(grid.cells.h));
}
function updateHistory(noStat) {
const step = edits.n;
edits = edits.slice(0, step);
edits[step] = grid.cells.h.slice();
edits.n = step + 1;
undo.disabled = templateUndo.disabled = edits.n <= 1;
redo.disabled = templateRedo.disabled = true;
if (!noStat) {
updateStatistics();
if (document.getElementById("preview")) drawHeightmapPreview(); // update heightmap preview if opened
if (document.getElementById("canvas3d")) ThreeD.redraw(); // update 3d heightmap preview if opened
}
}
// restoreHistory
function restoreHistory(step) {
edits.n = step;
redo.disabled = templateRedo.disabled = edits.n >= edits.length;
undo.disabled = templateUndo.disabled = edits.n <= 1;
if (edits[edits.n - 1] === undefined) return;
grid.cells.h = edits[edits.n - 1].slice();
mockHeightmap();
updateStatistics();
if (document.getElementById("preview")) drawHeightmapPreview(); // update heightmap preview if opened
if (document.getElementById("canvas3d")) ThreeD.redraw(); // update 3d heightmap preview if opened
}
// restart edits from 1st step
function restartHistory() {
edits = [];
edits.n = 0;
redo.disabled = templateRedo.disabled = true;
undo.disabled = templateUndo.disabled = true;
updateHistory();
}
function openBrushesPanel() {
if ($("#brushesPanel").is(":visible")) return;
$("#brushesPanel").dialog({
title: "Paint Brushes", resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
}).on('dialogclose', exitBrushMode);
if (modules.openBrushesPanel) return;
modules.openBrushesPanel = true;
// add listeners
document.getElementById("brushesButtons").addEventListener("click", e => toggleBrushMode(e));
document.getElementById("changeOnlyLand").addEventListener("click", e => changeOnlyLandClick(e));
document.getElementById("undo").addEventListener("click", () => restoreHistory(edits.n-1));
document.getElementById("redo").addEventListener("click", () => restoreHistory(edits.n+1));
document.getElementById("rescaleShow").addEventListener("click", () => {
document.getElementById("modifyButtons").style.display = "none";
document.getElementById("rescaleSection").style.display = "block";
});
document.getElementById("rescaleHide").addEventListener("click", () => {
document.getElementById("modifyButtons").style.display = "block";
document.getElementById("rescaleSection").style.display = "none";
});
document.getElementById("rescaler").addEventListener("change", (e) => rescale(e.target.valueAsNumber));
document.getElementById("rescaleCondShow").addEventListener("click", () => {
document.getElementById("modifyButtons").style.display = "none";
document.getElementById("rescaleCondSection").style.display = "block";
});
document.getElementById("rescaleCondHide").addEventListener("click", () => {
document.getElementById("modifyButtons").style.display = "block";
document.getElementById("rescaleCondSection").style.display = "none";
});
document.getElementById("rescaleExecute").addEventListener("click", rescaleWithCondition);
document.getElementById("smoothHeights").addEventListener("click", smoothAllHeights);
document.getElementById("disruptHeights").addEventListener("click", disruptAllHeights);
document.getElementById("brushClear").addEventListener("click", startFromScratch);
function exitBrushMode() {
const pressed = document.querySelector("#brushesButtons > button.pressed");
if (!pressed) return;
pressed.classList.remove("pressed");
viewbox.style("cursor", "default").on(".drag", null);
removeCircle();
document.getElementById("brushesSliders").style.display = "none";
}
function toggleBrushMode(e) {
if (e.target.classList.contains("pressed")) {exitBrushMode(); return;}
exitBrushMode();
document.getElementById("brushesSliders").style.display = "block";
e.target.classList.add("pressed");
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", dragBrush));
}
function dragBrush() {
const r = brushRadius.valueAsNumber;
const point = d3.mouse(this);
const start = findGridCell(point[0], point[1]);
d3.event.on("drag", () => {
const p = d3.mouse(this);
moveCircle(p[0], p[1], r, "#333");
if (~~d3.event.sourceEvent.timeStamp % 5 != 0) return; // slow down the edit
const inRadius = findGridAll(p[0], p[1], r);
const selection = changeOnlyLand.checked ? inRadius.filter(i => grid.cells.h[i] >= 20) : inRadius;
if (selection && selection.length) changeHeightForSelection(selection, start);
});
d3.event.on("end", updateHeightmap);
}
function changeHeightForSelection(s, start) {
const power = brushPower.valueAsNumber;
const interpolate = d3.interpolateRound(power, 1);
const land = changeOnlyLand.checked;
function lim(v) {return Math.max(Math.min(v, 100), land ? 20 : 0);}
const h = grid.cells.h;
const brush = document.querySelector("#brushesButtons > button.pressed").id;
if (brush === "brushRaise") s.forEach(i => h[i] = h[i] < 20 ? 20 : lim(h[i] + power)); else
if (brush === "brushElevate") s.forEach((i,d) => h[i] = lim(h[i] + interpolate(d/Math.max(s.length-1, 1)))); else
if (brush === "brushLower") s.forEach(i => h[i] = lim(h[i] - power)); else
if (brush === "brushDepress") s.forEach((i,d) => h[i] = lim(h[i] - interpolate(d/Math.max(s.length-1, 1)))); else
if (brush === "brushAlign") s.forEach(i => h[i] = lim(h[start])); else
if (brush === "brushSmooth") s.forEach(i => h[i] = rn((d3.mean(grid.cells.c[i].filter(i => land ? h[i] >= 20 : 1).map(c => h[c])) + h[i]*(10-power) + .6) / (11-power),1)); else
if (brush === "brushDisrupt") s.forEach(i => h[i] = h[i] < 15 ? h[i] : lim(h[i] + power/1.6 - Math.random()*power));
mockHeightmapSelection(s);
// updateHistory(); uncomment to update history every step
}
function changeOnlyLandClick(e) {
if (heightmapEditMode.innerHTML !== "keep") return;
e.preventDefault();
tip("You cannot change the coastline in 'Keep' edit mode", false, "error");
}
function rescale(v) {
const land = changeOnlyLand.checked;
grid.cells.h = grid.cells.h.map(h => land && (h < 20 || h+v < 20) ? h : lim(h+v));
updateHeightmap();
document.getElementById("rescaler").value = 0;
}
function rescaleWithCondition() {
const range = rescaleLower.value + "-" + rescaleHigher.value;
const operator = conditionSign.value;
const operand = rescaleModifier.valueAsNumber;
if (Number.isNaN(operand)) {tip("Operand should be a number", false, "error"); return;}
if ((operator === "add" || operator === "subtract") && !Number.isInteger(operand)) {tip("Operand should be an integer", false, "error"); return;}
if (operator === "multiply") HeightmapGenerator.modify(range, 0, operand, 0); else
if (operator === "divide") HeightmapGenerator.modify(range, 0, 1 / operand, 0); else
if (operator === "add") HeightmapGenerator.modify(range, operand, 1, 0); else
if (operator === "subtract") HeightmapGenerator.modify(range, -1 * operand, 1, 0); else
if (operator === "exponent") HeightmapGenerator.modify(range, 0, 1, operand);
updateHeightmap();
}
function smoothAllHeights() {
HeightmapGenerator.smooth(4, 1.5);
updateHeightmap();
}
function disruptAllHeights() {
grid.cells.h = grid.cells.h.map(h => h < 15 ? h : lim(h + 2.5 - Math.random() * 4));
updateHeightmap();
}
function startFromScratch() {
if (changeOnlyLand.checked) {tip("Not allowed when 'Change only land cells' mode is set", false, "error"); return;}
const someHeights = grid.cells.h.some(h => h);
if (!someHeights) {tip("Heightmap is already cleared, please do not click twice if not required", false, "error"); return;}
grid.cells.h = new Uint8Array(grid.cells.i.length);
viewbox.select("#heights").selectAll("*").remove();
updateHistory();
}
}
function openTemplateEditor() {
if ($("#templateEditor").is(":visible")) return;
const body = document.getElementById("templateBody");
$("#templateEditor").dialog({
title: "Template Editor", minHeight: "auto", width: "fit-content", resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
if (modules.openTemplateEditor) return;
modules.openTemplateEditor = true;
$("#templateBody").sortable({items: "> div", handle: ".icon-resize-vertical", containment: "#templateBody", axis: "y"});
// add listeners
body.addEventListener("click", function(ev) {
const el = ev.target;
if (el.classList.contains("icon-check")) {
el.classList.remove("icon-check");
el.classList.add("icon-check-empty");
el.parentElement.style.opacity = .5;
body.dataset.changed = 1;
return;
}
if (el.classList.contains("icon-check-empty")) {
el.classList.add("icon-check");
el.classList.remove("icon-check-empty");
el.parentElement.style.opacity = 1;
return;
}
if (el.classList.contains("icon-trash-empty")) {
el.parentElement.remove(); return;
}
});
document.getElementById("templateTools").addEventListener("click", e => addStepOnClick(e));
document.getElementById("templateSelect").addEventListener("change", e => selectTemplate(e));
document.getElementById("templateRun").addEventListener("click", executeTemplate);
document.getElementById("templateSave").addEventListener("click", downloadTemplate);
document.getElementById("templateLoad").addEventListener("click", () => templateToLoad.click());
document.getElementById("templateToLoad").addEventListener("change", function() {uploadFile(this, uploadTemplate)});
function addStepOnClick(e) {
if (e.target.tagName !== "BUTTON") return;
const type = e.target.id.replace("template", "");
document.getElementById("templateBody").dataset.changed = 1;
addStep(type);
}
function addStep(type, count, dist, arg4, arg5) {
const body = document.getElementById("templateBody");
body.insertAdjacentHTML("beforeend", getStepHTML(type, count, dist, arg4, arg5));
const elDist = body.querySelector("div:last-child").querySelector(".templateDist");
if (elDist) elDist.addEventListener("change", setRange);
if (dist && elDist && elDist.tagName === "SELECT") {
for (const o of elDist.options) {if (o.value === dist) elDist.value = dist;}
if (elDist.value !== dist) {
const opt = document.createElement("option");
opt.value = opt.innerHTML = dist;
elDist.add(opt);
elDist.value = dist;
}
}
}
function getStepHTML(type, count, arg3, arg4, arg5) {
const Trash = `<i class="icon-trash-empty pointer" data-tip="Click to remove the step"></i>`;
const Hide = `<div class="icon-check" data-tip="Click to skip the step"></div>`;
const Reorder = `<i class="icon-resize-vertical" data-tip="Drag to reorder"></i>`;
const common = `<div data-type="${type}">${Hide}<div style="width:4em">${type}</div>${Trash}${Reorder}`;
const TempY = `<span>y:<input class="templateY" data-tip="Placement range percentage along Y axis (minY-maxY)" value=${arg5||"20-80"}></span>`;
const TempX = `<span>x:<input class="templateX" data-tip="Placement range percentage along X axis (minX-maxX)" value=${arg4||"15-85"}></span>`;
const Height = `<span>h:<input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value=${arg3||"40-50"}></span>`;
const Count = `<span>n:<input class="templateCount" data-tip="Blobs to add, use hyphen to get a random number in range" value=${count||"1-2"}></span>`;
const blob = `${common}${TempY}${TempX}${Height}${Count}</div>`;
if (type === "Hill" || type === "Pit" || type === "Range" || type === "Trough") return blob;
if (type === "Strait") return `${common}<span>d:<select class="templateDist" data-tip="Strait direction"><option value="vertical" selected>vertical</option><option value="horizontal">horizontal</option></select></span><span>w:<input class="templateCount" data-tip="Strait width, use hyphen to get a random number in range" value=${count||"2-7"}></span></div>`;
if (type === "Add") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Add value to height of all cells (negative values are allowed)" type="number" value=${count||-10} min=-100 max=100 step=1></span></div>`;
if (type === "Multiply") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Multiply all cells Height by the value" type="number" value=${count||1.1} min=0 max=10 step=.1></span></div>`;
if (type === "Smooth") return `${common}<span>f:<input class="templateCount" data-tip="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min=1 max=10 value=${count||2}></span></div>`;
}
function setRange(event) {
if (event.target.value !== "interval") return;
prompt("Set a height interval. Avoid space, use hyphen as a separator", {default:"17-20"}, v => {
const opt = document.createElement("option");
opt.value = opt.innerHTML = v;
event.target.add(opt);
event.target.value = v;
});
}
function selectTemplate(e) {
const body = document.getElementById("templateBody");
const steps = body.querySelectorAll("div").length;
const changed = +body.getAttribute("data-changed");
const template = e.target.value;
if (!steps || !changed) {changeTemplate(template); return;}
alertMessage.innerHTML = "Are you sure you want to select a different template? All changes will be lost.";
$("#alert").dialog({resizable: false, title: "Change Template",
buttons: {
Change: function() {changeTemplate(template); $(this).dialog("close");},
Cancel: function() {$(this).dialog("close");}}
});
}
function changeTemplate(template) {
const body = document.getElementById("templateBody");
body.setAttribute("data-changed", 0);
body.innerHTML = "";
if (template === "templateVolcano") {
addStep("Hill", "1", "90-100", "44-56", "40-60");
addStep("Multiply", .8, "50-100");
addStep("Range", "1.5", "30-55", "45-55", "40-60");
addStep("Smooth", 2);
addStep("Hill", "1.5", "25-35", "25-30", "20-75");
addStep("Hill", "1", "25-35", "75-80", "25-75");
addStep("Hill", "0.5", "20-25", "10-15", "20-25");
}
else if (template === "templateHighIsland") {
addStep("Hill", "1", "90-100", "65-75", "47-53");
addStep("Add", 5, "all");
addStep("Hill", "6", "20-23", "25-55", "45-55");
addStep("Range", "1", "40-50", "45-55", "45-55");
addStep("Smooth", 2);
addStep("Trough", "2-3", "20-30", "20-30", "20-30");
addStep("Trough", "2-3", "20-30", "60-80", "70-80");
addStep("Hill", "1", "10-15", "60-60", "50-50");
addStep("Hill", "1.5", "13-16", "15-20", "20-75");
addStep("Multiply", .8, "20-100");
addStep("Range", "1.5", "30-40", "15-85", "30-40");
addStep("Range", "1.5", "30-40", "15-85", "60-70");
addStep("Pit", "2-3", "10-15", "15-85", "20-80");
}
else if (template === "templateLowIsland") {
addStep("Hill", "1", "90-99", "60-80", "45-55");
addStep("Hill", "4-5", "25-35", "20-65", "40-60");
addStep("Range", "1", "40-50", "45-55", "45-55");
addStep("Smooth", 3);
addStep("Trough", "1.5", "20-30", "15-85", "20-30");
addStep("Trough", "1.5", "20-30", "15-85", "70-80");
addStep("Hill", "1.5", "10-15", "5-15", "20-80");
addStep("Hill", "1", "10-15", "85-95", "70-80");
addStep("Pit", "3-5", "10-15", "15-85", "20-80");
addStep("Multiply", .4, "20-100");
}
else if (template === "templateContinents") {
addStep("Hill", "1", "80-85", "75-80", "40-60");
addStep("Hill", "1", "80-85", "20-25", "40-60");
addStep("Multiply", .22, "20-100");
addStep("Hill", "5-6", "15-20", "25-75", "20-82");
addStep("Range", ".8", "30-60", "5-15", "20-45");
addStep("Range", ".8", "30-60", "5-15", "55-80");
addStep("Range", "0-3", "30-60", "80-90", "20-80");
addStep("Trough", "3-4", "15-20", "15-85", "20-80");
addStep("Strait", "2", "vertical");
addStep("Smooth", 2);
addStep("Trough", "1-2", "5-10", "45-55", "45-55");
addStep("Pit", "3-4", "10-15", "15-85", "20-80");
addStep("Hill", "1", "5-10", "40-60", "40-60");
}
else if (template === "templateArchipelago") {
addStep("Add", 11, "all");
addStep("Range", "2-3", "40-60", "20-80", "20-80");
addStep("Hill", "5", "15-20", "10-90", "30-70");
addStep("Hill", "2", "10-15", "10-30", "20-80");
addStep("Hill", "2", "10-15", "60-90", "20-80");
addStep("Smooth", 3);
addStep("Trough", "10", "20-30", "5-95", "5-95");
addStep("Strait", "2", "vertical");
addStep("Strait", "2", "horizontal");
}
else if (template === "templateAtoll") {
addStep("Hill", "1", "75-80", "50-60", "45-55");
addStep("Hill", "1.5", "30-50", "25-75", "30-70");
addStep("Hill", ".5", "30-50", "25-35", "30-70");
addStep("Smooth", 1);
addStep("Multiply", .2, "25-100");
addStep("Hill", ".5", "10-20", "50-55", "48-52");
}
else if (template === "templateMediterranean") {
addStep("Range", "3-4", "30-50", "0-100", "0-10");
addStep("Range", "3-4", "30-50", "0-100", "90-100");
addStep("Hill", "5-6", "30-70", "0-100", "0-5");
addStep("Hill", "5-6", "30-70", "0-100", "95-100");
addStep("Smooth", 1);
addStep("Hill", "2-3", "30-70", "0-5", "20-80");
addStep("Hill", "2-3", "30-70", "95-100", "20-80");
addStep("Multiply", .8, "land");
addStep("Trough", "3-5", "40-50", "0-100", "0-10");
addStep("Trough", "3-5", "40-50", "0-100", "90-100");
}
else if (template === "templatePeninsula") {
addStep("Range", "2-3", "20-35", "40-50", "0-15");
addStep("Add", 5, "all");
addStep("Hill", "1", "90-100", "10-90", "0-5");
addStep("Add", 13, "all");
addStep("Hill", "3-4", "3-5", "5-95", "80-100");
addStep("Hill", "1-2", "3-5", "5-95", "40-60");
addStep("Trough", "5-6", "10-25", "5-95", "5-95");
addStep("Smooth", 3);
}
else if (template === "templatePangea") {
addStep("Hill", "1-2", "25-40", "15-50", "0-10");
addStep("Hill", "1-2", "5-40", "50-85", "0-10");
addStep("Hill", "1-2", "25-40", "50-85", "90-100");
addStep("Hill", "1-2", "5-40", "15-50", "90-100");
addStep("Hill", "8-12", "20-40", "20-80", "48-52");
addStep("Smooth", 2);
addStep("Multiply", .7, "land");
addStep("Trough", "3-4", "25-35", "5-95", "10-20");
addStep("Trough", "3-4", "25-35", "5-95", "80-90");
addStep("Range", "5-6", "30-40", "10-90", "35-65");
}
else if (template === "templateIsthmus") {
addStep("Hill", "5-10", "15-30", "0-30", "0-20");
addStep("Hill", "5-10", "15-30", "10-50", "20-40");
addStep("Hill", "5-10", "15-30", "30-70", "40-60");
addStep("Hill", "5-10", "15-30", "50-90", "60-80");
addStep("Hill", "5-10", "15-30", "70-100", "80-100");
addStep("Smooth", 2);
addStep("Trough", "4-8", "15-30", "0-30", "0-20");
addStep("Trough", "4-8", "15-30", "10-50", "20-40");
addStep("Trough", "4-8", "15-30", "30-70", "40-60");
addStep("Trough", "4-8", "15-30", "50-90", "60-80");
addStep("Trough", "4-8", "15-30", "70-100", "80-100");
}
else if (template === "templateShattered") {
addStep("Hill", "8", "35-40", "15-85", "30-70");
addStep("Trough", "10-20", "40-50", "5-95", "5-95");
addStep("Range", "5-7", "30-40", "10-90", "20-80");
addStep("Pit", "12-20", "30-40", "15-85", "20-80");
}
}
function executeTemplate() {
const body = document.getElementById("templateBody");
const steps = body.querySelectorAll("#templateBody > div");
if (!steps.length) return;
grid.cells.h = new Uint8Array(grid.cells.i.length); // clean all heights
for (const s of steps) {
if (s.style.opacity == .5) continue;
const type = s.getAttribute("data-type");
const elCount = s.querySelector(".templateCount") || "";
const elHeight = s.querySelector(".templateHeight") || "";
const elDist = s.querySelector(".templateDist");
const dist = elDist ? elDist.value : null;
const templateX = s.querySelector(".templateX");
const x = templateX ? templateX.value : null;
const templateY = s.querySelector(".templateY");
const y = templateY ? templateY.value : null;
if (type === "Hill") HeightmapGenerator.addHill(elCount.value, elHeight.value, x, y); else
if (type === "Pit") HeightmapGenerator.addPit(elCount.value, elHeight.value, x, y); else
if (type === "Range") HeightmapGenerator.addRange(elCount.value, elHeight.value, x, y); else
if (type === "Trough") HeightmapGenerator.addTrough(elCount.value, elHeight.value, x, y); else
if (type === "Strait") HeightmapGenerator.addStrait(elCount.value, dist); else
if (type === "Add") HeightmapGenerator.modify(dist, +elCount.value, 1); else
if (type === "Multiply") HeightmapGenerator.modify(dist, 0, +elCount.value); else
if (type === "Smooth") HeightmapGenerator.smooth(+elCount.value);
updateHistory("noStat"); // update history every step
}
updateStatistics();
mockHeightmap();
if (document.getElementById("preview")) drawHeightmapPreview(); // update heightmap preview if opened
if (document.getElementById("canvas3d")) ThreeD.redraw(); // update 3d heightmap preview if opened
}
function downloadTemplate() {
const body = document.getElementById("templateBody");
body.dataset.changed = 0;
const steps = body.querySelectorAll("#templateBody > div");
if (!steps.length) return;
let data = "";
for (const s of steps) {
if (s.style.opacity == .5) continue;
const type = s.getAttribute("data-type");
const elCount = s.querySelector(".templateCount");
const count = elCount ? elCount.value : "0";
const elHeight = s.querySelector(".templateHeight");
const elDist = s.querySelector(".templateDist");
const arg3 = elHeight ? elHeight.value : elDist ? elDist.value : "0";
const templateX = s.querySelector(".templateX");
const x = templateX ? templateX.value : "0";
const templateY = s.querySelector(".templateY");
const y = templateY ? templateY.value : "0";
data += `${type} ${count} ${arg3} ${x} ${y}\r\n`;
}
const name = "template_" + Date.now() + ".txt";
downloadFile(data, name);
}
function uploadTemplate(dataLoaded) {
const steps = dataLoaded.split("\r\n");
if (!steps.length) {tip("Cannot parse the template, please check the file", false, "error"); return;}
templateBody.innerHTML = "";
for (const s of steps) {
const step = s.split(" ");
if (step.length !== 5) {console.error("Cannot parse step, wrong arguments count", s); continue;}
addStep(step[0], step[1], step[2], step[3], step[4]);
}
}
}
function openImageConverter() {
if ($("#imageConverter").is(":visible")) return;
closeDialogs("#imageConverter");
$("#imageConverter").dialog({
title: "Image Converter", minHeight: "auto", width: "19.5em", resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"},
beforeClose: closeImageConverter
});
// create canvas for image
const canvas = document.createElement("canvas");
canvas.id = "canvas";
canvas.width = graphWidth;
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 the height for each of the colors', true, "warn"); // main tip
// remove all heights
grid.cells.h = new Uint8Array(grid.cells.i.length);
viewbox.select("#heights").selectAll("*").remove();
updateHistory();
if (modules.openImageConverter) return;
modules.openImageConverter = true;
// add color pallete
void function createColorPallete() {
const container = d3.select("#colorScheme");
container.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")
.on("touchmove mousemove", showPalleteHeight).on("click", assignHeight);
}()
// add listeners
document.getElementById("convertImageLoad").addEventListener("click", () => imageToLoad.click());
document.getElementById("imageToLoad").addEventListener("change", loadImage);
document.getElementById("convertAutoLum").addEventListener("click", () => autoAssing("lum"));
document.getElementById("convertAutoHue").addEventListener("click", () => autoAssing("hue"));
document.getElementById("convertColorsButton").addEventListener("click", setConvertColorsNumber);
document.getElementById("convertComplete").addEventListener("click", applyConversion);
document.getElementById("convertCancel").addEventListener("click", cancelConversion);
document.getElementById("convertOverlay").addEventListener("input", function() {setOverlayOpacity(this.value)});
document.getElementById("convertOverlayNumber").addEventListener("input", function() {setOverlayOpacity(this.value)});
function showPalleteHeight() {
const height = +this.getAttribute("data-color");
colorsSelectValue.innerHTML = height;
colorsSelectFriendly.innerHTML = getHeight(height);
const former = colorScheme.querySelector(".hoveredColor")
if (former) former.className = "";
this.className = "hoveredColor";
}
function loadImage() {
const file = this.files[0];
this.value = ""; // reset input value to get triggered if the file is re-uploaded
const reader = new FileReader();
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 = function() {img.src = reader.result;};
reader.readAsDataURL(file);
}
function heightsFromImage(count) {
const ctx = document.getElementById("canvas").getContext("2d");
const imageData = ctx.getImageData(0, 0, graphWidth, graphHeight);
const data = imageData.data;
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";
const 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];
return [r, g, b];
});
const cmap = MMCQ.quantize(gridColors, count);
const usedColors = new Set();
viewbox.select("#heights").selectAll("polygon").data(grid.cells.i).join("polygon")
.attr("points", d => getGridPolygon(d))
.attr("id", d => "cell"+d).attr("fill", d => {
const clr = `rgb(${cmap.nearest(gridColors[d])})`;
usedColors.add(clr);
return clr;
}).on("click", mapClicked);
const unassigned = [...usedColors].sort((a, b) => d3.lab(a).l - d3.lab(b).l);
const unassignedContainer = d3.select("#colorsUnassigned");
unassignedContainer.selectAll("div").data(unassigned).enter().append("div")
.attr("data-color", i => i).style("background-color", i => i)
.attr("class", "color-div").on("click", colorClicked);
convertColors.value = unassigned.length;
}
function mapClicked() {
const fill = this.getAttribute("fill");
const palleteColor = imageConverter.querySelector(`div[data-color="${fill}"]`);
palleteColor.click();
}
function colorClicked() {
viewbox.select("#heights").selectAll(".selectedCell").attr("class", null);
const unselect = this.classList.contains("selectedColor");
const selectedColor = imageConverter.querySelector("div.selectedColor");
if (selectedColor) selectedColor.classList.remove("selectedColor");
const hoveredColor = colorScheme.querySelector("div.hoveredColor");
if (hoveredColor) hoveredColor.classList.remove("hoveredColor");
colorsSelectValue.innerHTML = colorsSelectFriendly.innerHTML = 0;
if (unselect) return;
this.classList.add("selectedColor");
if (this.dataset.height) {
const height = +this.dataset.height;
colorScheme.querySelector(`div[data-color="${height}"]`).classList.add("hoveredColor");
colorsSelectValue.innerHTML = height;
colorsSelectFriendly.innerHTML = getHeight(height);
}
const color = this.getAttribute("data-color");
viewbox.select("#heights").selectAll("polygon.selectedCell").classed("selectedCell", 0);
viewbox.select("#heights").selectAll("polygon[fill='" + color + "']").classed("selectedCell", 1);
}
function assignHeight() {
const height = +this.dataset.color;
const rgb = color(1 - (height < 20 ? height-5 : height) / 100);
const selectedColor = imageConverter.querySelector("div.selectedColor");
selectedColor.style.backgroundColor = rgb;
selectedColor.setAttribute("data-color", rgb);
selectedColor.setAttribute("data-height", height);
viewbox.select("#heights").selectAll(".selectedCell").each(function() {
this.setAttribute("fill", rgb);
this.setAttribute("data-height", height);
});
if (selectedColor.parentNode.id === "colorsUnassigned") {
colorsAssigned.appendChild(selectedColor);
colorsAssigned.style.display = "block";
}
}
// 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;}
const assinged = []; // 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 increated one
if (assinged[heightTo] && heightTo < 100) heightTo += 1; // if height is already added, try increated 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 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
el.style.backgroundColor = el.dataset.color = colorTo;
el.dataset.height = heightTo;
colorsAssigned.appendChild(el);
assinged[heightTo] = true;
});
// sort assigned colors by height
Array.from(colorsAssigned.children).sort((a, b) => {
return +a.dataset.height - +b.dataset.height;
}).forEach(line => colorsAssigned.appendChild(line));
colorsAssigned.style.display = "block";
colorsUnassigned.style.display = "none";
}
function setConvertColorsNumber() {
prompt(`Please provide a desired number of colors. <br>An actual number depends on color scheme and may vary from desired`,
{default:convertColors.value, step:1, min:3, max:255}, number => {
convertColors.value = number;
heightsFromImage(number);
});
}
function setOverlayOpacity(v) {
convertOverlay.value = convertOverlayNumber.value = v;
document.getElementById("canvas").style.opacity = v;
}
function applyConversion() {
viewbox.select("#heights").selectAll("polygon").each(function() {
const height = +this.dataset.height || 0;
const i = +this.id.slice(4);
grid.cells.h[i] = height;
});
viewbox.select("#heights").selectAll("polygon").remove();
updateHeightmap();
restoreImageConverterState();
}
function cancelConversion() {
restoreImageConverterState();
viewbox.select("#heights").selectAll("polygon").remove();
restoreHistory(edits.n-1);
}
function restoreImageConverterState() {
const canvas = document.getElementById("canvas");
if (canvas) canvas.remove(); else return;
const img = document.getElementById("image");
if (img) img.remove(); else return;
d3.select("#imageConverter").selectAll("div.color-div").remove();
colorsAssigned.style.display = "none";
colorsUnassigned.style.display = "none";
colorsSelectValue.innerHTML = colorsSelectFriendly.innerHTML = 0;
viewbox.style("cursor", "default").on(".drag", null);
tip('Heightmap edit mode is active. Click on "Exit Customization" to finalize the heightmap', true);
$("#imageConverter").dialog("destroy");
}
function closeImageConverter(event) {
event.preventDefault();
event.stopPropagation();
alertMessage.innerHTML = 'Are you sure you want to close the Image Converter? Click "Cancel" to geck back to convertion. Click "Complete" to apply the conversion. Click "Close" to exit conversion mode and restore previous heightmap';
$("#alert").dialog({resizable: false, title: "Close Image Converter",
buttons: {
Cancel: function() {
$(this).dialog("close");
},
Complete: function() {
$(this).dialog("close");
applyConversion();
},
Close: function() {
$(this).dialog("close");
restoreImageConverterState();
viewbox.select("#heights").selectAll("polygon").remove();
restoreHistory(edits.n-1);
}
}
});
}
}
function toggleHeightmapPreview() {
if (document.getElementById("preview")) {
document.getElementById("preview").remove();
return;
}
const preview = document.createElement("canvas");
preview.id = "preview";
preview.width = grid.cellsX;
preview.height = grid.cellsY;
document.body.insertBefore(preview, optionsContainer);
preview.addEventListener("mouseover", () => tip("Heightmap preview. Click to download a screen-sized image"));
preview.addEventListener("click", downloadPreview);
drawHeightmapPreview();
}
function drawHeightmapPreview() {
const ctx = document.getElementById("preview").getContext("2d");
const imageData = ctx.createImageData(grid.cellsX, grid.cellsY);
grid.cells.h.forEach((height, i) => {
let h = height < 20 ? Math.max(height / 1.5, 0) : height;
const v = h / 100 * 255;
imageData.data[i*4] = v;
imageData.data[i*4 + 1] = v;
imageData.data[i*4 + 2] = v;
imageData.data[i*4 + 3] = 255;
});
ctx.putImageData(imageData, 0, 0);
}
function downloadPreview() {
const preview = document.getElementById("preview");
const dataURL = preview.toDataURL("image/png");
const img = new Image();
img.src = dataURL;
img.onload = function() {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = svgWidth;
canvas.height = svgHeight;
document.body.insertBefore(canvas, optionsContainer);
ctx.drawImage(img, 0, 0, svgWidth, svgHeight);
const imgBig = canvas.toDataURL("image/png");
const link = document.createElement("a");
link.download = getFileName("Heightmap") + ".png";
link.href = imgBig;
document.body.appendChild(link);
link.click();
canvas.remove();
}
}
}