heightmap selection - refactor, make generation immutable to get predictable result

This commit is contained in:
Azgaar 2022-05-29 22:11:32 +03:00
parent 4f372c7a46
commit 662163176b
12 changed files with 197 additions and 158 deletions

View file

@ -520,4 +520,9 @@ export function resolveVersionConflicts(version) {
if (!zone.dataset.type) zone.dataset.type = "Unknown";
});
}
if (version < 1.84) {
// v1.84.0 added grid.cellsDesired to stored data
if (!grid.cellsDesired) grid.cellsDesired = rn((graphWidth * graphHeight) / grid.spacing ** 2, -3);
}
}

View file

@ -1,4 +1,6 @@
const initialSeed = generateSeed();
let graph = getGraph(grid);
appendStyleSheet();
insertEditorHtml();
addListeners();
@ -8,6 +10,7 @@ export function open() {
const $templateInput = byId("templateInput");
setSelected($templateInput.value);
graph = getGraph(graph);
$("#heightmapSelection").dialog({
title: "Select Heightmap",
@ -30,8 +33,7 @@ export function open() {
lock("template");
const seed = getSeed();
Math.random = aleaPRNG(seed);
regeneratePrompt({seed});
regeneratePrompt({seed, graph});
$(this).dialog("close");
}
@ -182,13 +184,14 @@ function insertEditorHtml() {
</div>`;
byId("dialogs").insertAdjacentHTML("beforeend", heightmapSelectionHtml);
const sections = document.getElementsByClassName("heightmap-selection_container");
sections[0].innerHTML = Object.keys(heightmapTemplates)
.map(key => {
const name = heightmapTemplates[key].name;
Math.random = aleaPRNG(initialSeed);
const heights = generateHeightmap(key);
const heights = HeightmapGenerator.fromTemplate(graph, key);
const dataUrl = drawHeights(heights);
return /* html */ `<article data-id="${key}" data-seed="${initialSeed}">
@ -220,12 +223,8 @@ function addListeners() {
if (!article) return;
const id = article.dataset.id;
if (event.target.matches("span.icon-cw")) {
const seed = generateSeed();
article.dataset.seed = seed;
Math.random = aleaPRNG(seed);
drawTemplatePreview(id);
} else setSelected(id);
if (event.target.matches("span.icon-cw")) regeneratePreview(article, id);
setSelected(id);
});
byId("heightmapSelectionRenderOcean").on("change", redrawAll);
@ -254,12 +253,18 @@ function getName(id) {
return isTemplate ? heightmapTemplates[id].name : precreatedHeightmaps[id].name;
}
function getGraph(currentGraph) {
const newGraph = shouldRegenerateGrid(currentGraph) ? generateGrid() : deepCopy(currentGraph);
delete newGraph.cells.h;
return newGraph;
}
function drawHeights(heights) {
const canvas = document.createElement("canvas");
canvas.width = grid.cellsX;
canvas.height = grid.cellsY;
canvas.width = graph.cellsX;
canvas.height = graph.cellsY;
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(grid.cellsX, grid.cellsY);
const imageData = ctx.createImageData(graph.cellsX, graph.cellsY);
const schemeId = byId("heightmapSelectionColorScheme").value;
const scheme = getColorScheme(schemeId);
@ -281,32 +286,30 @@ function drawHeights(heights) {
return canvas.toDataURL("image/png");
}
function generateHeightmap(id) {
const heights = new Uint8Array(grid.points.length);
// use cells number of the current graph, no matter what UI input value is
const cellsDesired = rn((graphWidth * graphHeight) / grid.spacing ** 2, -3);
HeightmapGenerator.setHeights(heights, cellsDesired);
const newHeights = HeightmapGenerator.fromTemplate(id);
HeightmapGenerator.cleanup();
return newHeights;
}
function drawTemplatePreview(id) {
const heights = generateHeightmap(id);
const heights = HeightmapGenerator.fromTemplate(graph, id);
const dataUrl = drawHeights(heights);
const article = byId("heightmapSelection").querySelector(`[data-id="${id}"]`);
article.querySelector("img").src = dataUrl;
}
async function drawPrecreatedHeightmap(id) {
const heights = await HeightmapGenerator.fromPrecreated(id);
const heights = await HeightmapGenerator.fromPrecreated(graph, id);
const dataUrl = drawHeights(heights);
const article = byId("heightmapSelection").querySelector(`[data-id="${id}"]`);
article.querySelector("img").src = dataUrl;
}
function regeneratePreview(article, id) {
graph = getGraph(graph);
const seed = generateSeed();
article.dataset.seed = seed;
Math.random = aleaPRNG(seed);
drawTemplatePreview(id);
}
function redrawAll() {
graph = getGraph(graph);
const articles = byId("heightmapSelection").querySelectorAll(`article`);
for (const article of articles) {
const {id, seed} = article.dataset;

View file

@ -1,47 +1,48 @@
"use strict";
window.HeightmapGenerator = (function () {
let grid = null;
let heights = null;
let blobPower;
let linePower;
const setHeights = (savedHeights, cellsNumber) => {
heights = savedHeights;
blobPower = getBlobPower(cellsNumber);
linePower = getLinePower(cellsNumber);
const setGraph = graph => {
const {cellsDesired, cells, points} = graph;
heights = cells.h || createTypedArray({maxValue: 100, length: points.length});
blobPower = getBlobPower(cellsDesired);
linePower = getLinePower(cellsDesired);
grid = graph;
};
const resetHeights = () => {
heights = new Uint8Array(grid.points.length);
const cellsNumber = +byId("pointsInput").dataset.cells;
blobPower = getBlobPower(cellsNumber);
linePower = getLinePower(cellsNumber);
};
const getHeights = () => heights;
const cleanup = () => (heights = null);
const clearData = () => {
heights = null;
grid = null;
};
const fromTemplate = template => {
const templateString = heightmapTemplates[template]?.template || "";
const fromTemplate = (graph, id) => {
const templateString = heightmapTemplates[id]?.template || "";
const steps = templateString.split("\n");
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${template}. Steps: ${steps}`);
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`);
setGraph(graph);
for (const step of steps) {
const elements = step.trim().split(" ");
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${template}. Step: ${elements}`);
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${id}. Step: ${elements}`);
addStep(...elements);
}
return heights;
};
const fromPrecreated = id => {
const fromPrecreated = (graph, id) => {
return new Promise(resolve => {
// create canvas where 1px corresponts to a cell
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const {cellsX, cellsY} = grid;
const {cellsX, cellsY} = graph;
canvas.width = cellsX;
canvas.height = cellsY;
@ -51,7 +52,8 @@ window.HeightmapGenerator = (function () {
img.onload = () => {
ctx.drawImage(img, 0, 0, cellsX, cellsY);
const imageData = ctx.getImageData(0, 0, cellsX, cellsY);
const heights = getHeightsFromImageData(imageData.data);
setGraph(graph);
getHeightsFromImageData(imageData.data);
canvas.remove();
img.remove();
resolve(heights);
@ -59,18 +61,17 @@ window.HeightmapGenerator = (function () {
});
};
const generate = async function () {
Math.random = aleaPRNG(seed);
const generate = async function (graph) {
TIME && console.time("defineHeightmap");
const id = byId("templateInput").value;
resetHeights();
Math.random = aleaPRNG(seed);
const isTemplate = id in heightmapTemplates;
grid.cells.h = isTemplate ? fromTemplate(id) : await fromPrecreated(id);
cleanup();
const heights = isTemplate ? fromTemplate(graph, id) : await fromPrecreated(graph, id);
TIME && console.timeEnd("defineHeightmap");
clearData();
return heights;
};
function addStep(tool, a2, a3, a4, a5) {
@ -141,7 +142,7 @@ window.HeightmapGenerator = (function () {
do {
const x = getPointInRange(rangeX, graphWidth);
const y = getPointInRange(rangeY, graphHeight);
start = findGridCell(x, y);
start = findGridCell(x, y, grid);
limit++;
} while (heights[start] + h > 90 && limit < 50);
@ -177,7 +178,7 @@ window.HeightmapGenerator = (function () {
do {
const x = getPointInRange(rangeX, graphWidth);
const y = getPointInRange(rangeY, graphHeight);
start = findGridCell(x, y);
start = findGridCell(x, y, grid);
limit++;
} while (heights[start] < 20 && limit < 50);
@ -223,7 +224,9 @@ window.HeightmapGenerator = (function () {
limit++;
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50);
let range = getRange(findGridCell(startX, startY), findGridCell(endX, endY));
const startCell = findGridCell(startX, startY, grid);
const endCell = findGridCell(endX, endY, grid);
let range = getRange(startCell, endCell);
// get main ridge
function getRange(cur, end) {
@ -305,7 +308,7 @@ window.HeightmapGenerator = (function () {
do {
startX = getPointInRange(rangeX, graphWidth);
startY = getPointInRange(rangeY, graphHeight);
start = findGridCell(startX, startY);
start = findGridCell(startX, startY, grid);
limit++;
} while (heights[start] < 20 && limit < 50);
@ -317,7 +320,7 @@ window.HeightmapGenerator = (function () {
limit++;
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50);
let range = getRange(start, findGridCell(endX, endY));
let range = getRange(start, findGridCell(endX, endY, grid));
// get main ridge
function getRange(cur, end) {
@ -388,8 +391,8 @@ window.HeightmapGenerator = (function () {
const endX = vert ? Math.floor(graphWidth - startX - graphWidth * 0.1 + Math.random() * graphWidth * 0.2) : graphWidth - 5;
const endY = vert ? graphHeight - 5 : Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2);
const start = findGridCell(startX, startY);
const end = findGridCell(endX, endY);
const start = findGridCell(startX, startY, grid);
const end = findGridCell(endX, endY, grid);
let range = getRange(start, end);
const query = [];
@ -502,20 +505,16 @@ window.HeightmapGenerator = (function () {
}
function getHeightsFromImageData(imageData) {
const heights = new Uint8Array(grid.points.length);
for (let i = 0; i < heights.length; i++) {
const lightness = imageData[i * 4] / 255;
const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8;
heights[i] = minmax(Math.floor(powered * 100), 0, 100);
}
return heights;
}
return {
setHeights,
resetHeights,
setGraph,
getHeights,
cleanup,
generate,
fromTemplate,
fromPrecreated,

View file

@ -324,7 +324,11 @@ async function parseLoadedData(data) {
void (function parseGridData() {
grid = JSON.parse(data[6]);
calculateVoronoi(grid, grid.points);
const {cells, vertices} = calculateVoronoi(grid.points, grid.boundary);
grid.cells = cells;
grid.vertices = vertices;
grid.cells.h = Uint8Array.from(data[7].split(","));
grid.cells.prec = Uint8Array.from(data[8].split(","));
grid.cells.f = Uint16Array.from(data[9].split(","));
@ -333,7 +337,6 @@ async function parseLoadedData(data) {
})();
void (function parsePackData() {
pack = {};
reGraph();
reMarkFeatures();
pack.features = JSON.parse(data[12]);

View file

@ -54,8 +54,8 @@ function getMapData() {
const serializedSVG = new XMLSerializer().serializeToString(cloneEl);
const {spacing, cellsX, cellsY, boundary, points, features} = grid;
const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features});
const {spacing, cellsX, cellsY, boundary, points, features, cellsDesired} = grid;
const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features, cellsDesired});
const packFeatures = JSON.stringify(pack.features);
const cultures = JSON.stringify(pack.cultures);
const states = JSON.stringify(pack.states);

View file

@ -41,8 +41,7 @@ window.Submap = (function () {
// create new grid
applyMapSize();
placePoints();
calculateVoronoi(grid, grid.points);
grid = generateGrid();
drawScaleBar(scale);
const resampler = (points, qtree, f) => {

View file

@ -81,10 +81,10 @@ function handleMouseMove() {
if (i === undefined) return;
showNotes(d3.event);
const g = findGridCell(point[0], point[1]); // grid cell id
const gridCell = findGridCell(point[0], point[1], grid);
if (tooltip.dataset.main) showMainTip();
else showMapTooltip(point, d3.event, i, g);
if (cellInfo?.offsetParent) updateCellInfo(point, i, g);
else showMapTooltip(point, d3.event, i, gridCell);
if (cellInfo?.offsetParent) updateCellInfo(point, i, gridCell);
}
// show note box on hover (if any)
@ -244,7 +244,7 @@ function updateCellInfo(point, i, g) {
infoCell.innerHTML = i;
infoArea.innerHTML = cells.area[i] ? si(getArea(cells.area[i])) + " " + getAreaUnit() : "n/a";
infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
infoDepth.innerHTML = getDepth(pack.features[f], pack.cells.h[i], point);
infoDepth.innerHTML = getDepth(pack.features[f], point);
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : "no";
@ -276,11 +276,11 @@ function getElevation(f, h) {
}
// get water depth
function getDepth(f, h, p) {
function getDepth(f, p) {
if (f.land) return "0 " + heightUnit.value; // land: 0
// lake: difference between surface and bottom
const gridH = grid.cells.h[findGridCell(p[0], p[1])];
const gridH = grid.cells.h[findGridCell(p[0], p[1], grid)];
if (f.type === "lake") {
const depth = gridH === 19 ? f.height / 2 : gridH;
return getHeight(depth, "abs");
@ -290,9 +290,9 @@ function getDepth(f, h, p) {
}
// get user-friendly (real-world) height value from map data
function getFriendlyHeight(p) {
const packH = pack.cells.h[findCell(p[0], p[1])];
const gridH = grid.cells.h[findGridCell(p[0], p[1])];
function getFriendlyHeight([x, y]) {
const packH = pack.cells.h[findCell(x, y, grid)];
const gridH = grid.cells.h[findGridCell(x, y, grid)];
const h = packH < 20 ? gridH : packH;
return getHeight(h);
}

View file

@ -117,7 +117,7 @@ function editHeightmap(options) {
function moveCursor() {
const [x, y] = d3.mouse(this);
const cell = findGridCell(x, y);
const cell = findGridCell(x, y, grid);
heightmapInfoX.innerHTML = rn(x);
heightmapInfoY.innerHTML = rn(y);
heightmapInfoCell.innerHTML = cell;
@ -605,8 +605,8 @@ function editHeightmap(options) {
function dragBrush() {
const r = brushRadius.valueAsNumber;
const point = d3.mouse(this);
const start = findGridCell(point[0], point[1]);
const [x, y] = d3.mouse(this);
const start = findGridCell(x, y, grid);
d3.event.on("drag", () => {
const p = d3.mouse(this);
@ -664,7 +664,7 @@ function editHeightmap(options) {
if (Number.isNaN(operand)) return tip("Operand should be a number", false, "error");
if ((operator === "add" || operator === "subtract") && !Number.isInteger(operand)) return tip("Operand should be an integer", false, "error");
HeightmapGenerator.setHeights(grid.cells.h);
HeightmapGenerator.setGraph(grid);
if (operator === "multiply") HeightmapGenerator.modify(range, 0, operand, 0);
else if (operator === "divide") HeightmapGenerator.modify(range, 0, 1 / operand, 0);
@ -673,15 +673,13 @@ function editHeightmap(options) {
else if (operator === "exponent") HeightmapGenerator.modify(range, 0, 1, operand);
grid.cells.h = HeightmapGenerator.getHeights();
HeightmapGenerator.cleanup();
updateHeightmap();
}
function smoothAllHeights() {
HeightmapGenerator.setHeights(grid.cells.h);
HeightmapGenerator.setGraph(grid);
HeightmapGenerator.smooth(4, 1.5);
grid.cells.h = HeightmapGenerator.getHeights();
HeightmapGenerator.cleanup();
updateHeightmap();
}
@ -940,11 +938,8 @@ function editHeightmap(options) {
const seed = byId("templateSeed").value;
if (seed) Math.random = aleaPRNG(seed);
const heights = new Uint8Array(grid.points.length);
// use cells number of the current graph, no matter what UI input value is
const cellsDesired = rn((graphWidth * graphHeight) / grid.spacing ** 2, -3);
HeightmapGenerator.setHeights(heights, cellsDesired);
grid.cells.h = createTypedArray({maxValue: 100, length: grid.points.length});
HeightmapGenerator.setGraph(grid);
restartHistory();
for (const step of steps) {
@ -973,7 +968,6 @@ function editHeightmap(options) {
}
grid.cells.h = HeightmapGenerator.getHeights();
HeightmapGenerator.cleanup();
updateStatistics();
mockHeightmap();
if (byId("preview")) drawHeightmapPreview(); // update heightmap preview if opened

View file

@ -65,8 +65,8 @@ function editIce() {
}
function addIcebergOnClick() {
const point = d3.mouse(this);
const i = findGridCell(point[0], point[1]);
const [x, y] = d3.mouse(this);
const i = findGridCell(x, y, grid);
const c = grid.points[i];
const s = +document.getElementById("iceSize").value;