mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-02-04 17:41:23 +01:00
Merge pull request #1268 from SheepFromHeaven/refactor/migrate-modules
feat: Implement HeightmapGenerator and Voronoi module
This commit is contained in:
commit
5a460cab87
8 changed files with 750 additions and 601 deletions
|
|
@ -1,543 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
window.HeightmapGenerator = (function () {
|
|
||||||
let grid = null;
|
|
||||||
let heights = null;
|
|
||||||
let blobPower;
|
|
||||||
let linePower;
|
|
||||||
|
|
||||||
const setGraph = graph => {
|
|
||||||
const {cellsDesired, cells, points} = graph;
|
|
||||||
heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({maxValue: 100, length: points.length});
|
|
||||||
blobPower = getBlobPower(cellsDesired);
|
|
||||||
linePower = getLinePower(cellsDesired);
|
|
||||||
grid = graph;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHeights = () => heights;
|
|
||||||
|
|
||||||
const clearData = () => {
|
|
||||||
heights = null;
|
|
||||||
grid = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
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: ${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: ${id}. Step: ${elements}`);
|
|
||||||
addStep(...elements);
|
|
||||||
}
|
|
||||||
|
|
||||||
return heights;
|
|
||||||
};
|
|
||||||
|
|
||||||
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} = graph;
|
|
||||||
canvas.width = cellsX;
|
|
||||||
canvas.height = cellsY;
|
|
||||||
|
|
||||||
// load heightmap into image and render to canvas
|
|
||||||
const img = new Image();
|
|
||||||
img.src = `./heightmaps/${id}.png`;
|
|
||||||
img.onload = () => {
|
|
||||||
ctx.drawImage(img, 0, 0, cellsX, cellsY);
|
|
||||||
const imageData = ctx.getImageData(0, 0, cellsX, cellsY);
|
|
||||||
setGraph(graph);
|
|
||||||
getHeightsFromImageData(imageData.data);
|
|
||||||
canvas.remove();
|
|
||||||
img.remove();
|
|
||||||
resolve(heights);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const generate = async function (graph) {
|
|
||||||
TIME && console.time("defineHeightmap");
|
|
||||||
const id = byId("templateInput").value;
|
|
||||||
|
|
||||||
Math.random = aleaPRNG(seed);
|
|
||||||
const isTemplate = id in heightmapTemplates;
|
|
||||||
const heights = isTemplate ? fromTemplate(graph, id) : await fromPrecreated(graph, id);
|
|
||||||
TIME && console.timeEnd("defineHeightmap");
|
|
||||||
|
|
||||||
clearData();
|
|
||||||
return heights;
|
|
||||||
};
|
|
||||||
|
|
||||||
function addStep(tool, a2, a3, a4, a5) {
|
|
||||||
if (tool === "Hill") return addHill(a2, a3, a4, a5);
|
|
||||||
if (tool === "Pit") return addPit(a2, a3, a4, a5);
|
|
||||||
if (tool === "Range") return addRange(a2, a3, a4, a5);
|
|
||||||
if (tool === "Trough") return addTrough(a2, a3, a4, a5);
|
|
||||||
if (tool === "Strait") return addStrait(a2, a3);
|
|
||||||
if (tool === "Mask") return mask(a2);
|
|
||||||
if (tool === "Invert") return invert(a2, a3);
|
|
||||||
if (tool === "Add") return modify(a3, +a2, 1);
|
|
||||||
if (tool === "Multiply") return modify(a3, 0, +a2);
|
|
||||||
if (tool === "Smooth") return smooth(a2);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBlobPower(cells) {
|
|
||||||
const blobPowerMap = {
|
|
||||||
1000: 0.93,
|
|
||||||
2000: 0.95,
|
|
||||||
5000: 0.97,
|
|
||||||
10000: 0.98,
|
|
||||||
20000: 0.99,
|
|
||||||
30000: 0.991,
|
|
||||||
40000: 0.993,
|
|
||||||
50000: 0.994,
|
|
||||||
60000: 0.995,
|
|
||||||
70000: 0.9955,
|
|
||||||
80000: 0.996,
|
|
||||||
90000: 0.9964,
|
|
||||||
100000: 0.9973
|
|
||||||
};
|
|
||||||
return blobPowerMap[cells] || 0.98;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLinePower(cells) {
|
|
||||||
const linePowerMap = {
|
|
||||||
1000: 0.75,
|
|
||||||
2000: 0.77,
|
|
||||||
5000: 0.79,
|
|
||||||
10000: 0.81,
|
|
||||||
20000: 0.82,
|
|
||||||
30000: 0.83,
|
|
||||||
40000: 0.84,
|
|
||||||
50000: 0.86,
|
|
||||||
60000: 0.87,
|
|
||||||
70000: 0.88,
|
|
||||||
80000: 0.91,
|
|
||||||
90000: 0.92,
|
|
||||||
100000: 0.93
|
|
||||||
};
|
|
||||||
|
|
||||||
return linePowerMap[cells] || 0.81;
|
|
||||||
}
|
|
||||||
|
|
||||||
const addHill = (count, height, rangeX, rangeY) => {
|
|
||||||
count = getNumberInRange(count);
|
|
||||||
while (count > 0) {
|
|
||||||
addOneHill();
|
|
||||||
count--;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addOneHill() {
|
|
||||||
const change = new Uint8Array(heights.length);
|
|
||||||
let limit = 0;
|
|
||||||
let start;
|
|
||||||
let h = lim(getNumberInRange(height));
|
|
||||||
|
|
||||||
do {
|
|
||||||
const x = getPointInRange(rangeX, graphWidth);
|
|
||||||
const y = getPointInRange(rangeY, graphHeight);
|
|
||||||
start = findGridCell(x, y, grid);
|
|
||||||
limit++;
|
|
||||||
} while (heights[start] + h > 90 && limit < 50);
|
|
||||||
|
|
||||||
change[start] = h;
|
|
||||||
const queue = [start];
|
|
||||||
while (queue.length) {
|
|
||||||
const q = queue.shift();
|
|
||||||
|
|
||||||
for (const c of grid.cells.c[q]) {
|
|
||||||
if (change[c]) continue;
|
|
||||||
change[c] = change[q] ** blobPower * (Math.random() * 0.2 + 0.9);
|
|
||||||
if (change[c] > 1) queue.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
heights = heights.map((h, i) => lim(h + change[i]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addPit = (count, height, rangeX, rangeY) => {
|
|
||||||
count = getNumberInRange(count);
|
|
||||||
while (count > 0) {
|
|
||||||
addOnePit();
|
|
||||||
count--;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addOnePit() {
|
|
||||||
const used = new Uint8Array(heights.length);
|
|
||||||
let limit = 0,
|
|
||||||
start;
|
|
||||||
let h = lim(getNumberInRange(height));
|
|
||||||
|
|
||||||
do {
|
|
||||||
const x = getPointInRange(rangeX, graphWidth);
|
|
||||||
const y = getPointInRange(rangeY, graphHeight);
|
|
||||||
start = findGridCell(x, y, grid);
|
|
||||||
limit++;
|
|
||||||
} while (heights[start] < 20 && limit < 50);
|
|
||||||
|
|
||||||
const queue = [start];
|
|
||||||
while (queue.length) {
|
|
||||||
const q = queue.shift();
|
|
||||||
h = h ** blobPower * (Math.random() * 0.2 + 0.9);
|
|
||||||
if (h < 1) return;
|
|
||||||
|
|
||||||
grid.cells.c[q].forEach(function (c, i) {
|
|
||||||
if (used[c]) return;
|
|
||||||
heights[c] = lim(heights[c] - h * (Math.random() * 0.2 + 0.9));
|
|
||||||
used[c] = 1;
|
|
||||||
queue.push(c);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// fromCell, toCell are options cell ids
|
|
||||||
const addRange = (count, height, rangeX, rangeY, startCell, endCell) => {
|
|
||||||
count = getNumberInRange(count);
|
|
||||||
while (count > 0) {
|
|
||||||
addOneRange();
|
|
||||||
count--;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addOneRange() {
|
|
||||||
const used = new Uint8Array(heights.length);
|
|
||||||
let h = lim(getNumberInRange(height));
|
|
||||||
|
|
||||||
if (rangeX && rangeY) {
|
|
||||||
// find start and end points
|
|
||||||
const startX = getPointInRange(rangeX, graphWidth);
|
|
||||||
const startY = getPointInRange(rangeY, graphHeight);
|
|
||||||
|
|
||||||
let dist = 0,
|
|
||||||
limit = 0,
|
|
||||||
endX,
|
|
||||||
endY;
|
|
||||||
|
|
||||||
do {
|
|
||||||
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
|
|
||||||
endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
|
|
||||||
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
|
|
||||||
limit++;
|
|
||||||
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50);
|
|
||||||
|
|
||||||
startCell = findGridCell(startX, startY, grid);
|
|
||||||
endCell = findGridCell(endX, endY, grid);
|
|
||||||
}
|
|
||||||
|
|
||||||
let range = getRange(startCell, endCell);
|
|
||||||
|
|
||||||
// get main ridge
|
|
||||||
function getRange(cur, end) {
|
|
||||||
const range = [cur];
|
|
||||||
const p = grid.points;
|
|
||||||
used[cur] = 1;
|
|
||||||
|
|
||||||
while (cur !== end) {
|
|
||||||
let min = Infinity;
|
|
||||||
grid.cells.c[cur].forEach(function (e) {
|
|
||||||
if (used[e]) return;
|
|
||||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
|
||||||
if (Math.random() > 0.85) diff = diff / 2;
|
|
||||||
if (diff < min) {
|
|
||||||
min = diff;
|
|
||||||
cur = e;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (min === Infinity) return range;
|
|
||||||
range.push(cur);
|
|
||||||
used[cur] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return range;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add height to ridge and cells around
|
|
||||||
let queue = range.slice(),
|
|
||||||
i = 0;
|
|
||||||
while (queue.length) {
|
|
||||||
const frontier = queue.slice();
|
|
||||||
(queue = []), i++;
|
|
||||||
frontier.forEach(i => {
|
|
||||||
heights[i] = lim(heights[i] + h * (Math.random() * 0.3 + 0.85));
|
|
||||||
});
|
|
||||||
h = h ** linePower - 1;
|
|
||||||
if (h < 2) break;
|
|
||||||
frontier.forEach(f => {
|
|
||||||
grid.cells.c[f].forEach(i => {
|
|
||||||
if (!used[i]) {
|
|
||||||
queue.push(i);
|
|
||||||
used[i] = 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate prominences
|
|
||||||
range.forEach((cur, d) => {
|
|
||||||
if (d % 6 !== 0) return;
|
|
||||||
for (const l of d3.range(i)) {
|
|
||||||
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
|
|
||||||
heights[min] = (heights[cur] * 2 + heights[min]) / 3;
|
|
||||||
cur = min;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addTrough = (count, height, rangeX, rangeY, startCell, endCell) => {
|
|
||||||
count = getNumberInRange(count);
|
|
||||||
while (count > 0) {
|
|
||||||
addOneTrough();
|
|
||||||
count--;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addOneTrough() {
|
|
||||||
const used = new Uint8Array(heights.length);
|
|
||||||
let h = lim(getNumberInRange(height));
|
|
||||||
|
|
||||||
if (rangeX && rangeY) {
|
|
||||||
// find start and end points
|
|
||||||
let limit = 0,
|
|
||||||
startX,
|
|
||||||
startY,
|
|
||||||
dist = 0,
|
|
||||||
endX,
|
|
||||||
endY;
|
|
||||||
do {
|
|
||||||
startX = getPointInRange(rangeX, graphWidth);
|
|
||||||
startY = getPointInRange(rangeY, graphHeight);
|
|
||||||
startCell = findGridCell(startX, startY, grid);
|
|
||||||
limit++;
|
|
||||||
} while (heights[startCell] < 20 && limit < 50);
|
|
||||||
|
|
||||||
limit = 0;
|
|
||||||
do {
|
|
||||||
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
|
|
||||||
endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
|
|
||||||
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
|
|
||||||
limit++;
|
|
||||||
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50);
|
|
||||||
|
|
||||||
endCell = findGridCell(endX, endY, grid);
|
|
||||||
}
|
|
||||||
|
|
||||||
let range = getRange(startCell, endCell);
|
|
||||||
|
|
||||||
// get main ridge
|
|
||||||
function getRange(cur, end) {
|
|
||||||
const range = [cur];
|
|
||||||
const p = grid.points;
|
|
||||||
used[cur] = 1;
|
|
||||||
|
|
||||||
while (cur !== end) {
|
|
||||||
let min = Infinity;
|
|
||||||
grid.cells.c[cur].forEach(function (e) {
|
|
||||||
if (used[e]) return;
|
|
||||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
|
||||||
if (Math.random() > 0.8) diff = diff / 2;
|
|
||||||
if (diff < min) {
|
|
||||||
min = diff;
|
|
||||||
cur = e;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (min === Infinity) return range;
|
|
||||||
range.push(cur);
|
|
||||||
used[cur] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return range;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add height to ridge and cells around
|
|
||||||
let queue = range.slice(),
|
|
||||||
i = 0;
|
|
||||||
while (queue.length) {
|
|
||||||
const frontier = queue.slice();
|
|
||||||
(queue = []), i++;
|
|
||||||
frontier.forEach(i => {
|
|
||||||
heights[i] = lim(heights[i] - h * (Math.random() * 0.3 + 0.85));
|
|
||||||
});
|
|
||||||
h = h ** linePower - 1;
|
|
||||||
if (h < 2) break;
|
|
||||||
frontier.forEach(f => {
|
|
||||||
grid.cells.c[f].forEach(i => {
|
|
||||||
if (!used[i]) {
|
|
||||||
queue.push(i);
|
|
||||||
used[i] = 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate prominences
|
|
||||||
range.forEach((cur, d) => {
|
|
||||||
if (d % 6 !== 0) return;
|
|
||||||
for (const l of d3.range(i)) {
|
|
||||||
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
|
|
||||||
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
|
|
||||||
heights[min] = (heights[cur] * 2 + heights[min]) / 3;
|
|
||||||
cur = min;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addStrait = (width, direction = "vertical") => {
|
|
||||||
width = Math.min(getNumberInRange(width), grid.cellsX / 3);
|
|
||||||
if (width < 1 && P(width)) return;
|
|
||||||
const used = new Uint8Array(heights.length);
|
|
||||||
const vert = direction === "vertical";
|
|
||||||
const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5;
|
|
||||||
const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * 0.4 + graphHeight * 0.3);
|
|
||||||
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, grid);
|
|
||||||
const end = findGridCell(endX, endY, grid);
|
|
||||||
let range = getRange(start, end);
|
|
||||||
const query = [];
|
|
||||||
|
|
||||||
function getRange(cur, end) {
|
|
||||||
const range = [];
|
|
||||||
const p = grid.points;
|
|
||||||
|
|
||||||
while (cur !== end) {
|
|
||||||
let min = Infinity;
|
|
||||||
grid.cells.c[cur].forEach(function (e) {
|
|
||||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
|
||||||
if (Math.random() > 0.8) diff = diff / 2;
|
|
||||||
if (diff < min) {
|
|
||||||
min = diff;
|
|
||||||
cur = e;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
range.push(cur);
|
|
||||||
}
|
|
||||||
|
|
||||||
return range;
|
|
||||||
}
|
|
||||||
|
|
||||||
const step = 0.1 / width;
|
|
||||||
|
|
||||||
while (width > 0) {
|
|
||||||
const exp = 0.9 - step * width;
|
|
||||||
range.forEach(function (r) {
|
|
||||||
grid.cells.c[r].forEach(function (e) {
|
|
||||||
if (used[e]) return;
|
|
||||||
used[e] = 1;
|
|
||||||
query.push(e);
|
|
||||||
heights[e] **= exp;
|
|
||||||
if (heights[e] > 100) heights[e] = 5;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
range = query.slice();
|
|
||||||
|
|
||||||
width--;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const modify = (range, add, mult, power) => {
|
|
||||||
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
|
|
||||||
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
|
|
||||||
const isLand = min === 20;
|
|
||||||
|
|
||||||
heights = heights.map(h => {
|
|
||||||
if (h < min || h > max) return h;
|
|
||||||
|
|
||||||
if (add) h = isLand ? Math.max(h + add, 20) : h + add;
|
|
||||||
if (mult !== 1) h = isLand ? (h - 20) * mult + 20 : h * mult;
|
|
||||||
if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
|
|
||||||
return lim(h);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const smooth = (fr = 2, add = 0) => {
|
|
||||||
heights = heights.map((h, i) => {
|
|
||||||
const a = [h];
|
|
||||||
grid.cells.c[i].forEach(c => a.push(heights[c]));
|
|
||||||
if (fr === 1) return d3.mean(a) + add;
|
|
||||||
return lim((h * (fr - 1) + d3.mean(a) + add) / fr);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const mask = (power = 1) => {
|
|
||||||
const fr = power ? Math.abs(power) : 1;
|
|
||||||
|
|
||||||
heights = heights.map((h, i) => {
|
|
||||||
const [x, y] = grid.points[i];
|
|
||||||
const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center
|
|
||||||
const ny = (2 * y) / graphHeight - 1; // [-1, 1], 0 is center
|
|
||||||
let distance = (1 - nx ** 2) * (1 - ny ** 2); // 1 is center, 0 is edge
|
|
||||||
if (power < 0) distance = 1 - distance; // inverted, 0 is center, 1 is edge
|
|
||||||
const masked = h * distance;
|
|
||||||
return lim((h * (fr - 1) + masked) / fr);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const invert = (count, axes) => {
|
|
||||||
if (!P(count)) return;
|
|
||||||
|
|
||||||
const invertX = axes !== "y";
|
|
||||||
const invertY = axes !== "x";
|
|
||||||
const {cellsX, cellsY} = grid;
|
|
||||||
|
|
||||||
const inverted = heights.map((h, i) => {
|
|
||||||
const x = i % cellsX;
|
|
||||||
const y = Math.floor(i / cellsX);
|
|
||||||
|
|
||||||
const nx = invertX ? cellsX - x - 1 : x;
|
|
||||||
const ny = invertY ? cellsY - y - 1 : y;
|
|
||||||
const invertedI = nx + ny * cellsX;
|
|
||||||
return heights[invertedI];
|
|
||||||
});
|
|
||||||
|
|
||||||
heights = inverted;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getPointInRange(range, length) {
|
|
||||||
if (typeof range !== "string") {
|
|
||||||
ERROR && console.error("Range should be a string");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const min = range.split("-")[0] / 100 || 0;
|
|
||||||
const max = range.split("-")[1] / 100 || min;
|
|
||||||
return rand(min * length, max * length);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHeightsFromImageData(imageData) {
|
|
||||||
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 {
|
|
||||||
setGraph,
|
|
||||||
getHeights,
|
|
||||||
generate,
|
|
||||||
fromTemplate,
|
|
||||||
fromPrecreated,
|
|
||||||
addHill,
|
|
||||||
addRange,
|
|
||||||
addTrough,
|
|
||||||
addStrait,
|
|
||||||
addPit,
|
|
||||||
smooth,
|
|
||||||
modify,
|
|
||||||
mask,
|
|
||||||
invert
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
@ -8465,11 +8465,10 @@
|
||||||
<script src="libs/indexedDB.js?v=1.99.00"></script>
|
<script src="libs/indexedDB.js?v=1.99.00"></script>
|
||||||
|
|
||||||
<script type="module" src="utils/index.ts"></script>
|
<script type="module" src="utils/index.ts"></script>
|
||||||
|
<script type="module" src="modules/index.ts"></script>
|
||||||
|
|
||||||
<script defer src="modules/voronoi.js"></script>
|
|
||||||
<script defer src="config/heightmap-templates.js"></script>
|
<script defer src="config/heightmap-templates.js"></script>
|
||||||
<script defer src="config/precreated-heightmaps.js"></script>
|
<script defer src="config/precreated-heightmaps.js"></script>
|
||||||
<script defer src="modules/heightmap-generator.js?v=1.99.00"></script>
|
|
||||||
<script defer src="modules/features.js?v=1.104.0"></script>
|
<script defer src="modules/features.js?v=1.104.0"></script>
|
||||||
<script defer src="modules/ocean-layers.js?v=1.108.4"></script>
|
<script defer src="modules/ocean-layers.js?v=1.108.4"></script>
|
||||||
<script defer src="modules/river-generator.js?v=1.106.7"></script>
|
<script defer src="modules/river-generator.js?v=1.106.7"></script>
|
||||||
|
|
|
||||||
582
src/modules/heightmap-generator.ts
Normal file
582
src/modules/heightmap-generator.ts
Normal file
|
|
@ -0,0 +1,582 @@
|
||||||
|
import Alea from "alea";
|
||||||
|
import { range as d3Range, leastIndex, mean } from "d3";
|
||||||
|
import { createTypedArray, byId, findGridCell, getNumberInRange, lim, minmax, P, rand } from "../utils";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
HeightmapGenerator: HeightmapGenerator;
|
||||||
|
}
|
||||||
|
var heightmapTemplates: any;
|
||||||
|
var TIME: boolean;
|
||||||
|
var ERROR: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tool = "Hill" | "Pit" | "Range" | "Trough" | "Strait" | "Mask" | "Invert" | "Add" | "Multiply" | "Smooth";
|
||||||
|
|
||||||
|
class HeightmapGenerator {
|
||||||
|
grid: any = null;
|
||||||
|
heights: Uint8Array | null = null;
|
||||||
|
blobPower: number = 0;
|
||||||
|
linePower: number = 0;
|
||||||
|
|
||||||
|
// TODO: remove after migration to TS and use param in constructor
|
||||||
|
get seed() {
|
||||||
|
return (window as any).seed;
|
||||||
|
}
|
||||||
|
get graphWidth() {
|
||||||
|
return (window as any).graphWidth;
|
||||||
|
}
|
||||||
|
get graphHeight() {
|
||||||
|
return (window as any).graphHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearData() {
|
||||||
|
this.heights = null;
|
||||||
|
this.grid = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
private getBlobPower(cells: number): number {
|
||||||
|
const blobPowerMap: Record<number, number> = {
|
||||||
|
1000: 0.93,
|
||||||
|
2000: 0.95,
|
||||||
|
5000: 0.97,
|
||||||
|
10000: 0.98,
|
||||||
|
20000: 0.99,
|
||||||
|
30000: 0.991,
|
||||||
|
40000: 0.993,
|
||||||
|
50000: 0.994,
|
||||||
|
60000: 0.995,
|
||||||
|
70000: 0.9955,
|
||||||
|
80000: 0.996,
|
||||||
|
90000: 0.9964,
|
||||||
|
100000: 0.9973
|
||||||
|
};
|
||||||
|
return blobPowerMap[cells] || 0.98;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLinePower(cells: number): number {
|
||||||
|
const linePowerMap: Record<number, number> = {
|
||||||
|
1000: 0.75,
|
||||||
|
2000: 0.77,
|
||||||
|
5000: 0.79,
|
||||||
|
10000: 0.81,
|
||||||
|
20000: 0.82,
|
||||||
|
30000: 0.83,
|
||||||
|
40000: 0.84,
|
||||||
|
50000: 0.86,
|
||||||
|
60000: 0.87,
|
||||||
|
70000: 0.88,
|
||||||
|
80000: 0.91,
|
||||||
|
90000: 0.92,
|
||||||
|
100000: 0.93
|
||||||
|
};
|
||||||
|
|
||||||
|
return linePowerMap[cells] || 0.81;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPointInRange(range: string, length: number): number | undefined {
|
||||||
|
if (typeof range !== "string") {
|
||||||
|
window.ERROR && console.error("Range should be a string");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = parseInt(range.split("-")[0]) / 100 || 0;
|
||||||
|
const max = parseInt(range.split("-")[1]) / 100 || min;
|
||||||
|
return rand(min * length, max * length);
|
||||||
|
}
|
||||||
|
|
||||||
|
setGraph(graph: any) {
|
||||||
|
const {cellsDesired, cells, points} = graph;
|
||||||
|
this.heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({maxValue: 100, length: points.length}) as Uint8Array;
|
||||||
|
this.blobPower = this.getBlobPower(cellsDesired);
|
||||||
|
this.linePower = this.getLinePower(cellsDesired);
|
||||||
|
this.grid = graph;
|
||||||
|
};
|
||||||
|
|
||||||
|
addHill(count: string, height: string, rangeX: string, rangeY: string): void {
|
||||||
|
const addOneHill = () => {
|
||||||
|
if(!this.heights || !this.grid) return;
|
||||||
|
const change = new Uint8Array(this.heights.length);
|
||||||
|
let limit = 0;
|
||||||
|
let start: number;
|
||||||
|
let h = lim(getNumberInRange(height));
|
||||||
|
|
||||||
|
do {
|
||||||
|
const x = this.getPointInRange(rangeX, this.graphWidth);
|
||||||
|
const y = this.getPointInRange(rangeY, this.graphHeight);
|
||||||
|
if (x === undefined || y === undefined) return;
|
||||||
|
start = findGridCell(x, y, this.grid);
|
||||||
|
limit++;
|
||||||
|
} while (this.heights[start] + h > 90 && limit < 50);
|
||||||
|
change[start] = h;
|
||||||
|
const queue = [start];
|
||||||
|
while (queue.length) {
|
||||||
|
const q = queue.shift() as number;
|
||||||
|
|
||||||
|
for (const c of this.grid.cells.c[q]) {
|
||||||
|
if (change[c]) continue;
|
||||||
|
change[c] = change[q] ** this.blobPower * (Math.random() * 0.2 + 0.9);
|
||||||
|
if (change[c] > 1) queue.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.heights = this.heights.map((h, i) => lim(h + change[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const desiredHillCount = getNumberInRange(count);
|
||||||
|
for (let i = 0; i < desiredHillCount; i++) {
|
||||||
|
addOneHill();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addPit(count: string, height: string, rangeX: string, rangeY: string): void {
|
||||||
|
const addOnePit = () => {
|
||||||
|
if(!this.heights || !this.grid) return;
|
||||||
|
const used = new Uint8Array(this.heights.length);
|
||||||
|
let limit = 0;
|
||||||
|
let start: number;
|
||||||
|
let h = lim(getNumberInRange(height));
|
||||||
|
|
||||||
|
do {
|
||||||
|
const x = this.getPointInRange(rangeX, this.graphWidth);
|
||||||
|
const y = this.getPointInRange(rangeY, this.graphHeight);
|
||||||
|
if (x === undefined || y === undefined) return;
|
||||||
|
start = findGridCell(x, y, this.grid);
|
||||||
|
limit++;
|
||||||
|
} while (this.heights[start] < 20 && limit < 50);
|
||||||
|
|
||||||
|
const queue = [start];
|
||||||
|
while (queue.length) {
|
||||||
|
const q = queue.shift() as number;
|
||||||
|
h = h ** this.blobPower * (Math.random() * 0.2 + 0.9);
|
||||||
|
if (h < 1) return;
|
||||||
|
|
||||||
|
this.grid.cells.c[q].forEach((c: number) => {
|
||||||
|
if (used[c] || this.heights === null) return;
|
||||||
|
this.heights[c] = lim(this.heights[c] - h * (Math.random() * 0.2 + 0.9));
|
||||||
|
used[c] = 1;
|
||||||
|
queue.push(c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const desiredPitCount = getNumberInRange(count);
|
||||||
|
for (let i = 0; i < desiredPitCount; i++) {
|
||||||
|
addOnePit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addRange(count: string, height: string, rangeX: string, rangeY: string, startCellId?: number, endCellId?: number): void {
|
||||||
|
if(!this.heights || !this.grid) return;
|
||||||
|
|
||||||
|
const addOneRange = () => {
|
||||||
|
if(!this.heights || !this.grid) return;
|
||||||
|
|
||||||
|
// get main ridge
|
||||||
|
const getRange = (cur: number, end: number) => {
|
||||||
|
const range = [cur];
|
||||||
|
const p = this.grid.points;
|
||||||
|
used[cur] = 1;
|
||||||
|
|
||||||
|
while (cur !== end) {
|
||||||
|
let min = Infinity;
|
||||||
|
this.grid.cells.c[cur].forEach((e: number) => {
|
||||||
|
if (used[e]) return;
|
||||||
|
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||||
|
if (Math.random() > 0.85) diff = diff / 2;
|
||||||
|
if (diff < min) {
|
||||||
|
min = diff;
|
||||||
|
cur = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (min === Infinity) return range;
|
||||||
|
range.push(cur);
|
||||||
|
used[cur] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
const used = new Uint8Array(this.heights.length);
|
||||||
|
let h = lim(getNumberInRange(height));
|
||||||
|
|
||||||
|
if (rangeX && rangeY) {
|
||||||
|
// find start and end points
|
||||||
|
const startX = this.getPointInRange(rangeX, this.graphWidth) as number;
|
||||||
|
const startY = this.getPointInRange(rangeY, this.graphHeight) as number;
|
||||||
|
|
||||||
|
let dist = 0;
|
||||||
|
let limit = 0;
|
||||||
|
let endY;
|
||||||
|
let endX;
|
||||||
|
|
||||||
|
do {
|
||||||
|
endX = Math.random() * this.graphWidth * 0.8 + this.graphWidth * 0.1;
|
||||||
|
endY = Math.random() * this.graphHeight * 0.7 + this.graphHeight * 0.15;
|
||||||
|
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
|
||||||
|
limit++;
|
||||||
|
} while ((dist < this.graphWidth / 8 || dist > this.graphWidth / 3) && limit < 50);
|
||||||
|
|
||||||
|
startCellId = findGridCell(startX, startY, this.grid);
|
||||||
|
endCellId = findGridCell(endX, endY, this.grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
let range = getRange(startCellId as number, endCellId as number);
|
||||||
|
|
||||||
|
|
||||||
|
// add height to ridge and cells around
|
||||||
|
let queue = range.slice();
|
||||||
|
let i = 0;
|
||||||
|
while (queue.length) {
|
||||||
|
const frontier = queue.slice();
|
||||||
|
(queue = []), i++;
|
||||||
|
frontier.forEach((i: number) => {
|
||||||
|
if(!this.heights) return;
|
||||||
|
this.heights[i] = lim(this.heights[i] + h * (Math.random() * 0.3 + 0.85));
|
||||||
|
});
|
||||||
|
h = h ** this.linePower - 1;
|
||||||
|
if (h < 2) break;
|
||||||
|
frontier.forEach((f: number) => {
|
||||||
|
this.grid.cells.c[f].forEach((i: number) => {
|
||||||
|
if (!used[i]) {
|
||||||
|
queue.push(i);
|
||||||
|
used[i] = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate prominences
|
||||||
|
range.forEach((cur: number, d: number) => {
|
||||||
|
if (d % 6 !== 0) return;
|
||||||
|
for (const _l of d3Range(i)) {
|
||||||
|
const index = leastIndex(this.grid.cells.c[cur], (a: number, b: number) => this.heights![a] - this.heights![b]);
|
||||||
|
if(index === undefined) continue;
|
||||||
|
const min = this.grid.cells.c[cur][index]; // downhill cell
|
||||||
|
this.heights![min] = (this.heights![cur] * 2 + this.heights![min]) / 3;
|
||||||
|
cur = min;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const desiredRangeCount = getNumberInRange(count);
|
||||||
|
for (let i = 0; i < desiredRangeCount; i++) {
|
||||||
|
addOneRange();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addTrough(count: string, height: string, rangeX: string, rangeY: string, startCellId?: number, endCellId?: number): void {
|
||||||
|
const addOneTrough = () => {
|
||||||
|
if(!this.heights || !this.grid) return;
|
||||||
|
|
||||||
|
// get main ridge
|
||||||
|
const getRange = (cur: number, end: number) => {
|
||||||
|
const range = [cur];
|
||||||
|
const p = this.grid.points;
|
||||||
|
used[cur] = 1;
|
||||||
|
|
||||||
|
while (cur !== end) {
|
||||||
|
let min = Infinity;
|
||||||
|
this.grid.cells.c[cur].forEach((e: number) => {
|
||||||
|
if (used[e]) return;
|
||||||
|
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||||
|
if (Math.random() > 0.8) diff = diff / 2;
|
||||||
|
if (diff < min) {
|
||||||
|
min = diff;
|
||||||
|
cur = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (min === Infinity) return range;
|
||||||
|
range.push(cur);
|
||||||
|
used[cur] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
const used = new Uint8Array(this.heights.length);
|
||||||
|
let h = lim(getNumberInRange(height));
|
||||||
|
|
||||||
|
if (rangeX && rangeY) {
|
||||||
|
// find start and end points
|
||||||
|
let limit = 0;
|
||||||
|
let startX: number;
|
||||||
|
let startY: number;
|
||||||
|
let dist = 0;
|
||||||
|
let endX: number;
|
||||||
|
let endY: number;
|
||||||
|
do {
|
||||||
|
startX = this.getPointInRange(rangeX, this.graphWidth) as number;
|
||||||
|
startY = this.getPointInRange(rangeY, this.graphHeight) as number;
|
||||||
|
startCellId = findGridCell(startX, startY, this.grid);
|
||||||
|
limit++;
|
||||||
|
} while (this.heights[startCellId] < 20 && limit < 50);
|
||||||
|
|
||||||
|
limit = 0;
|
||||||
|
do {
|
||||||
|
endX = Math.random() * this.graphWidth * 0.8 + this.graphWidth * 0.1;
|
||||||
|
endY = Math.random() * this.graphHeight * 0.7 + this.graphHeight * 0.15;
|
||||||
|
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
|
||||||
|
limit++;
|
||||||
|
} while ((dist < this.graphWidth / 8 || dist > this.graphWidth / 2) && limit < 50);
|
||||||
|
|
||||||
|
endCellId = findGridCell(endX, endY, this.grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
let range = getRange(startCellId as number, endCellId as number);
|
||||||
|
|
||||||
|
|
||||||
|
// add height to ridge and cells around
|
||||||
|
let queue = range.slice(),
|
||||||
|
i = 0;
|
||||||
|
while (queue.length) {
|
||||||
|
const frontier = queue.slice();
|
||||||
|
(queue = []), i++;
|
||||||
|
frontier.forEach((i: number) => {
|
||||||
|
this.heights![i] = lim(this.heights![i] - h * (Math.random() * 0.3 + 0.85));
|
||||||
|
});
|
||||||
|
h = h ** this.linePower - 1;
|
||||||
|
if (h < 2) break;
|
||||||
|
frontier.forEach((f: number) => {
|
||||||
|
this.grid.cells.c[f].forEach((i: number) => {
|
||||||
|
if (!used[i]) {
|
||||||
|
queue.push(i);
|
||||||
|
used[i] = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate prominences
|
||||||
|
range.forEach((cur: number, d: number) => {
|
||||||
|
if (d % 6 !== 0) return;
|
||||||
|
for (const _l of d3Range(i)) {
|
||||||
|
const index = leastIndex(this.grid.cells.c[cur], (a: number, b: number) => this.heights![a] - this.heights![b]);
|
||||||
|
if(index === undefined) continue;
|
||||||
|
const min = this.grid.cells.c[cur][index]; // downhill cell
|
||||||
|
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
|
||||||
|
this.heights![min] = (this.heights![cur] * 2 + this.heights![min]) / 3;
|
||||||
|
cur = min;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const desiredTroughCount = getNumberInRange(count);
|
||||||
|
for(let i = 0; i < desiredTroughCount; i++) {
|
||||||
|
addOneTrough();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addStrait(width: string, direction = "vertical"): void {
|
||||||
|
if(!this.heights || !this.grid) return;
|
||||||
|
const desiredWidth = Math.min(getNumberInRange(width), this.grid.cellsX / 3);
|
||||||
|
if (desiredWidth < 1 && P(desiredWidth)) return;
|
||||||
|
const used = new Uint8Array(this.heights.length);
|
||||||
|
const vert = direction === "vertical";
|
||||||
|
const startX = vert ? Math.floor(Math.random() * this.graphWidth * 0.4 + this.graphWidth * 0.3) : 5;
|
||||||
|
const startY = vert ? 5 : Math.floor(Math.random() * this.graphHeight * 0.4 + this.graphHeight * 0.3);
|
||||||
|
const endX = vert
|
||||||
|
? Math.floor(this.graphWidth - startX - this.graphWidth * 0.1 + Math.random() * this.graphWidth * 0.2)
|
||||||
|
: this.graphWidth - 5;
|
||||||
|
const endY = vert
|
||||||
|
? this.graphHeight - 5
|
||||||
|
: Math.floor(this.graphHeight - startY - this.graphHeight * 0.1 + Math.random() * this.graphHeight * 0.2);
|
||||||
|
|
||||||
|
const start = findGridCell(startX, startY, this.grid);
|
||||||
|
const end = findGridCell(endX, endY, this.grid);
|
||||||
|
|
||||||
|
const getRange = (cur: number, end: number) => {
|
||||||
|
const range = [];
|
||||||
|
const p = this.grid.points;
|
||||||
|
|
||||||
|
while (cur !== end) {
|
||||||
|
let min = Infinity;
|
||||||
|
this.grid.cells.c[cur].forEach((e: number) => {
|
||||||
|
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||||
|
if (Math.random() > 0.8) diff = diff / 2;
|
||||||
|
if (diff < min) {
|
||||||
|
min = diff;
|
||||||
|
cur = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
range.push(cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
let range = getRange(start, end);
|
||||||
|
const query: number[] = [];
|
||||||
|
|
||||||
|
|
||||||
|
const step = 0.1 / desiredWidth;
|
||||||
|
|
||||||
|
for(let i = 0; i < desiredWidth; i++) {
|
||||||
|
const exp = 0.9 - step * desiredWidth;
|
||||||
|
range.forEach((r: number) => {
|
||||||
|
this.grid.cells.c[r].forEach((e: number) => {
|
||||||
|
if (used[e]) return;
|
||||||
|
used[e] = 1;
|
||||||
|
query.push(e);
|
||||||
|
this.heights![e] **= exp;
|
||||||
|
if (this.heights![e] > 100) this.heights![e] = 5;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
range = query.slice();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
modify(range: string, add: number, mult: number, power?: number): void {
|
||||||
|
if(!this.heights) return;
|
||||||
|
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
|
||||||
|
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
|
||||||
|
const isLand = min === 20;
|
||||||
|
|
||||||
|
this.heights = this.heights.map(h => {
|
||||||
|
if (h < min || h > max) return h;
|
||||||
|
|
||||||
|
if (add) h = isLand ? Math.max(h + add, 20) : h + add;
|
||||||
|
if (mult !== 1) h = isLand ? (h - 20) * mult + 20 : h * mult;
|
||||||
|
if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
|
||||||
|
return lim(h);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
smooth(fr = 2, add = 0): void {
|
||||||
|
if(!this.heights || !this.grid) return;
|
||||||
|
this.heights = this.heights.map((h, i) => {
|
||||||
|
const a = [h];
|
||||||
|
this.grid.cells.c[i].forEach((c: number) => a.push(this.heights![c]));
|
||||||
|
if (fr === 1) return (mean(a) as number) + add;
|
||||||
|
return lim((h * (fr - 1) + (mean(a) as number) + add) / fr);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
mask(power = 1): void {
|
||||||
|
if(!this.heights || !this.grid) return;
|
||||||
|
const fr = power ? Math.abs(power) : 1;
|
||||||
|
|
||||||
|
this.heights = this.heights.map((h, i) => {
|
||||||
|
const [x, y] = this.grid.points[i];
|
||||||
|
const nx = (2 * x) / this.graphWidth - 1; // [-1, 1], 0 is center
|
||||||
|
const ny = (2 * y) / this.graphHeight - 1; // [-1, 1], 0 is center
|
||||||
|
let distance = (1 - nx ** 2) * (1 - ny ** 2); // 1 is center, 0 is edge
|
||||||
|
if (power < 0) distance = 1 - distance; // inverted, 0 is center, 1 is edge
|
||||||
|
const masked = h * distance;
|
||||||
|
return lim((h * (fr - 1) + masked) / fr);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
invert(count: number, axes: string): void {
|
||||||
|
if (!P(count) || !this.heights || !this.grid) return;
|
||||||
|
|
||||||
|
const invertX = axes !== "y";
|
||||||
|
const invertY = axes !== "x";
|
||||||
|
const {cellsX, cellsY} = this.grid;
|
||||||
|
|
||||||
|
const inverted = this.heights.map((_h: number, i: number) => {
|
||||||
|
if(!this.heights) return 0;
|
||||||
|
const x = i % cellsX;
|
||||||
|
const y = Math.floor(i / cellsX);
|
||||||
|
|
||||||
|
const nx = invertX ? cellsX - x - 1 : x;
|
||||||
|
const ny = invertY ? cellsY - y - 1 : y;
|
||||||
|
const invertedI = nx + ny * cellsX;
|
||||||
|
return this.heights[invertedI];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.heights = inverted;
|
||||||
|
};
|
||||||
|
|
||||||
|
addStep(tool: Tool, a2: string, a3: string, a4: string, a5: string): void {
|
||||||
|
if (tool === "Hill") return this.addHill(a2, a3, a4, a5);
|
||||||
|
if (tool === "Pit") return this.addPit(a2, a3, a4, a5);
|
||||||
|
if (tool === "Range") return this.addRange(a2, a3, a4, a5);
|
||||||
|
if (tool === "Trough") return this.addTrough(a2, a3, a4, a5);
|
||||||
|
if (tool === "Strait") return this.addStrait(a2, a3);
|
||||||
|
if (tool === "Mask") return this.mask(+a2);
|
||||||
|
if (tool === "Invert") return this.invert(+a2, a3);
|
||||||
|
if (tool === "Add") return this.modify(a3, +a2, 1);
|
||||||
|
if (tool === "Multiply") return this.modify(a3, 0, +a2);
|
||||||
|
if (tool === "Smooth") return this.smooth(+a2);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generate(graph: any): Promise<Uint8Array> {
|
||||||
|
TIME && console.time("defineHeightmap");
|
||||||
|
const id = (byId("templateInput")! as HTMLInputElement).value;
|
||||||
|
|
||||||
|
Math.random = Alea(this.seed);
|
||||||
|
const isTemplate = id in heightmapTemplates;
|
||||||
|
|
||||||
|
const heights = isTemplate ? this.fromTemplate(graph, id) : await this.fromPrecreated(graph, id);
|
||||||
|
TIME && console.timeEnd("defineHeightmap");
|
||||||
|
|
||||||
|
this.clearData();
|
||||||
|
return heights as Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
fromTemplate(graph: any, id: string): Uint8Array | null {
|
||||||
|
const templateString = heightmapTemplates[id]?.template || "";
|
||||||
|
const steps = templateString.split("\n");
|
||||||
|
|
||||||
|
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`);
|
||||||
|
this.setGraph(graph);
|
||||||
|
|
||||||
|
for (const step of steps) {
|
||||||
|
const elements = step.trim().split(" ");
|
||||||
|
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${id}. Step: ${elements}`);
|
||||||
|
this.addStep(...elements as [Tool, string, string, string, string]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.heights;
|
||||||
|
};
|
||||||
|
|
||||||
|
private getHeightsFromImageData(imageData: Uint8ClampedArray): void {
|
||||||
|
if(!this.heights) return;
|
||||||
|
for (let i = 0; i < this.heights.length; i++) {
|
||||||
|
const lightness = imageData[i * 4] / 255;
|
||||||
|
const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8;
|
||||||
|
this.heights[i] = minmax(Math.floor(powered * 100), 0, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fromPrecreated(graph: any, id: string): Promise<Uint8Array> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
// create canvas where 1px corresponds to a cell
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||||
|
const {cellsX, cellsY} = graph;
|
||||||
|
canvas.width = cellsX;
|
||||||
|
canvas.height = cellsY;
|
||||||
|
|
||||||
|
// load heightmap into image and render to canvas
|
||||||
|
const img = new Image();
|
||||||
|
img.src = `./heightmaps/${id}.png`;
|
||||||
|
img.onload = () => {
|
||||||
|
if(!ctx) {
|
||||||
|
throw new Error("Could not get canvas context");
|
||||||
|
}
|
||||||
|
if(!this.heights) {
|
||||||
|
throw new Error("Heights array is not initialized");
|
||||||
|
}
|
||||||
|
ctx.drawImage(img, 0, 0, cellsX, cellsY);
|
||||||
|
const imageData = ctx.getImageData(0, 0, cellsX, cellsY);
|
||||||
|
this.setGraph(graph);
|
||||||
|
this.getHeightsFromImageData(imageData.data);
|
||||||
|
canvas.remove();
|
||||||
|
img.remove();
|
||||||
|
resolve(this.heights);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getHeights() {
|
||||||
|
return this.heights;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.HeightmapGenerator = new HeightmapGenerator();
|
||||||
2
src/modules/index.ts
Normal file
2
src/modules/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
import "./voronoi";
|
||||||
|
import "./heightmap-generator";
|
||||||
|
|
@ -1,17 +1,27 @@
|
||||||
class Voronoi {
|
import Delaunator from "delaunator";
|
||||||
/**
|
export type Vertices = { p: Point[], v: number[][], c: number[][] };
|
||||||
* Creates a Voronoi diagram from the given Delaunator, a list of points, and the number of points. The Voronoi diagram is constructed using (I think) the {@link https://en.wikipedia.org/wiki/Bowyer%E2%80%93Watson_algorithm |Bowyer-Watson Algorithm}
|
export type Cells = { v: number[][], c: number[][], b: number[], i: Uint32Array<ArrayBufferLike> } ;
|
||||||
* The {@link https://github.com/mapbox/delaunator/ |Delaunator} library uses {@link https://en.wikipedia.org/wiki/Doubly_connected_edge_list |half-edges} to represent the relationship between points and triangles.
|
export type Point = [number, number];
|
||||||
* @param {{triangles: Uint32Array, halfedges: Int32Array}} delaunay A {@link https://github.com/mapbox/delaunator/blob/master/index.js |Delaunator} instance.
|
|
||||||
* @param {[number, number][]} points A list of coordinates.
|
/**
|
||||||
* @param {number} pointsN The number of points.
|
* Creates a Voronoi diagram from the given Delaunator, a list of points, and the number of points. The Voronoi diagram is constructed using (I think) the {@link https://en.wikipedia.org/wiki/Bowyer%E2%80%93Watson_algorithm |Bowyer-Watson Algorithm}
|
||||||
*/
|
* The {@link https://github.com/mapbox/delaunator/ |Delaunator} library uses {@link https://en.wikipedia.org/wiki/Doubly_connected_edge_list |half-edges} to represent the relationship between points and triangles.
|
||||||
constructor(delaunay, points, pointsN) {
|
* @param {{triangles: Uint32Array, halfedges: Int32Array}} delaunay A {@link https://github.com/mapbox/delaunator/blob/master/index.js |Delaunator} instance.
|
||||||
|
* @param {[number, number][]} points A list of coordinates.
|
||||||
|
* @param {number} pointsN The number of points.
|
||||||
|
*/
|
||||||
|
export class Voronoi {
|
||||||
|
delaunay: Delaunator<Float64Array<ArrayBufferLike>>
|
||||||
|
points: Point[];
|
||||||
|
pointsN: number;
|
||||||
|
cells: Cells = { v: [], c: [], b: [], i: new Uint32Array() }; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell, i = cell indexes;
|
||||||
|
vertices: Vertices = { p: [], v: [], c: [] }; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
|
||||||
|
|
||||||
|
constructor(delaunay: Delaunator<Float64Array<ArrayBufferLike>>, points: Point[], pointsN: number) {
|
||||||
this.delaunay = delaunay;
|
this.delaunay = delaunay;
|
||||||
this.points = points;
|
this.points = points;
|
||||||
this.pointsN = pointsN;
|
this.pointsN = pointsN;
|
||||||
this.cells = { v: [], c: [], b: [] }; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
|
this.vertices
|
||||||
this.vertices = { p: [], v: [], c: [] }; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
|
|
||||||
|
|
||||||
// Half-edges are the indices into the delaunator outputs:
|
// Half-edges are the indices into the delaunator outputs:
|
||||||
// delaunay.triangles[e] gives the point ID where the half-edge starts
|
// delaunay.triangles[e] gives the point ID where the half-edge starts
|
||||||
|
|
@ -40,18 +50,18 @@ class Voronoi {
|
||||||
* @param {number} t The index of the triangle
|
* @param {number} t The index of the triangle
|
||||||
* @returns {[number, number, number]} The IDs of the points comprising the given triangle.
|
* @returns {[number, number, number]} The IDs of the points comprising the given triangle.
|
||||||
*/
|
*/
|
||||||
pointsOfTriangle(t) {
|
private pointsOfTriangle(triangleIndex: number): [number, number, number] {
|
||||||
return this.edgesOfTriangle(t).map(edge => this.delaunay.triangles[edge]);
|
return this.edgesOfTriangle(triangleIndex).map(edge => this.delaunay.triangles[edge]) as [number, number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifies what triangles are adjacent to the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-triangles| the Delaunator docs.}
|
* Identifies what triangles are adjacent to the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-triangles| the Delaunator docs.}
|
||||||
* @param {number} t The index of the triangle
|
* @param {number} triangleIndex The index of the triangle
|
||||||
* @returns {number[]} The indices of the triangles that share half-edges with this triangle.
|
* @returns {number[]} The indices of the triangles that share half-edges with this triangle.
|
||||||
*/
|
*/
|
||||||
trianglesAdjacentToTriangle(t) {
|
private trianglesAdjacentToTriangle(triangleIndex: number): number[] {
|
||||||
let triangles = [];
|
let triangles = [];
|
||||||
for (let edge of this.edgesOfTriangle(t)) {
|
for (let edge of this.edgesOfTriangle(triangleIndex)) {
|
||||||
let opposite = this.delaunay.halfedges[edge];
|
let opposite = this.delaunay.halfedges[edge];
|
||||||
triangles.push(this.triangleOfEdge(opposite));
|
triangles.push(this.triangleOfEdge(opposite));
|
||||||
}
|
}
|
||||||
|
|
@ -61,9 +71,9 @@ class Voronoi {
|
||||||
/**
|
/**
|
||||||
* Gets the indices of all the incoming and outgoing half-edges that touch the given point. Taken from {@link https://mapbox.github.io/delaunator/#point-to-edges| the Delaunator docs.}
|
* Gets the indices of all the incoming and outgoing half-edges that touch the given point. Taken from {@link https://mapbox.github.io/delaunator/#point-to-edges| the Delaunator docs.}
|
||||||
* @param {number} start The index of an incoming half-edge that leads to the desired point
|
* @param {number} start The index of an incoming half-edge that leads to the desired point
|
||||||
* @returns {number[]} The indices of all half-edges (incoming or outgoing) that touch the point.
|
* @returns {[number, number, number]} The indices of all half-edges (incoming or outgoing) that touch the point.
|
||||||
*/
|
*/
|
||||||
edgesAroundPoint(start) {
|
private edgesAroundPoint(start: number): [number, number, number] {
|
||||||
const result = [];
|
const result = [];
|
||||||
let incoming = start;
|
let incoming = start;
|
||||||
do {
|
do {
|
||||||
|
|
@ -71,46 +81,46 @@ class Voronoi {
|
||||||
const outgoing = this.nextHalfedge(incoming);
|
const outgoing = this.nextHalfedge(incoming);
|
||||||
incoming = this.delaunay.halfedges[outgoing];
|
incoming = this.delaunay.halfedges[outgoing];
|
||||||
} while (incoming !== -1 && incoming !== start && result.length < 20);
|
} while (incoming !== -1 && incoming !== start && result.length < 20);
|
||||||
return result;
|
return result as [number, number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the center of the triangle located at the given index.
|
* Returns the center of the triangle located at the given index.
|
||||||
* @param {number} t The index of the triangle
|
* @param {number} triangleIndex The index of the triangle
|
||||||
* @returns {[number, number]}
|
* @returns {[number, number]} The coordinates of the triangle's circumcenter.
|
||||||
*/
|
*/
|
||||||
triangleCenter(t) {
|
private triangleCenter(triangleIndex: number): Point {
|
||||||
let vertices = this.pointsOfTriangle(t).map(p => this.points[p]);
|
let vertices = this.pointsOfTriangle(triangleIndex).map(p => this.points[p]);
|
||||||
return this.circumcenter(vertices[0], vertices[1], vertices[2]);
|
return this.circumcenter(vertices[0], vertices[1], vertices[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all of the half-edges for a specific triangle `t`. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
|
* Retrieves all of the half-edges for a specific triangle `triangleIndex`. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
|
||||||
* @param {number} t The index of the triangle
|
* @param {number} triangleIndex The index of the triangle
|
||||||
* @returns {[number, number, number]} The edges of the triangle.
|
* @returns {[number, number, number]} The edges of the triangle.
|
||||||
*/
|
*/
|
||||||
edgesOfTriangle(t) { return [3 * t, 3 * t + 1, 3 * t + 2]; }
|
private edgesOfTriangle(triangleIndex: number): [number, number, number] { return [3 * triangleIndex, 3 * triangleIndex + 1, 3 * triangleIndex + 2]; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables lookup of a triangle, given one of the half-edges of that triangle. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
|
* Enables lookup of a triangle, given one of the half-edges of that triangle. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
|
||||||
* @param {number} e The index of the edge
|
* @param {number} e The index of the edge
|
||||||
* @returns {number} The index of the triangle
|
* @returns {number} The index of the triangle
|
||||||
*/
|
*/
|
||||||
triangleOfEdge(e) { return Math.floor(e / 3); }
|
private triangleOfEdge(e: number): number { return Math.floor(e / 3); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves to the next half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
|
* Moves to the next half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
|
||||||
* @param {number} e The index of the current half edge
|
* @param {number} e The index of the current half edge
|
||||||
* @returns {number} The index of the next half edge
|
* @returns {number} The index of the next half edge
|
||||||
*/
|
*/
|
||||||
nextHalfedge(e) { return (e % 3 === 2) ? e - 2 : e + 1; }
|
private nextHalfedge(e: number): number { return (e % 3 === 2) ? e - 2 : e + 1; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves to the previous half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
|
* Moves to the previous half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
|
||||||
* @param {number} e The index of the current half edge
|
* @param {number} e The index of the current half edge
|
||||||
* @returns {number} The index of the previous half edge
|
* @returns {number} The index of the previous half edge
|
||||||
*/
|
*/
|
||||||
prevHalfedge(e) { return (e % 3 === 0) ? e + 2 : e - 1; }
|
// private prevHalfedge(e: number): number { return (e % 3 === 0) ? e + 2 : e - 1; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the circumcenter of the triangle identified by points a, b, and c. Taken from {@link https://en.wikipedia.org/wiki/Circumscribed_circle#Circumcenter_coordinates| Wikipedia}
|
* Finds the circumcenter of the triangle identified by points a, b, and c. Taken from {@link https://en.wikipedia.org/wiki/Circumscribed_circle#Circumcenter_coordinates| Wikipedia}
|
||||||
|
|
@ -119,7 +129,7 @@ class Voronoi {
|
||||||
* @param {[number, number]} c The coordinates of the third point of the triangle
|
* @param {[number, number]} c The coordinates of the third point of the triangle
|
||||||
* @return {[number, number]} The coordinates of the circumcenter of the triangle.
|
* @return {[number, number]} The coordinates of the circumcenter of the triangle.
|
||||||
*/
|
*/
|
||||||
circumcenter(a, b, c) {
|
private circumcenter(a: Point, b: Point, c: Point): Point {
|
||||||
const [ax, ay] = a;
|
const [ax, ay] = a;
|
||||||
const [bx, by] = b;
|
const [bx, by] = b;
|
||||||
const [cx, cy] = c;
|
const [cx, cy] = c;
|
||||||
|
|
@ -132,6 +142,4 @@ class Voronoi {
|
||||||
Math.floor(1 / D * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax)))
|
Math.floor(1 / D * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax)))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Voronoi = Voronoi;
|
|
||||||
|
|
@ -78,7 +78,7 @@ export const getTypedArray = (maxValue: number) => {
|
||||||
* @param {Array} [options.from] - An optional array to create the typed array from
|
* @param {Array} [options.from] - An optional array to create the typed array from
|
||||||
* @returns The created typed array
|
* @returns The created typed array
|
||||||
*/
|
*/
|
||||||
export const createTypedArray = ({maxValue, length, from}: {maxValue: number; length: number; from?: ArrayLike<number>}) => {
|
export const createTypedArray = ({maxValue, length, from}: {maxValue: number; length: number; from?: ArrayLike<number>}): Uint8Array | Uint16Array | Uint32Array => {
|
||||||
const typedArray = getTypedArray(maxValue);
|
const typedArray = getTypedArray(maxValue);
|
||||||
if (!from) return new typedArray(length);
|
if (!from) return new typedArray(length);
|
||||||
return typedArray.from(from);
|
return typedArray.from(from);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { color } from "d3";
|
||||||
import { byId } from "./shorthands";
|
import { byId } from "./shorthands";
|
||||||
import { rn } from "./numberUtils";
|
import { rn } from "./numberUtils";
|
||||||
import { createTypedArray } from "./arrayUtils";
|
import { createTypedArray } from "./arrayUtils";
|
||||||
|
import { Cells, Vertices, Voronoi, Point } from "../modules/voronoi";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get boundary points on a regular square grid
|
* Get boundary points on a regular square grid
|
||||||
|
|
@ -12,14 +13,14 @@ import { createTypedArray } from "./arrayUtils";
|
||||||
* @param {number} spacing - The spacing between points
|
* @param {number} spacing - The spacing between points
|
||||||
* @returns {Array} - An array of boundary points
|
* @returns {Array} - An array of boundary points
|
||||||
*/
|
*/
|
||||||
const getBoundaryPoints = (width: number, height: number, spacing: number) => {
|
const getBoundaryPoints = (width: number, height: number, spacing: number): Point[] => {
|
||||||
const offset = rn(-1 * spacing);
|
const offset = rn(-1 * spacing);
|
||||||
const bSpacing = spacing * 2;
|
const bSpacing = spacing * 2;
|
||||||
const w = width - offset * 2;
|
const w = width - offset * 2;
|
||||||
const h = height - offset * 2;
|
const h = height - offset * 2;
|
||||||
const numberX = Math.ceil(w / bSpacing) - 1;
|
const numberX = Math.ceil(w / bSpacing) - 1;
|
||||||
const numberY = Math.ceil(h / bSpacing) - 1;
|
const numberY = Math.ceil(h / bSpacing) - 1;
|
||||||
const points = [];
|
const points: Point[] = [];
|
||||||
|
|
||||||
for (let i = 0.5; i < numberX; i++) {
|
for (let i = 0.5; i < numberX; i++) {
|
||||||
let x = Math.ceil((w * i) / numberX + offset);
|
let x = Math.ceil((w * i) / numberX + offset);
|
||||||
|
|
@ -41,13 +42,13 @@ const getBoundaryPoints = (width: number, height: number, spacing: number) => {
|
||||||
* @param {number} spacing - The spacing between points
|
* @param {number} spacing - The spacing between points
|
||||||
* @returns {Array} - An array of jittered grid points
|
* @returns {Array} - An array of jittered grid points
|
||||||
*/
|
*/
|
||||||
const getJitteredGrid = (width: number, height: number, spacing: number): number[][] => {
|
const getJitteredGrid = (width: number, height: number, spacing: number): Point[] => {
|
||||||
const radius = spacing / 2; // square radius
|
const radius = spacing / 2; // square radius
|
||||||
const jittering = radius * 0.9; // max deviation
|
const jittering = radius * 0.9; // max deviation
|
||||||
const doubleJittering = jittering * 2;
|
const doubleJittering = jittering * 2;
|
||||||
const jitter = () => Math.random() * doubleJittering - jittering;
|
const jitter = () => Math.random() * doubleJittering - jittering;
|
||||||
|
|
||||||
let points: number[][] = [];
|
let points: Point[] = [];
|
||||||
for (let y = radius; y < height; y += spacing) {
|
for (let y = radius; y < height; y += spacing) {
|
||||||
for (let x = radius; x < width; x += spacing) {
|
for (let x = radius; x < width; x += spacing) {
|
||||||
const xj = Math.min(rn(x + jitter(), 2), width);
|
const xj = Math.min(rn(x + jitter(), 2), width);
|
||||||
|
|
@ -64,18 +65,18 @@ const getJitteredGrid = (width: number, height: number, spacing: number): number
|
||||||
* @param {number} graphHeight - The height of the graph
|
* @param {number} graphHeight - The height of the graph
|
||||||
* @returns {Object} - An object containing spacing, cellsDesired, boundary points, grid points, cellsX, and cellsY
|
* @returns {Object} - An object containing spacing, cellsDesired, boundary points, grid points, cellsX, and cellsY
|
||||||
*/
|
*/
|
||||||
const placePoints = (graphWidth: number, graphHeight: number) => {
|
const placePoints = (graphWidth: number, graphHeight: number): {spacing: number, cellsDesired: number, boundary: Point[], points: Point[], cellsX: number, cellsY: number} => {
|
||||||
window.TIME && console.time("placePoints");
|
TIME && console.time("placePoints");
|
||||||
const cellsDesired = +(byId("pointsInput")?.dataset.cells || 0);
|
const cellsDesired = +(byId("pointsInput")?.dataset.cells || 0);
|
||||||
const spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); // spacing between points before jirrering
|
const spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); // spacing between points before jittering
|
||||||
|
|
||||||
const boundary = getBoundaryPoints(graphWidth, graphHeight, spacing);
|
const boundary = getBoundaryPoints(graphWidth, graphHeight, spacing);
|
||||||
const points = getJitteredGrid(graphWidth, graphHeight, spacing); // points of jittered square grid
|
const points = getJitteredGrid(graphWidth, graphHeight, spacing); // points of jittered square grid
|
||||||
const cellsX = Math.floor((graphWidth + 0.5 * spacing - 1e-10) / spacing);
|
const cellCountX = Math.floor((graphWidth + 0.5 * spacing - 1e-10) / spacing); // number of cells in x direction
|
||||||
const cellsY = Math.floor((graphHeight + 0.5 * spacing - 1e-10) / spacing);
|
const cellCountY = Math.floor((graphHeight + 0.5 * spacing - 1e-10) / spacing); // number of cells in y direction
|
||||||
window.TIME && console.timeEnd("placePoints");
|
TIME && console.timeEnd("placePoints");
|
||||||
|
|
||||||
return {spacing, cellsDesired, boundary, points, cellsX, cellsY};
|
return {spacing, cellsDesired, boundary, points, cellsX: cellCountX, cellsY: cellCountY};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -100,11 +101,22 @@ export const shouldRegenerateGrid = (grid: any, expectedSeed: number, graphWidth
|
||||||
return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY;
|
return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Grid {
|
||||||
|
spacing: number;
|
||||||
|
cellsDesired: number;
|
||||||
|
boundary: Point[];
|
||||||
|
points: Point[];
|
||||||
|
cellsX: number;
|
||||||
|
cellsY: number;
|
||||||
|
seed: string | number;
|
||||||
|
cells: Cells;
|
||||||
|
vertices: Vertices;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Generates a Voronoi grid based on jittered grid points
|
* Generates a Voronoi grid based on jittered grid points
|
||||||
* @returns {Object} - The generated grid object containing spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, and seed
|
* @returns {Object} - The generated grid object containing spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, and seed
|
||||||
*/
|
*/
|
||||||
export const generateGrid = (seed: string, graphWidth: number, graphHeight: number) => {
|
export const generateGrid = (seed: string, graphWidth: number, graphHeight: number): Grid => {
|
||||||
Math.random = Alea(seed); // reset PRNG
|
Math.random = Alea(seed); // reset PRNG
|
||||||
const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints(graphWidth, graphHeight);
|
const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints(graphWidth, graphHeight);
|
||||||
const {cells, vertices} = calculateVoronoi(points, boundary);
|
const {cells, vertices} = calculateVoronoi(points, boundary);
|
||||||
|
|
@ -117,19 +129,19 @@ export const generateGrid = (seed: string, graphWidth: number, graphHeight: numb
|
||||||
* @param {Array} boundary - The boundary points to clip the Voronoi cells
|
* @param {Array} boundary - The boundary points to clip the Voronoi cells
|
||||||
* @returns {Object} - An object containing Voronoi cells and vertices
|
* @returns {Object} - An object containing Voronoi cells and vertices
|
||||||
*/
|
*/
|
||||||
export const calculateVoronoi = (points: number[][], boundary: number[][]) => {
|
export const calculateVoronoi = (points: Point[], boundary: Point[]): {cells: Cells, vertices: Vertices} => {
|
||||||
window.TIME && console.time("calculateDelaunay");
|
TIME && console.time("calculateDelaunay");
|
||||||
const allPoints = points.concat(boundary);
|
const allPoints = points.concat(boundary);
|
||||||
const delaunay = Delaunator.from(allPoints);
|
const delaunay = Delaunator.from(allPoints);
|
||||||
window.TIME && console.timeEnd("calculateDelaunay");
|
TIME && console.timeEnd("calculateDelaunay");
|
||||||
|
|
||||||
window.TIME && console.time("calculateVoronoi");
|
TIME && console.time("calculateVoronoi");
|
||||||
const voronoi = new window.Voronoi(delaunay, allPoints, points.length);
|
const voronoi = new Voronoi(delaunay, allPoints, points.length);
|
||||||
|
|
||||||
const cells = voronoi.cells;
|
const cells = voronoi.cells;
|
||||||
cells.i = createTypedArray({maxValue: points.length, length: points.length}).map((_, i) => i); // array of indexes
|
cells.i = createTypedArray({maxValue: points.length, length: points.length}).map((_, i) => i) as Uint32Array; // array of indexes
|
||||||
const vertices = voronoi.vertices;
|
const vertices = voronoi.vertices;
|
||||||
window.TIME && console.timeEnd("calculateVoronoi");
|
TIME && console.timeEnd("calculateVoronoi");
|
||||||
|
|
||||||
return {cells, vertices};
|
return {cells, vertices};
|
||||||
}
|
}
|
||||||
|
|
@ -432,9 +444,8 @@ export const drawHeights = ({heights, width, height, scheme, renderOcean}: {heig
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
var TIME: boolean;
|
||||||
interface Window {
|
interface Window {
|
||||||
TIME: boolean;
|
|
||||||
Voronoi: any;
|
|
||||||
|
|
||||||
shouldRegenerateGrid: typeof shouldRegenerateGrid;
|
shouldRegenerateGrid: typeof shouldRegenerateGrid;
|
||||||
generateGrid: typeof generateGrid;
|
generateGrid: typeof generateGrid;
|
||||||
|
|
|
||||||
|
|
@ -143,4 +143,94 @@ window.drawCellsValue = (data:any[]) => drawCellsValue(data, (window as any).pac
|
||||||
window.drawPolygons = (data: any[]) => drawPolygons(data, (window as any).terrs, (window as any).grid);
|
window.drawPolygons = (data: any[]) => drawPolygons(data, (window as any).terrs, (window as any).grid);
|
||||||
window.drawRouteConnections = () => drawRouteConnections((window as any).packedGraph);
|
window.drawRouteConnections = () => drawRouteConnections((window as any).packedGraph);
|
||||||
window.drawPoint = drawPoint;
|
window.drawPoint = drawPoint;
|
||||||
window.drawPath = drawPath;
|
window.drawPath = drawPath;
|
||||||
|
|
||||||
|
|
||||||
|
export {
|
||||||
|
rn,
|
||||||
|
lim,
|
||||||
|
minmax,
|
||||||
|
normalize,
|
||||||
|
lerp,
|
||||||
|
isVowel,
|
||||||
|
trimVowels,
|
||||||
|
getAdjective,
|
||||||
|
nth,
|
||||||
|
abbreviate,
|
||||||
|
list,
|
||||||
|
last,
|
||||||
|
unique,
|
||||||
|
deepCopy,
|
||||||
|
getTypedArray,
|
||||||
|
createTypedArray,
|
||||||
|
TYPED_ARRAY_MAX_VALUES,
|
||||||
|
rand,
|
||||||
|
P,
|
||||||
|
each,
|
||||||
|
gauss,
|
||||||
|
Pint,
|
||||||
|
biased,
|
||||||
|
generateSeed,
|
||||||
|
getNumberInRange,
|
||||||
|
ra,
|
||||||
|
rw,
|
||||||
|
convertTemperature,
|
||||||
|
si,
|
||||||
|
getIntegerFromSI,
|
||||||
|
toHEX,
|
||||||
|
getColors,
|
||||||
|
getRandomColor,
|
||||||
|
getMixedColor,
|
||||||
|
C_12,
|
||||||
|
getComposedPath,
|
||||||
|
getNextId,
|
||||||
|
rollups,
|
||||||
|
distanceSquared,
|
||||||
|
getIsolines,
|
||||||
|
getPolesOfInaccessibility,
|
||||||
|
connectVertices,
|
||||||
|
findPath,
|
||||||
|
getVertexPath,
|
||||||
|
round,
|
||||||
|
capitalize,
|
||||||
|
splitInTwo,
|
||||||
|
parseTransform,
|
||||||
|
isValidJSON,
|
||||||
|
safeParseJSON,
|
||||||
|
sanitizeId,
|
||||||
|
byId,
|
||||||
|
shouldRegenerateGrid,
|
||||||
|
generateGrid,
|
||||||
|
findGridAll,
|
||||||
|
findGridCell,
|
||||||
|
findClosestCell,
|
||||||
|
calculateVoronoi,
|
||||||
|
findAllCellsInRadius,
|
||||||
|
getPackPolygon,
|
||||||
|
getGridPolygon,
|
||||||
|
poissonDiscSampler,
|
||||||
|
isLand,
|
||||||
|
isWater,
|
||||||
|
findAllInQuadtree,
|
||||||
|
drawHeights,
|
||||||
|
clipPoly,
|
||||||
|
getSegmentId,
|
||||||
|
debounce,
|
||||||
|
throttle,
|
||||||
|
parseError,
|
||||||
|
getBase64,
|
||||||
|
openURL,
|
||||||
|
wiki,
|
||||||
|
link,
|
||||||
|
isCtrlClick,
|
||||||
|
generateDate,
|
||||||
|
getLongitude,
|
||||||
|
getLatitude,
|
||||||
|
getCoordinates,
|
||||||
|
initializePrompt,
|
||||||
|
drawCellsValue,
|
||||||
|
drawPolygons,
|
||||||
|
drawRouteConnections,
|
||||||
|
drawPoint,
|
||||||
|
drawPath
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue