mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
Refactor layers rendering (#1120)
* feat: render states - use global fn * feat: render states - separate pole detection from layer render * feat: render provinces * chore: unify drawFillWithGap * refactor: drawIce * refactor: drawBorders * refactor: drawHeightmap * refactor: drawTemperature * refactor: drawBiomes * refactor: drawPrec * refactor: drawPrecipitation * refactor: drawPopulation * refactor: drawCells * refactor: geColor * refactor: drawMarkers * refactor: drawScaleBar * refactor: drawScaleBar * refactor: drawMilitary * refactor: pump version to 1.104.00 * refactor: pump version to 1.104.00 * refactor: drawCoastline and createDefaultRuler * refactor: drawCoastline * refactor: Features module start * refactor: features - define distance fields * feat: drawFeatures * feat: drawIce don't hide * feat: detect coastline - fix issue with border feature * feat: separate labels rendering from generation process * feat: auto-update and restore layers * refactor - change layers * refactor - sort layers * fix: regenerate burgs to re-render layers * fix: getColor is not defined * fix: burgs overview - don't auto-show labels on hover * fix: redraw population on change * refactor: improve tooltip logic for burg labels and icons * chore: pump version to 1.104.0 * fefactor: edit coastline and lake * fix: minot fixes * fix: submap --------- Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
This commit is contained in:
parent
ec993d1a9b
commit
05de284e02
52 changed files with 2473 additions and 2713 deletions
328
main.js
328
main.js
|
|
@ -14,6 +14,7 @@ const ERROR = true;
|
|||
const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile;
|
||||
|
||||
// typed arrays max values
|
||||
const INT8_MAX = 127;
|
||||
const UINT8_MAX = 255;
|
||||
const UINT16_MAX = 65535;
|
||||
const UINT32_MAX = 4294967295;
|
||||
|
|
@ -72,7 +73,7 @@ let trails = routes.append("g").attr("id", "trails");
|
|||
let searoutes = routes.append("g").attr("id", "searoutes");
|
||||
let temperature = viewbox.append("g").attr("id", "temperature");
|
||||
let coastline = viewbox.append("g").attr("id", "coastline");
|
||||
let ice = viewbox.append("g").attr("id", "ice").style("display", "none");
|
||||
let ice = viewbox.append("g").attr("id", "ice");
|
||||
let prec = viewbox.append("g").attr("id", "prec").style("display", "none");
|
||||
let population = viewbox.append("g").attr("id", "population");
|
||||
let emblems = viewbox.append("g").attr("id", "emblems").style("display", "none");
|
||||
|
|
@ -80,7 +81,7 @@ let labels = viewbox.append("g").attr("id", "labels");
|
|||
let icons = viewbox.append("g").attr("id", "icons");
|
||||
let burgIcons = icons.append("g").attr("id", "burgIcons");
|
||||
let anchors = icons.append("g").attr("id", "anchors");
|
||||
let armies = viewbox.append("g").attr("id", "armies").style("display", "none");
|
||||
let armies = viewbox.append("g").attr("id", "armies");
|
||||
let markers = viewbox.append("g").attr("id", "markers");
|
||||
let fogging = viewbox
|
||||
.append("g")
|
||||
|
|
@ -313,9 +314,10 @@ async function checkLoadParameters() {
|
|||
async function generateMapOnLoad() {
|
||||
await applyStyleOnLoad(); // apply previously selected default or custom style
|
||||
await generate(); // generate map
|
||||
applyPreset(); // apply saved layers preset
|
||||
applyLayersPreset(); // apply saved layers preset and reder layers
|
||||
drawLayers();
|
||||
fitMapToScreen();
|
||||
focusOn(); // based on searchParams focus on point, cell or burg from MFCG
|
||||
focusOn(); // focus on point, cell or burg from MFCG based on url searchParams
|
||||
}
|
||||
|
||||
// focus on coordinates, cell or burg provided in searchParams
|
||||
|
|
@ -432,7 +434,9 @@ function findBurgForMFCG(params) {
|
|||
function handleZoom(isScaleChanged, isPositionChanged) {
|
||||
viewbox.attr("transform", `translate(${viewX} ${viewY}) scale(${scale})`);
|
||||
|
||||
if (isPositionChanged) drawCoordinates();
|
||||
if (isPositionChanged) {
|
||||
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
||||
}
|
||||
|
||||
if (isScaleChanged) {
|
||||
invokeActiveZooming();
|
||||
|
|
@ -544,21 +548,6 @@ function invokeActiveZooming() {
|
|||
}
|
||||
}
|
||||
|
||||
async function renderGroupCOAs(g) {
|
||||
const [group, type] =
|
||||
g.id === "burgEmblems"
|
||||
? [pack.burgs, "burg"]
|
||||
: g.id === "provinceEmblems"
|
||||
? [pack.provinces, "province"]
|
||||
: [pack.states, "state"];
|
||||
for (let use of g.children) {
|
||||
const i = +use.dataset.i;
|
||||
const id = type + "COA" + i;
|
||||
COArenderer.trigger(id, group[i].coa);
|
||||
use.setAttribute("href", "#" + id);
|
||||
}
|
||||
}
|
||||
|
||||
// add drag to upload logic, pull request from @evyatron
|
||||
void (function addDragToUpload() {
|
||||
document.addEventListener("dragover", function (e) {
|
||||
|
|
@ -624,8 +613,7 @@ async function generate(options) {
|
|||
grid.cells.h = await HeightmapGenerator.generate(grid);
|
||||
pack = {}; // reset pack
|
||||
|
||||
markFeatures();
|
||||
markupGridOcean();
|
||||
Features.markupGrid();
|
||||
addLakesInDeepDepressions();
|
||||
openNearSeaLakes();
|
||||
|
||||
|
|
@ -636,11 +624,10 @@ async function generate(options) {
|
|||
generatePrecipitation();
|
||||
|
||||
reGraph();
|
||||
drawCoastline();
|
||||
Features.markupPack();
|
||||
createDefaultRuler();
|
||||
|
||||
Rivers.generate();
|
||||
drawRivers();
|
||||
Lakes.defineGroup();
|
||||
Biomes.define();
|
||||
|
||||
rankCells();
|
||||
|
|
@ -650,15 +637,12 @@ async function generate(options) {
|
|||
Routes.generate();
|
||||
Religions.generate();
|
||||
BurgsAndStates.defineStateForms();
|
||||
BurgsAndStates.generateProvinces();
|
||||
Provinces.generate();
|
||||
Provinces.getPoles();
|
||||
BurgsAndStates.defineBurgFeatures();
|
||||
|
||||
drawStates();
|
||||
drawBorders();
|
||||
drawStateLabels();
|
||||
|
||||
Rivers.specify();
|
||||
Lakes.generateName();
|
||||
Features.specify();
|
||||
|
||||
Military.generate();
|
||||
Markers.generate();
|
||||
|
|
@ -716,69 +700,6 @@ function setSeed(precreatedSeed) {
|
|||
Math.random = aleaPRNG(seed);
|
||||
}
|
||||
|
||||
// Mark features (ocean, lakes, islands) and calculate distance field
|
||||
function markFeatures() {
|
||||
TIME && console.time("markFeatures");
|
||||
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
|
||||
|
||||
const cells = grid.cells;
|
||||
const heights = grid.cells.h;
|
||||
cells.f = new Uint16Array(cells.i.length); // cell feature number
|
||||
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land coast; -1 = water near coast
|
||||
grid.features = [0];
|
||||
|
||||
for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
|
||||
cells.f[queue[0]] = i; // feature number
|
||||
const land = heights[queue[0]] >= 20;
|
||||
let border = false; // true if feature touches map border
|
||||
|
||||
while (queue.length) {
|
||||
const q = queue.pop();
|
||||
if (cells.b[q]) border = true;
|
||||
|
||||
cells.c[q].forEach(c => {
|
||||
const cLand = heights[c] >= 20;
|
||||
if (land === cLand && !cells.f[c]) {
|
||||
cells.f[c] = i;
|
||||
queue.push(c);
|
||||
} else if (land && !cLand) {
|
||||
cells.t[q] = 1;
|
||||
cells.t[c] = -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
const type = land ? "island" : border ? "ocean" : "lake";
|
||||
grid.features.push({i, land, border, type});
|
||||
|
||||
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("markFeatures");
|
||||
}
|
||||
|
||||
function markupGridOcean() {
|
||||
TIME && console.time("markupGridOcean");
|
||||
markup(grid.cells, -2, -1, -10);
|
||||
TIME && console.timeEnd("markupGridOcean");
|
||||
}
|
||||
|
||||
// Calculate cell-distance to coast for every cell
|
||||
function markup(cells, start, increment, limit) {
|
||||
for (let t = start, count = Infinity; count > 0 && t > limit; t += increment) {
|
||||
count = 0;
|
||||
const prevT = t - increment;
|
||||
for (let i = 0; i < cells.i.length; i++) {
|
||||
if (cells.t[i] !== prevT) continue;
|
||||
|
||||
for (const c of cells.c[i]) {
|
||||
if (cells.t[c]) continue;
|
||||
cells.t[c] = t;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addLakesInDeepDepressions() {
|
||||
TIME && console.time("addLakesInDeepDepressions");
|
||||
const {cells, features} = grid;
|
||||
|
|
@ -1220,222 +1141,6 @@ function reGraph() {
|
|||
TIME && console.timeEnd("reGraph");
|
||||
}
|
||||
|
||||
// Detect and draw the coastline
|
||||
function drawCoastline() {
|
||||
TIME && console.time("drawCoastline");
|
||||
reMarkFeatures();
|
||||
|
||||
const cells = pack.cells,
|
||||
vertices = pack.vertices,
|
||||
n = cells.i.length,
|
||||
features = pack.features;
|
||||
const used = new Uint8Array(features.length); // store connected features
|
||||
const largestLand = d3.scan(
|
||||
features.map(f => (f.land ? f.cells : 0)),
|
||||
(a, b) => b - a
|
||||
);
|
||||
const landMask = defs.select("#land");
|
||||
const waterMask = defs.select("#water");
|
||||
lineGen.curve(d3.curveBasisClosed);
|
||||
|
||||
for (const i of cells.i) {
|
||||
const startFromEdge = !i && cells.h[i] >= 20;
|
||||
if (!startFromEdge && cells.t[i] !== -1 && cells.t[i] !== 1) continue; // non-edge cell
|
||||
const f = cells.f[i];
|
||||
if (used[f]) continue; // already connected
|
||||
if (features[f].type === "ocean") continue; // ocean cell
|
||||
|
||||
const type = features[f].type === "lake" ? 1 : -1; // type value to search for
|
||||
const start = findStart(i, type);
|
||||
if (start === -1) continue; // cannot start here
|
||||
let vchain = connectVertices(start, type);
|
||||
if (features[f].type === "lake") relax(vchain, 1.2);
|
||||
used[f] = 1;
|
||||
let points = clipPoly(
|
||||
vchain.map(v => vertices.p[v]),
|
||||
1
|
||||
);
|
||||
const area = d3.polygonArea(points); // area with lakes/islands
|
||||
if (area > 0 && features[f].type === "lake") {
|
||||
points = points.reverse();
|
||||
vchain = vchain.reverse();
|
||||
}
|
||||
|
||||
features[f].area = Math.abs(area);
|
||||
features[f].vertices = vchain;
|
||||
|
||||
const path = round(lineGen(points));
|
||||
|
||||
if (features[f].type === "lake") {
|
||||
landMask
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", "black")
|
||||
.attr("id", "land_" + f);
|
||||
// waterMask.append("path").attr("d", path).attr("fill", "white").attr("id", "water_"+id); // uncomment to show over lakes
|
||||
lakes
|
||||
.select("#freshwater")
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("id", "lake_" + f)
|
||||
.attr("data-f", f); // draw the lake
|
||||
} else {
|
||||
landMask
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", "white")
|
||||
.attr("id", "land_" + f);
|
||||
waterMask
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", "black")
|
||||
.attr("id", "water_" + f);
|
||||
const g = features[f].group === "lake_island" ? "lake_island" : "sea_island";
|
||||
coastline
|
||||
.select("#" + g)
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("id", "island_" + f)
|
||||
.attr("data-f", f); // draw the coastline
|
||||
}
|
||||
|
||||
// draw ruler to cover the biggest land piece
|
||||
if (f === largestLand) {
|
||||
const from = points[d3.scan(points, (a, b) => a[0] - b[0])];
|
||||
const to = points[d3.scan(points, (a, b) => b[0] - a[0])];
|
||||
rulers.create(Ruler, [from, to]);
|
||||
}
|
||||
}
|
||||
|
||||
// find cell vertex to start path detection
|
||||
function findStart(i, t) {
|
||||
if (t === -1 && cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell
|
||||
const filtered = cells.c[i].filter(c => cells.t[c] === t);
|
||||
const index = cells.c[i].indexOf(d3.min(filtered));
|
||||
return index === -1 ? index : cells.v[i][index];
|
||||
}
|
||||
|
||||
// connect vertices to chain
|
||||
function connectVertices(start, t) {
|
||||
const chain = []; // vertices chain to form a path
|
||||
for (let i = 0, current = start; i === 0 || (current !== start && i < 50000); i++) {
|
||||
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||
chain.push(current); // add current vertex to sequence
|
||||
const c = vertices.c[current]; // cells adjacent to vertex
|
||||
const v = vertices.v[current]; // neighboring vertices
|
||||
const c0 = c[0] >= n || cells.t[c[0]] === t;
|
||||
const c1 = c[1] >= n || cells.t[c[1]] === t;
|
||||
const c2 = c[2] >= n || cells.t[c[2]] === t;
|
||||
if (v[0] !== prev && c0 !== c1) current = v[0];
|
||||
else if (v[1] !== prev && c1 !== c2) current = v[1];
|
||||
else if (v[2] !== prev && c0 !== c2) current = v[2];
|
||||
if (current === chain[chain.length - 1]) {
|
||||
ERROR && console.error("Next vertex is not found");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
// move vertices that are too close to already added ones
|
||||
function relax(vchain, r) {
|
||||
const p = vertices.p,
|
||||
tree = d3.quadtree();
|
||||
|
||||
for (let i = 0; i < vchain.length; i++) {
|
||||
const v = vchain[i];
|
||||
let [x, y] = [p[v][0], p[v][1]];
|
||||
if (i && vchain[i + 1] && tree.find(x, y, r) !== undefined) {
|
||||
const v1 = vchain[i - 1],
|
||||
v2 = vchain[i + 1];
|
||||
const [x1, y1] = [p[v1][0], p[v1][1]];
|
||||
const [x2, y2] = [p[v2][0], p[v2][1]];
|
||||
[x, y] = [(x1 + x2) / 2, (y1 + y2) / 2];
|
||||
p[v] = [x, y];
|
||||
}
|
||||
tree.add([x, y]);
|
||||
}
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("drawCoastline");
|
||||
}
|
||||
|
||||
// Re-mark features (ocean, lakes, islands)
|
||||
function reMarkFeatures() {
|
||||
TIME && console.time("reMarkFeatures");
|
||||
const cells = pack.cells;
|
||||
const features = (pack.features = [0]);
|
||||
|
||||
cells.f = new Uint16Array(cells.i.length); // cell feature number
|
||||
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land along coast; -1 = water along coast;
|
||||
cells.haven = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length); // cell haven (opposite water cell);
|
||||
cells.harbor = new Uint8Array(cells.i.length); // cell harbor (number of adjacent water cells);
|
||||
|
||||
if (!cells.i.length) return; // no cells -> there is nothing to do
|
||||
for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
|
||||
const start = queue[0]; // first cell
|
||||
cells.f[start] = i; // assign feature number
|
||||
const land = cells.h[start] >= 20;
|
||||
let border = false; // true if feature touches map border
|
||||
let cellNumber = 1; // to count cells number in a feature
|
||||
|
||||
while (queue.length) {
|
||||
const q = queue.pop();
|
||||
if (cells.b[q]) border = true;
|
||||
cells.c[q].forEach(function (e) {
|
||||
const eLand = cells.h[e] >= 20;
|
||||
if (land && !eLand) {
|
||||
cells.t[q] = 1;
|
||||
cells.t[e] = -1;
|
||||
if (!cells.haven[q]) defineHaven(q);
|
||||
} else if (land && eLand) {
|
||||
if (!cells.t[e] && cells.t[q] === 1) cells.t[e] = 2;
|
||||
else if (!cells.t[q] && cells.t[e] === 1) cells.t[q] = 2;
|
||||
}
|
||||
if (!cells.f[e] && land === eLand) {
|
||||
queue.push(e);
|
||||
cells.f[e] = i;
|
||||
cellNumber++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const type = land ? "island" : border ? "ocean" : "lake";
|
||||
let group;
|
||||
if (type === "ocean") group = defineOceanGroup(cellNumber);
|
||||
else if (type === "island") group = defineIslandGroup(start, cellNumber);
|
||||
features.push({i, land, border, type, cells: cellNumber, firstCell: start, group});
|
||||
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
|
||||
}
|
||||
|
||||
markup(pack.cells, 3, 1, 0); // markupPackLand
|
||||
markup(pack.cells, -2, -1, -10); // markupPackWater
|
||||
|
||||
function defineHaven(i) {
|
||||
const water = cells.c[i].filter(c => cells.h[c] < 20);
|
||||
const dist2 = water.map(c => (cells.p[i][0] - cells.p[c][0]) ** 2 + (cells.p[i][1] - cells.p[c][1]) ** 2);
|
||||
const closest = water[dist2.indexOf(Math.min.apply(Math, dist2))];
|
||||
|
||||
cells.haven[i] = closest;
|
||||
cells.harbor[i] = water.length;
|
||||
}
|
||||
|
||||
function defineOceanGroup(number) {
|
||||
if (number > grid.cells.i.length / 25) return "ocean";
|
||||
if (number > grid.cells.i.length / 100) return "sea";
|
||||
return "gulf";
|
||||
}
|
||||
|
||||
function defineIslandGroup(cell, number) {
|
||||
if (cell && features[cells.f[cell - 1]].type === "lake") return "lake_island";
|
||||
if (number > grid.cells.i.length / 10) return "continent";
|
||||
if (number > grid.cells.i.length / 1000) return "island";
|
||||
return "isle";
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("reMarkFeatures");
|
||||
}
|
||||
|
||||
function isWetLand(moisture, temperature, height) {
|
||||
if (moisture > 40 && temperature > -2 && height < 25) return true; //near coast
|
||||
if (moisture > 24 && temperature > -2 && height > 24 && height < 60) return true; //off coast
|
||||
|
|
@ -1522,7 +1227,7 @@ const regenerateMap = debounce(async function (options) {
|
|||
resetZoom(1000);
|
||||
undraw();
|
||||
await generate(options);
|
||||
restoreLayers();
|
||||
drawLayers();
|
||||
if (ThreeD.options.isOn) ThreeD.redraw();
|
||||
if ($("#worldConfigurator").is(":visible")) editWorld();
|
||||
|
||||
|
|
@ -1541,6 +1246,5 @@ function undraw() {
|
|||
.forEach(el => el.remove());
|
||||
byId("coas").innerHTML = ""; // remove auto-generated emblems
|
||||
notes = [];
|
||||
rulers = new Rulers();
|
||||
unfog();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue