diff --git a/main.js b/main.js
index c0a96eb2..07183d9e 100644
--- a/main.js
+++ b/main.js
@@ -624,6 +624,7 @@ function generate() {
drawCoastline();
Rivers.generate();
+ drawRivers();
Lakes.defineGroup();
defineBiomes();
diff --git a/modules/load.js b/modules/load.js
index a8a33ac0..cb02d6e9 100644
--- a/modules/load.js
+++ b/modules/load.js
@@ -271,12 +271,13 @@ function parseLoadedData(data) {
}
})();
- const notHidden = selection => selection.node() && selection.style("display") !== "none";
- const hasChildren = selection => selection.node()?.hasChildNodes();
- const hasChild = (selection, selector) => selection.node()?.querySelector(selector);
- const turnOn = el => document.getElementById(el).classList.remove("buttonoff");
-
void (function restoreLayersState() {
+ // helper functions
+ const notHidden = selection => selection.node() && selection.style("display") !== "none";
+ const hasChildren = selection => selection.node()?.hasChildNodes();
+ const hasChild = (selection, selector) => selection.node()?.querySelector(selector);
+ const turnOn = el => document.getElementById(el).classList.remove("buttonoff");
+
// turn all layers off
document
.getElementById("mapLayers")
@@ -291,7 +292,7 @@ function parseLoadedData(data) {
if (hasChildren(gridOverlay)) turnOn("toggleGrid");
if (hasChildren(coordinates)) turnOn("toggleCoordinates");
if (notHidden(compass) && hasChild(compass, "use")) turnOn("toggleCompass");
- if (notHidden(rivers)) turnOn("toggleRivers");
+ if (hasChildren(rivers)) turnOn("toggleRivers");
if (notHidden(terrain) && hasChildren(terrain)) turnOn("toggleRelief");
if (hasChildren(relig)) turnOn("toggleReligions");
if (hasChildren(cults)) turnOn("toggleCultures");
diff --git a/modules/river-generator.js b/modules/river-generator.js
index a1b2bff1..3613820d 100644
--- a/modules/river-generator.js
+++ b/modules/river-generator.js
@@ -24,7 +24,6 @@
Lakes.prepareLakeData(h);
resolveDepressions(h);
drainWater();
- lineGen.curve(d3.curveCatmullRom.alpha(0.1));
defineRivers();
calculateConfluenceFlux();
Lakes.cleanupLakeData();
@@ -150,7 +149,6 @@
cells.r = new Uint16Array(cells.i.length);
cells.conf = new Uint16Array(cells.i.length);
pack.rivers = [];
- const riverPaths = [];
for (const key in riversData) {
const riverCells = riversData[key];
@@ -170,20 +168,13 @@
const parent = riverParents[key] || 0;
const widthFactor = !parent || parent === riverId ? 1.2 : 1;
- const riverMeandered = addMeandering(riverCells);
- const [path, length, offset] = getRiverPath(riverMeandered, widthFactor);
- riverPaths.push([path, riverId]);
-
- // Real mouth width examples: Amazon 6000m, Volga 6000m, Dniepr 3000m, Mississippi 1300m, Themes 900m,
- // Danube 800m, Daugava 600m, Neva 500m, Nile 450m, Don 400m, Wisla 300m, Pripyat 150m, Bug 140m, Muchavets 40m
- const width = rn((offset / 1.5) ** 1.8, 2); // mouth width in km
+ const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
+ const length = getApproximateLength(meanderedPoints);
+ const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor));
pack.rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells});
}
-
- // draw rivers
- rivers.html(riverPaths.map(d => ``).join(""));
}
function calculateConfluenceFlux() {
@@ -346,18 +337,21 @@
return [graphWidth, y];
};
- const fluxFactor = 500;
- const maxFluxWidth = 2;
- const lengthFactor = 200;
- const stepWidth = 1 / lengthFactor;
- const lengthProgression = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / lengthFactor);
- const maxProgression = last(lengthProgression);
+ const FLUX_FACTOR = 500;
+ const MAX_FLUX_WIDTH = 2;
+ const LENGTH_FACTOR = 200;
+ const STEP_WIDTH = 1 / LENGTH_FACTOR;
+ const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
+ const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
+
+ const getOffset = (flux, pointNumber, widthFactor = 1, startingWidth = 0) => {
+ const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH);
+ const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
+ return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
+ };
// build polygon from a list of points and calculated offset (width)
const getRiverPath = function (points, widthFactor = 1, startingWidth = 0) {
- const riverLength = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
- let width = 0;
-
const riverPointsLeft = [];
const riverPointsRight = [];
@@ -366,13 +360,10 @@
const [x1, y1, flux] = points[p];
const [x2, y2] = points[p + 1] || points[p];
- const fluxWidth = Math.min(flux ** 0.9 / fluxFactor, maxFluxWidth);
- const lengthWidth = p * stepWidth + (lengthProgression[p] || maxProgression);
- width = widthFactor * (lengthWidth + fluxWidth) + startingWidth;
-
+ const offset = getOffset(flux, p, widthFactor, startingWidth);
const angle = Math.atan2(y0 - y2, x0 - x2);
- const sinOffset = Math.sin(angle) * width;
- const cosOffset = Math.cos(angle) * width;
+ const sinOffset = Math.sin(angle) * offset;
+ const cosOffset = Math.cos(angle) * offset;
riverPointsLeft.push([x1 - sinOffset, y1 + cosOffset]);
riverPointsRight.push([x1 + sinOffset, y1 - cosOffset]);
@@ -382,7 +373,7 @@
let left = lineGen(riverPointsLeft);
left = left.substring(left.indexOf("C"));
- return [round(right + left, 2), rn(riverLength, 2), width];
+ return round(right + left, 1);
};
const specify = function () {
@@ -424,6 +415,12 @@
return rw(riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"]);
};
+ const getApproximateLength = points => points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
+
+ // Real mouth width examples: Amazon 6000m, Volga 6000m, Dniepr 3000m, Mississippi 1300m, Themes 900m,
+ // Danube 800m, Daugava 600m, Neva 500m, Nile 450m, Don 400m, Wisla 300m, Pripyat 150m, Bug 140m, Muchavets 40m
+ const getWidth = offset => rn((offset / 1.5) ** 1.8, 2); // mouth width in km
+
// remove river and all its tributaries
const remove = function (id) {
const cells = pack.cells;
@@ -444,5 +441,5 @@
return getBasin(parent);
};
- return {generate, alterHeights, resolveDepressions, addMeandering, getRiverPath, specify, getName, getType, getBasin, remove};
+ return {generate, alterHeights, resolveDepressions, addMeandering, getRiverPath, specify, getName, getType, getBasin, getWidth, getOffset, getApproximateLength, remove};
});
diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js
index 4cac39f3..f19e8cf0 100644
--- a/modules/ui/heightmap-editor.js
+++ b/modules/ui/heightmap-editor.js
@@ -197,6 +197,7 @@ function editHeightmap() {
}
}
+ drawRivers();
Lakes.defineGroup();
defineBiomes();
rankCells();
diff --git a/modules/ui/layers.js b/modules/ui/layers.js
index 2063721d..46beb692 100644
--- a/modules/ui/layers.js
+++ b/modules/ui/layers.js
@@ -875,7 +875,6 @@ function toggleStates(event) {
}
}
-// draw states
function drawStates() {
TIME && console.time("drawStates");
regions.selectAll("path").remove();
@@ -1015,6 +1014,21 @@ function drawStates() {
TIME && console.timeEnd("drawStates");
}
+function toggleBorders(event) {
+ if (!layerIsOn("toggleBorders")) {
+ turnButtonOn("toggleBorders");
+ drawBorders();
+ if (event && isCtrlClick(event)) editStyle("borders");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("borders");
+ return;
+ }
+ turnButtonOff("toggleBorders");
+ borders.selectAll("path").remove();
+ }
+}
+
// draw state and province borders
function drawBorders() {
TIME && console.time("drawBorders");
@@ -1118,21 +1132,6 @@ function drawBorders() {
TIME && console.timeEnd("drawBorders");
}
-function toggleBorders(event) {
- if (!layerIsOn("toggleBorders")) {
- turnButtonOn("toggleBorders");
- $("#borders").fadeIn();
- if (event && isCtrlClick(event)) editStyle("borders");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("borders");
- return;
- }
- turnButtonOff("toggleBorders");
- $("#borders").fadeOut();
- }
-}
-
function toggleProvinces(event) {
if (!layerIsOn("toggleProvinces")) {
turnButtonOn("toggleProvinces");
@@ -1454,14 +1453,18 @@ function toggleRivers(event) {
}
function drawRivers() {
+ TIME && console.time("drawRivers");
+ const {addMeandering, getRiverPath} = Rivers;
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const riverPaths = pack.rivers.map(river => {
- const riverMeandered = Rivers.addMeandering(river.cells, 0.5, river.points);
+ const riverMeandered = addMeandering(river.cells, 0.5, river.points);
const widthFactor = river.widthFactor || 1;
const startingWidth = river.startingWidth || 0;
- const [path] = Rivers.getRiverPath(riverMeandered, widthFactor, startingWidth);
- return [path, river.i];
+ const path = getRiverPath(riverMeandered, widthFactor, startingWidth);
+ return ``;
});
- rivers.html(riverPaths.map(d => ``).join(""));
+ rivers.html(riverPaths.join(""));
+ TIME && console.timeEnd("drawRivers");
}
function toggleRoutes(event) {
diff --git a/modules/ui/tools.js b/modules/ui/tools.js
index 4ac393c6..01de28f4 100644
--- a/modules/ui/tools.js
+++ b/modules/ui/tools.js
@@ -538,7 +538,7 @@ function addRiverOnClick() {
if (cells.h[i] < 20) return tip("Cannot create river in water cell", false, "error");
if (cells.b[i]) return;
- const {alterHeights, resolveDepressions, addMeandering, getRiverPath, getBasin, getName, getType} = Rivers;
+ const {alterHeights, resolveDepressions, addMeandering, getRiverPath, getBasin, getName, getType, getWidth, getOffset, getApproximateLength} = Rivers;
const riverCells = [];
let riverId = last(rivers).id + 1;
let parent = 0;
@@ -614,22 +614,15 @@ function addRiverOnClick() {
}
const river = rivers.find(r => r.i === riverId);
- const widthFactor = river?.widthFactor || (!parent || parent === riverId ? 1.2 : 1);
- const riverMeandered = addMeandering(riverCells);
- lineGen.curve(d3.curveCatmullRom.alpha(0.1));
- const [path, length, offset] = getRiverPath(riverMeandered, widthFactor);
- viewbox
- .select("#rivers")
- .append("path")
- .attr("d", path)
- .attr("id", "river" + riverId);
-
- // add new river to data or change extended river attributes
const source = riverCells[0];
const mouth = riverCells[riverCells.length - 2];
+ const widthFactor = river?.widthFactor || (!parent || parent === riverId ? 1.2 : 1);
+ const riverMeandered = addMeandering(riverCells);
+
const discharge = cells.fl[mouth]; // m3 in second
- const width = rn((offset / 1.4) ** 2, 2); // mounth width in km
+ const length = getApproximateLength(riverMeandered);
+ const width = getWidth(getOffset(discharge, riverMeandered.length, widthFactor));
if (river) {
river.source = source;
@@ -645,6 +638,13 @@ function addRiverOnClick() {
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells, basin, name, type});
}
+ // render river
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ const path = getRiverPath(riverMeandered, widthFactor);
+ const id = "river" + riverId;
+ const riversG = viewbox.select("#rivers");
+ riversG.append("path").attr("d", path).attr("id", id);
+
if (d3.event.shiftKey === false) {
Lakes.cleanupLakeData();
unpressClickToAddButton();