Fantasy-Map-Generator/utils/graphUtils.js
Azgaar 374c21b3d7
Ocean heightmap and Scale bar styling change [v1.96] (#1045)
* Scale bar styling (#1025)

* feat: style scale bar

* feat: style scale bar - style presets

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>

* Ocean heightmap to v1.96 (#1044)

* feat: allow to render ocean heightmap

* feat: allow to render ocean heightmap - test

* feat: allow to render ocean heightmap - fix issue

* feat: allow to render ocean heightmap - cleanup

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>

* fix: scale bar size

* fix: remove mask on terrs lavel

* fix: regenerate heigtmap preview to use current graph size

* Add the name of culture and namesbase in the name editor dialog (#1033)

* Add the name of culture and namesbase in the name editor dialog

Added the name of the culture and  namesbase in the dialog "name editor".
This tells information on the "click to generate a culture-specific name"
It tells you the culture before changing name.

* cultureName into cultureId + cultureName

And deleted the incomplete code of showing culture name on datatip

* refactor: leave culture name only

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>

* Added Burgs column to province editor (#1031)

* Added Burgs column to province editor

Added to province editor:
+ Burgs column
+ the number of Burgs, p.burgs.length
+ "icon-dot-circled" to go to overviewBurgs.
+ overviewBurgs Filtered by state id.
+ Fixed some typos.

* fixed code as Azgaar suggested

+ Corrected provincesHeader distance in em.
+ const stateId = pack.provinces[p].state;
- Deleted cell count.

* deleted HTML code for provincesFooter cells

- Deleted Total land cells number HTML from provincesFooter.

* deleting totalCells in the code, maybe i will add provinceCells in the future.

Deleted lines for const totalCells and for (+cells / totalCells) * 100 + "%";

* refactor: cleanup

* refactor: cleanup

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>

* fix: burgs overview - add MFCG link back

* feat: add more details to burgs export

* feat: don't show auto-update dialog

* feat: pump version

* fix: #1041

* feat: update style presets

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
Co-authored-by: Ángel Montero Lamas <angel.montero1@gmail.com>
2024-02-24 19:12:48 +04:00

369 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
// FMG utils related to graph
// check if new grid graph should be generated or we can use the existing one
function shouldRegenerateGrid(grid, expectedSeed) {
if (expectedSeed && expectedSeed !== grid.seed) return true;
const cellsDesired = +byId("pointsInput").dataset.cells;
if (cellsDesired !== grid.cellsDesired) return true;
const newSpacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2);
const newCellsX = Math.floor((graphWidth + 0.5 * newSpacing - 1e-10) / newSpacing);
const newCellsY = Math.floor((graphHeight + 0.5 * newSpacing - 1e-10) / newSpacing);
return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY;
}
function generateGrid() {
Math.random = aleaPRNG(seed); // reset PRNG
const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints();
const {cells, vertices} = calculateVoronoi(points, boundary);
return {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, seed};
}
// place random points to calculate Voronoi diagram
function placePoints() {
TIME && console.time("placePoints");
const cellsDesired = +byId("pointsInput").dataset.cells;
const spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); // spacing between points before jirrering
const boundary = getBoundaryPoints(graphWidth, graphHeight, spacing);
const points = getJitteredGrid(graphWidth, graphHeight, spacing); // points of jittered square grid
const cellsX = Math.floor((graphWidth + 0.5 * spacing - 1e-10) / spacing);
const cellsY = Math.floor((graphHeight + 0.5 * spacing - 1e-10) / spacing);
TIME && console.timeEnd("placePoints");
return {spacing, cellsDesired, boundary, points, cellsX, cellsY};
}
// calculate Delaunay and then Voronoi diagram
function calculateVoronoi(points, boundary) {
TIME && console.time("calculateDelaunay");
const allPoints = points.concat(boundary);
const delaunay = Delaunator.from(allPoints);
TIME && console.timeEnd("calculateDelaunay");
TIME && console.time("calculateVoronoi");
const voronoi = new Voronoi(delaunay, allPoints, points.length);
const cells = voronoi.cells;
cells.i = createTypedArray({maxValue: points.length, length: points.length}).map((_, i) => i); // array of indexes
const vertices = voronoi.vertices;
TIME && console.timeEnd("calculateVoronoi");
return {cells, vertices};
}
// add points along map edge to pseudo-clip voronoi cells
function getBoundaryPoints(width, height, spacing) {
const offset = rn(-1 * spacing);
const bSpacing = spacing * 2;
const w = width - offset * 2;
const h = height - offset * 2;
const numberX = Math.ceil(w / bSpacing) - 1;
const numberY = Math.ceil(h / bSpacing) - 1;
const points = [];
for (let i = 0.5; i < numberX; i++) {
let x = Math.ceil((w * i) / numberX + offset);
points.push([x, offset], [x, h + offset]);
}
for (let i = 0.5; i < numberY; i++) {
let y = Math.ceil((h * i) / numberY + offset);
points.push([offset, y], [w + offset, y]);
}
return points;
}
// get points on a regular square grid and jitter them a bit
function getJitteredGrid(width, height, spacing) {
const radius = spacing / 2; // square radius
const jittering = radius * 0.9; // max deviation
const doubleJittering = jittering * 2;
const jitter = () => Math.random() * doubleJittering - jittering;
let points = [];
for (let y = radius; y < height; y += spacing) {
for (let x = radius; x < width; x += spacing) {
const xj = Math.min(rn(x + jitter(), 2), width);
const yj = Math.min(rn(y + jitter(), 2), height);
points.push([xj, yj]);
}
}
return points;
}
// return cell index on a regular square grid
function findGridCell(x, y, grid) {
return (
Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX +
Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1))
);
}
// return array of cell indexes in radius on a regular square grid
function findGridAll(x, y, radius) {
const c = grid.cells.c;
let r = Math.floor(radius / grid.spacing);
let found = [findGridCell(x, y, grid)];
if (!r || radius === 1) return found;
if (r > 0) found = found.concat(c[found[0]]);
if (r > 1) {
let frontier = c[found[0]];
while (r > 1) {
let cycle = frontier.slice();
frontier = [];
cycle.forEach(function (s) {
c[s].forEach(function (e) {
if (found.indexOf(e) !== -1) return;
found.push(e);
frontier.push(e);
});
});
r--;
}
}
return found;
}
// return closest pack points quadtree datum
function find(x, y, radius = Infinity) {
return pack.cells.q.find(x, y, radius);
}
// return closest cell index
function findCell(x, y, radius = Infinity) {
if (!pack.cells?.q) return;
const found = pack.cells.q.find(x, y, radius);
return found ? found[2] : undefined;
}
// return array of cell indexes in radius
function findAll(x, y, radius) {
const found = pack.cells.q.findAll(x, y, radius);
return found.map(r => r[2]);
}
// get polygon points for packed cells knowing cell id
function getPackPolygon(i) {
return pack.cells.v[i].map(v => pack.vertices.p[v]);
}
// get polygon points for initial cells knowing cell id
function getGridPolygon(i) {
return grid.cells.v[i].map(v => grid.vertices.p[v]);
}
// mbostock's poissonDiscSampler
function* poissonDiscSampler(x0, y0, x1, y1, r, k = 3) {
if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error();
const width = x1 - x0;
const height = y1 - y0;
const r2 = r * r;
const r2_3 = 3 * r2;
const cellSize = r * Math.SQRT1_2;
const gridWidth = Math.ceil(width / cellSize);
const gridHeight = Math.ceil(height / cellSize);
const grid = new Array(gridWidth * gridHeight);
const queue = [];
function far(x, y) {
const i = (x / cellSize) | 0;
const j = (y / cellSize) | 0;
const i0 = Math.max(i - 2, 0);
const j0 = Math.max(j - 2, 0);
const i1 = Math.min(i + 3, gridWidth);
const j1 = Math.min(j + 3, gridHeight);
for (let j = j0; j < j1; ++j) {
const o = j * gridWidth;
for (let i = i0; i < i1; ++i) {
const s = grid[o + i];
if (s) {
const dx = s[0] - x;
const dy = s[1] - y;
if (dx * dx + dy * dy < r2) return false;
}
}
}
return true;
}
function sample(x, y) {
queue.push((grid[gridWidth * ((y / cellSize) | 0) + ((x / cellSize) | 0)] = [x, y]));
return [x + x0, y + y0];
}
yield sample(width / 2, height / 2);
pick: while (queue.length) {
const i = (Math.random() * queue.length) | 0;
const parent = queue[i];
for (let j = 0; j < k; ++j) {
const a = 2 * Math.PI * Math.random();
const r = Math.sqrt(Math.random() * r2_3 + r2);
const x = parent[0] + r * Math.cos(a);
const y = parent[1] + r * Math.sin(a);
if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) {
yield sample(x, y);
continue pick;
}
}
const r = queue.pop();
if (i < queue.length) queue[i] = r;
}
}
// filter land cells
function isLand(i) {
return pack.cells.h[i] >= 20;
}
// filter water cells
function isWater(i) {
return pack.cells.h[i] < 20;
}
// findAll d3.quandtree search from https://bl.ocks.org/lwthatcher/b41479725e0ff2277c7ac90df2de2b5e
void (function addFindAll() {
const Quad = function (node, x0, y0, x1, y1) {
this.node = node;
this.x0 = x0;
this.y0 = y0;
this.x1 = x1;
this.y1 = y1;
};
const tree_filter = function (x, y, radius) {
var t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
if (t.node) {
t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
}
radiusSearchInit(t, radius);
var i = 0;
while ((t.q = t.quads.pop())) {
i++;
// Stop searching if this quadrant cant contain a closer node.
if (
!(t.node = t.q.node) ||
(t.x1 = t.q.x0) > t.x3 ||
(t.y1 = t.q.y0) > t.y3 ||
(t.x2 = t.q.x1) < t.x0 ||
(t.y2 = t.q.y1) < t.y0
)
continue;
// Bisect the current quadrant.
if (t.node.length) {
t.node.explored = true;
var xm = (t.x1 + t.x2) / 2,
ym = (t.y1 + t.y2) / 2;
t.quads.push(
new Quad(t.node[3], xm, ym, t.x2, t.y2),
new Quad(t.node[2], t.x1, ym, xm, t.y2),
new Quad(t.node[1], xm, t.y1, t.x2, ym),
new Quad(t.node[0], t.x1, t.y1, xm, ym)
);
// Visit the closest quadrant first.
if ((t.i = ((y >= ym) << 1) | (x >= xm))) {
t.q = t.quads[t.quads.length - 1];
t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i];
t.quads[t.quads.length - 1 - t.i] = t.q;
}
}
// Visit this point. (Visiting coincident points isnt necessary!)
else {
var dx = x - +this._x.call(null, t.node.data),
dy = y - +this._y.call(null, t.node.data),
d2 = dx * dx + dy * dy;
radiusSearchVisit(t, d2);
}
}
return t.result;
};
d3.quadtree.prototype.findAll = tree_filter;
var radiusSearchInit = function (t, radius) {
t.result = [];
(t.x0 = t.x - radius), (t.y0 = t.y - radius);
(t.x3 = t.x + radius), (t.y3 = t.y + radius);
t.radius = radius * radius;
};
var radiusSearchVisit = function (t, d2) {
t.node.data.scanned = true;
if (d2 < t.radius) {
do {
t.result.push(t.node.data);
t.node.data.selected = true;
} while ((t.node = t.node.next));
}
};
})();
// helper function non-used for the generation
function drawCellsValue(data) {
debug.selectAll("text").remove();
debug
.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", (d, i) => pack.cells.p[i][0])
.attr("y", (d, i) => pack.cells.p[i][1])
.text(d => d);
}
// helper function non-used for the main generation
function drawPolygons(data) {
const max = d3.max(data),
min = d3.min(data),
scheme = getColorScheme(terrs.select("#landHeights").attr("scheme"));
data = data.map(d => 1 - normalize(d, min, max));
debug.selectAll("polygon").remove();
debug
.selectAll("polygon")
.data(data)
.enter()
.append("polygon")
.attr("points", (d, i) => getGridPolygon(i))
.attr("fill", d => scheme(d))
.attr("stroke", d => scheme(d));
}
// draw raster heightmap preview (not used in main generation)
function drawHeights({heights, width, height, scheme, renderOcean}) {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(width, height);
const getHeight = height => (height < 20 ? (renderOcean ? height : 0) : height);
for (let i = 0; i < heights.length; i++) {
const color = scheme(1 - getHeight(heights[i]) / 100);
const {r, g, b} = d3.color(color);
const n = i * 4;
imageData.data[n] = r;
imageData.data[n + 1] = g;
imageData.data[n + 2] = b;
imageData.data[n + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
return canvas.toDataURL("image/png");
}