Merge pull request #648 from Azgaar/river-rendering

River rendering
This commit is contained in:
Azgaar 2021-07-28 22:40:52 +03:00 committed by GitHub
commit b098865bdf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1042 additions and 542 deletions

2
.gitignore vendored
View file

@ -1,2 +1,2 @@
run_php_server.bat .bat
.vscode .vscode

View file

@ -1,6 +1,6 @@
MIT License MIT License
Copyright 2018-2020 Max Ganiev (Azgaar), azgaar.fmg@yandex.by Copyright 2017-2021 Max Haniyeu (Azgaar), azgaar.fmg@yandex.com
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -202,6 +202,7 @@ a {
stroke: none; stroke: none;
mask: url(#land); mask: url(#land);
cursor: pointer; cursor: pointer;
fill-rule: nonzero;
} }
#anchors { #anchors {
@ -984,6 +985,18 @@ body button.noicon {
cursor: pointer; cursor: pointer;
} }
#controlCells > .current {
fill: #82c8ff40;
stroke: #82c8ff;
stroke-width: 0.4;
}
#controlCells > .occupied {
fill: #ff828240;
stroke: #ff8282;
stroke-width: 0.4;
}
#vertices > circle { #vertices > circle {
fill: #ff0000; fill: #ff0000;
stroke: #841f1f; stroke: #841f1f;
@ -1121,6 +1134,12 @@ div#regimentSelectorBody > div > div {
fill: none; fill: none;
} }
#debug > text {
font-size: 2px;
text-anchor: middle;
dominant-baseline: central;
}
.selectedCell { .selectedCell {
stroke-width: 1; stroke-width: 1;
stroke: #da3126; stroke: #da3126;
@ -1655,6 +1674,12 @@ rect.fillRect {
text-align: center; text-align: center;
} }
div.editorLine {
margin: 0.2em 0;
padding: 0 0.2em;
font-size: 0.9em;
}
#emblemDownloadControl > input { #emblemDownloadControl > input {
width: 4.1em; width: 4.1em;
} }

View file

@ -234,7 +234,7 @@
<div id="loading"> <div id="loading">
<div id="titleName"><t data-t="titleName">Azgaar's</t></div> <div id="titleName"><t data-t="titleName">Azgaar's</t></div>
<div id="title"><t data-t="title">Fantasy Map Generator</t></div> <div id="title"><t data-t="title">Fantasy Map Generator</t></div>
<div id="version"><t data-t="version">v. </t>1.63</div> <div id="version"><t data-t="version">v. </t>1.65</div>
<p id="loading-text"><t data-t="loading">LOADING</t><span>.</span><span>.</span><span>.</span></p> <p id="loading-text"><t data-t="loading">LOADING</t><span>.</span><span>.</span><span>.</span></p>
</div> </div>
@ -1644,19 +1644,19 @@
<input id="riverWidth" disabled/> <input id="riverWidth" disabled/>
</div> </div>
<div data-tip="River source width in pixels"> <div data-tip="River additional width. Default value is 0">
<div class="label">Source width:</div> <div class="label">Source width:</div>
<input id="riverSourceWidth" type="number" min=0 max=3 step=.1 /> <input id="riverSourceWidth" type="number" min=0 max=3 step=.1 />
</div> </div>
<div data-tip="River width multiplier"> <div data-tip="River width multiplier. Default value is 1">
<div class="label">Width modifier:</div> <div class="label">Width modifier:</div>
<input id="riverWidthFactor" type="number" min=.1 max=4 step=.1 /> <input id="riverWidthFactor" type="number" min=.1 max=4 step=.1 />
</div> </div>
</div> </div>
<div id="riverBottom"> <div id="riverBottom">
<button id="riverNew" data-tip="Create new river clicking on map" class="icon-map-pin"></button> <button id="riverCreateSelectingCells" data-tip="Create new river selecting river cells" class="icon-map-pin"></button>
<button id="riverEditStyle" data-tip="Edit style for all rivers in Style Editor" class="icon-brush"></button> <button id="riverEditStyle" data-tip="Edit style for all rivers in Style Editor" class="icon-brush"></button>
<button id="riverElevationProfile" data-tip="Show the elevation profile for the river" class="icon-chart-area"></button> <button id="riverElevationProfile" data-tip="Show the elevation profile for the river" class="icon-chart-area"></button>
<button id="riverLegend" data-tip="Edit free text notes (legend) for the river" class="icon-edit"></button> <button id="riverLegend" data-tip="Edit free text notes (legend) for the river" class="icon-edit"></button>
@ -1664,6 +1664,14 @@
</div> </div>
</div> </div>
<div id="riverCreator" class="dialog" style="display: none">
<div id="riverCreatorBody"></div>
<div id="riverCreatorBottom">
<button id="riverCreatorComplete" data-tip="Complete river creation" class="icon-check"></button>
<button id="riverCreatorCancel" data-tip="Cancel the creation" class="icon-cancel"></button>
</div>
</div>
<div id="lakeEditor" class="dialog" style="display: none"> <div id="lakeEditor" class="dialog" style="display: none">
<div id="lakeBody" style="padding-bottom: .3em"> <div id="lakeBody" style="padding-bottom: .3em">
<div> <div>
@ -3224,7 +3232,8 @@
<div id="riversBottom"> <div id="riversBottom">
<button id="riversOverviewRefresh" data-tip="Refresh the Editor" class="icon-cw"></button> <button id="riversOverviewRefresh" data-tip="Refresh the Editor" class="icon-cw"></button>
<button id="addNewRiver" data-tip="Add a new river. Hold Shift to add multiple" class="icon-plus"></button> <button id="addNewRiver" data-tip="Automatically add river starting from clicked cell. Hold Shift to add multiple" class="icon-plus"></button>
<button id="riverCreateNew" data-tip="Create new river selecting river cells" class="icon-map-pin"></button>
<button id="riversBasinHighlight" data-tip="Toggle basin highlight mode" class="icon-sitemap"></button> <button id="riversBasinHighlight" data-tip="Toggle basin highlight mode" class="icon-sitemap"></button>
<button id="riversExport" data-tip="Save rivers-related data as a text file (.csv)" class="icon-download"></button> <button id="riversExport" data-tip="Save rivers-related data as a text file (.csv)" class="icon-download"></button>
<button id="riversRemoveAll" data-tip="Remove all rivers" class="icon-trash"></button> <button id="riversRemoveAll" data-tip="Remove all rivers" class="icon-trash"></button>
@ -4219,6 +4228,7 @@
<script defer src="modules/ui/coastline-editor.js"></script> <script defer src="modules/ui/coastline-editor.js"></script>
<script defer src="modules/ui/labels-editor.js"></script> <script defer src="modules/ui/labels-editor.js"></script>
<script defer src="modules/ui/rivers-editor.js"></script> <script defer src="modules/ui/rivers-editor.js"></script>
<script defer src="modules/ui/rivers-creator.js"></script>
<script defer src="modules/ui/relief-editor.js"></script> <script defer src="modules/ui/relief-editor.js"></script>
<script defer src="modules/ui/religions-editor.js"></script> <script defer src="modules/ui/religions-editor.js"></script>
<script defer src="modules/ui/markers-editor.js"></script> <script defer src="modules/ui/markers-editor.js"></script>

20
main.js
View file

@ -2,7 +2,7 @@
// https://github.com/Azgaar/Fantasy-Map-Generator // https://github.com/Azgaar/Fantasy-Map-Generator
"use strict"; "use strict";
const version = "1.64"; // generator version const version = "1.65"; // generator version1
document.title += " v" + version; document.title += " v" + version;
// Switches to disable/enable logging features // Switches to disable/enable logging features
@ -389,7 +389,6 @@ function applyDefaultBiomesSystem() {
} }
function showWelcomeMessage() { function showWelcomeMessage() {
const post = link("https://www.patreon.com/posts/48228540", "Main changes:");
const changelog = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "previous version"); const changelog = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "previous version");
const reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit community"); const reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit community");
const discord = link("https://discordapp.com/invite/X7E84HU", "Discord server"); const discord = link("https://discordapp.com/invite/X7E84HU", "Discord server");
@ -397,18 +396,10 @@ function showWelcomeMessage() {
alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version <b>${version}</b>. alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version <b>${version}</b>.
This version is compatible with ${changelog}, loaded <i>.map</i> files will be auto-updated. This version is compatible with ${changelog}, loaded <i>.map</i> files will be auto-updated.
<ul>${post} <ul>Main changes:
<li>River overview and River editor rework</li> <li>Ability to add river selecting its cells</li>
<li>River generation code refactored and optimized</li> <li>Keep river course on edit</li>
<li>Rivers discharge (flux) and mouth width calculated</li> <li>Refactor river rendering code</li>
<li>Lake editor rework</li>
<li>Lake type based on evaporation and river system</li>
<li>Lake flux, inlets and outlet tracked properly</li>
<li>Lake outlet width depends on flux</li>
<li>Lakes now have names</li>
<li>Rulers rework (v1.61)</li>
<li>New ocean pattern by Kiwiroo (v1.61)</li>
<li>Water erosion rework (v1.62)</li>
</ul> </ul>
<p>Join our ${discord} and ${reddit} to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p> <p>Join our ${discord} and ${reddit} to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
@ -624,6 +615,7 @@ function generate() {
drawCoastline(); drawCoastline();
Rivers.generate(); Rivers.generate();
drawRivers();
Lakes.defineGroup(); Lakes.defineGroup();
defineBiomes(); defineBiomes();

View file

@ -80,6 +80,7 @@
TIME && console.time("createStates"); TIME && console.time("createStates");
const states = [{i: 0, name: "Neutrals"}]; const states = [{i: 0, name: "Neutrals"}];
const colors = getColors(burgs.length - 1); const colors = getColors(burgs.length - 1);
const each5th = each(5);
burgs.forEach(function (b, i) { burgs.forEach(function (b, i) {
if (!i) return; // skip first element if (!i) return; // skip first element
@ -93,7 +94,7 @@
// states data // states data
const expansionism = rn(Math.random() * powerInput.value + 1, 1); const expansionism = rn(Math.random() * powerInput.value + 1, 1);
const basename = b.name.length < 9 && b.cell % 5 === 0 ? b.name : Names.getCultureShort(b.culture); const basename = b.name.length < 9 && each5th(b.cell) ? b.name : Names.getCultureShort(b.culture);
const name = Names.getState(basename, b.culture); const name = Names.getState(basename, b.culture);
const type = cultures[b.culture].type; const type = cultures[b.culture].type;

View file

@ -33,7 +33,7 @@ async function addFonts(url) {
function loadUsedFonts() { function loadUsedFonts() {
const fontsInUse = getFontsList(svg); const fontsInUse = getFontsList(svg);
const fontsToLoad = fontsInUse.filter(font => !fonts.includes(font)); const fontsToLoad = fontsInUse.filter(font => !fonts.includes(font));
if (fontsToLoad) { if (fontsToLoad?.length) {
const url = "https://fonts.googleapis.com/css?family=" + fontsToLoad.join("|"); const url = "https://fonts.googleapis.com/css?family=" + fontsToLoad.join("|");
addFonts(url); addFonts(url);
} }

View file

@ -422,7 +422,6 @@
if (d % 6 !== 0) return; if (d % 6 !== 0) return;
for (const l of d3.range(i)) { for (const l of d3.range(i)) {
const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3; cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3;
cur = min; cur = min;
} }

View file

@ -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() { 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 // turn all layers off
document document
.getElementById("mapLayers") .getElementById("mapLayers")
@ -291,7 +292,7 @@ function parseLoadedData(data) {
if (hasChildren(gridOverlay)) turnOn("toggleGrid"); if (hasChildren(gridOverlay)) turnOn("toggleGrid");
if (hasChildren(coordinates)) turnOn("toggleCoordinates"); if (hasChildren(coordinates)) turnOn("toggleCoordinates");
if (notHidden(compass) && hasChild(compass, "use")) turnOn("toggleCompass"); 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 (notHidden(terrain) && hasChildren(terrain)) turnOn("toggleRelief");
if (hasChildren(relig)) turnOn("toggleReligions"); if (hasChildren(relig)) turnOn("toggleReligions");
if (hasChildren(cults)) turnOn("toggleCultures"); if (hasChildren(cults)) turnOn("toggleCultures");
@ -705,6 +706,33 @@ function parseLoadedData(data) {
statesHalo.attr("opacity", opacity).attr("filter", "blur(5px)"); statesHalo.attr("opacity", opacity).attr("filter", "blur(5px)");
regions.attr("opacity", null).attr("filter", null); regions.attr("opacity", null).attr("filter", null);
} }
if (version < 1.65) {
// v 1.65 changed rivers data
for (const river of pack.rivers) {
const node = document.getElementById("river" + river.i);
if (node && !river.cells) {
const riverCells = new Set();
const length = node.getTotalLength() / 2;
const segments = Math.ceil(length / 6);
const increment = length / segments;
for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i);
const p2 = node.getPointAtLength(c);
const x = (p1.x + p2.x) / 2;
const y = (p1.y + p2.y) / 2;
const cell = findCell(x, y, 6);
if (cell) riverCells.add(cell);
}
river.cells = Array.from(riverCells);
}
pack.cells.i.forEach(i => {
if (pack.cells.r[i] && pack.cells.h[i] < 20) pack.cells.r[i] = 0;
});
}
}
})(); })();
void (function checkDataIntegrity() { void (function checkDataIntegrity() {

View file

@ -7,9 +7,14 @@
TIME && console.time("generateRivers"); TIME && console.time("generateRivers");
Math.random = aleaPRNG(seed); Math.random = aleaPRNG(seed);
const {cells, features} = pack; const {cells, features} = pack;
const p = cells.p;
const riversData = []; // rivers data const riversData = {}; // rivers data
const riverParents = {};
const addCellToRiver = function (cell, river) {
if (!riversData[river]) riversData[river] = [cell];
else riversData[river].push(cell);
};
cells.fl = new Uint16Array(cells.i.length); // water flux array cells.fl = new Uint16Array(cells.i.length); // water flux array
cells.r = new Uint16Array(cells.i.length); // rivers array cells.r = new Uint16Array(cells.i.length); // rivers array
cells.conf = new Uint8Array(cells.i.length); // confluences array cells.conf = new Uint8Array(cells.i.length); // confluences array
@ -20,6 +25,7 @@
resolveDepressions(h); resolveDepressions(h);
drainWater(); drainWater();
defineRivers(); defineRivers();
calculateConfluenceFlux();
Lakes.cleanupLakeData(); Lakes.cleanupLakeData();
if (allowErosion) cells.h = Uint8Array.from(h); // apply changed heights as basic one if (allowErosion) cells.h = Uint8Array.from(h); // apply changed heights as basic one
@ -28,51 +34,48 @@
function drainWater() { function drainWater() {
const MIN_FLUX_TO_FORM_RIVER = 30; const MIN_FLUX_TO_FORM_RIVER = 30;
const prec = grid.cells.prec;
const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]); const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
const lakeOutCells = Lakes.setClimateData(h); const lakeOutCells = Lakes.setClimateData(h);
land.forEach(function (i) { land.forEach(function (i) {
cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation cells.fl[i] += prec[cells.g[i]]; // add flux from precipitation
const [x, y] = p[i];
// create lake outlet if lake is not in deep depression and flux > evaporation // create lake outlet if lake is not in deep depression and flux > evaporation
const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : []; const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : [];
for (const lake of lakes) { for (const lake of lakes) {
const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i); const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
// allow chain lakes to retain identity // allow chain lakes to retain identity
if (cells.r[lakeCell] !== lake.river) { if (cells.r[lakeCell] !== lake.river) {
const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river); const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river);
const [x, y] = p[lakeCell];
const flux = cells.fl[lakeCell];
if (sameRiver) { if (sameRiver) {
cells.r[lakeCell] = lake.river; cells.r[lakeCell] = lake.river;
riversData.push({river: lake.river, cell: lakeCell, x, y, flux}); addCellToRiver(lakeCell, lake.river);
} else { } else {
cells.r[lakeCell] = riverNext; cells.r[lakeCell] = riverNext;
riversData.push({river: riverNext, cell: lakeCell, x, y, flux}); addCellToRiver(lakeCell, riverNext);
riverNext++; riverNext++;
} }
} }
lake.outlet = cells.r[lakeCell]; lake.outlet = cells.r[lakeCell];
flowDown(i, cells.fl[i], cells.fl[lakeCell], lake.outlet); flowDown(i, cells.fl[lakeCell], lake.outlet);
} }
// assign all tributary rivers to outlet basin // assign all tributary rivers to outlet basin
for (let outlet = lakes[0]?.outlet, l = 0; l < lakes.length; l++) { const outlet = lakes[0]?.outlet;
lakes[l].inlets?.forEach(fork => (riversData.find(r => r.river === fork).parent = outlet)); for (const lake of lakes) {
if (!Array.isArray(lake.inlets)) continue;
for (const inlet of lake.inlets) {
riverParents[inlet] = outlet;
}
} }
// near-border cell: pour water out of the screen // near-border cell: pour water out of the screen
if (cells.b[i] && cells.r[i]) { if (cells.b[i] && cells.r[i]) return addCellToRiver(-1, cells.r[i]);
const [x, y] = getBorderPoint(i);
riversData.push({river: cells.r[i], cell: -1, x, y, flux: cells.fl[i]});
return;
}
// downhill cell (make sure it's not in the source lake) // downhill cell (make sure it's not in the source lake)
let min = null; let min = null;
@ -89,31 +92,35 @@
if (h[i] <= h[min]) return; if (h[i] <= h[min]) return;
if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) { if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
// flux is too small to operate as a river
if (h[min] >= 20) cells.fl[min] += cells.fl[i]; if (h[min] >= 20) cells.fl[min] += cells.fl[i];
return; // flux is too small to operate as river return;
} }
// proclaim a new river // proclaim a new river
if (!cells.r[i]) { if (!cells.r[i]) {
cells.r[i] = riverNext; cells.r[i] = riverNext;
riversData.push({river: riverNext, cell: i, x, y, flux: cells.fl[i]}); addCellToRiver(i, riverNext);
riverNext++; riverNext++;
} }
flowDown(min, cells.fl[min], cells.fl[i], cells.r[i], i); flowDown(min, cells.fl[i], cells.r[i]);
}); });
} }
function flowDown(toCell, toFlux, fromFlux, river, fromCell = 0) { function flowDown(toCell, fromFlux, river) {
if (cells.r[toCell]) { const toFlux = cells.fl[toCell] - cells.conf[toCell];
const toRiver = cells.r[toCell];
if (toRiver) {
// downhill cell already has river assigned // downhill cell already has river assigned
if (toFlux < fromFlux) { if (fromFlux > toFlux) {
cells.conf[toCell] = cells.fl[toCell]; // mark confluence cells.conf[toCell] += cells.fl[toCell]; // mark confluence
if (h[toCell] >= 20) riversData.find(r => r.river === cells.r[toCell]).parent = river; // min river is a tributary of current river if (h[toCell] >= 20) riverParents[toRiver] = river; // min river is a tributary of current river
cells.r[toCell] = river; // re-assign river if downhill part has less flux cells.r[toCell] = river; // re-assign river if downhill part has less flux
} else { } else {
cells.conf[toCell] += fromFlux; // mark confluence cells.conf[toCell] += fromFlux; // mark confluence
if (h[toCell] >= 20) riversData.find(r => r.river === river).parent = cells.r[toCell]; // current river is a tributary of min river if (h[toCell] >= 20) riverParents[river] = toRiver; // current river is a tributary of min river
} }
} else cells.r[toCell] = river; // assign the river to the downhill cell } else cells.r[toCell] = river; // assign the river to the downhill cell
@ -126,53 +133,60 @@
waterBody.enteringFlux = fromFlux; waterBody.enteringFlux = fromFlux;
} }
waterBody.flux = waterBody.flux + fromFlux; waterBody.flux = waterBody.flux + fromFlux;
waterBody.inlets ? waterBody.inlets.push(river) : (waterBody.inlets = [river]); if (!waterBody.inlets) waterBody.inlets = [river];
else waterBody.inlets.push(river);
} }
} else { } else {
// propagate flux and add next river segment // propagate flux and add next river segment
cells.fl[toCell] += fromFlux; cells.fl[toCell] += fromFlux;
} }
const [x, y] = p[toCell]; addCellToRiver(toCell, river);
riversData.push({river, cell: toCell, x, y, flux: fromFlux});
} }
function defineRivers() { function defineRivers() {
cells.r = new Uint16Array(cells.i.length); // re-initiate rivers array // re-initialize rivers and confluence arrays
pack.rivers = []; // rivers data cells.r = new Uint16Array(cells.i.length);
const riverPaths = []; cells.conf = new Uint16Array(cells.i.length);
pack.rivers = [];
for (let r = 1; r <= riverNext; r++) { for (const key in riversData) {
const riverPoints = riversData.filter(d => d.river === r); const riverCells = riversData[key];
if (riverPoints.length < 3) continue; if (riverCells.length < 3) continue; // exclude tiny rivers
for (const segment of riverPoints) { const riverId = +key;
const i = segment.cell; for (const cell of riverCells) {
if (cells.r[i]) continue; if (cell < 0 || cells.h[cell] < 20) continue;
if (cells.h[i] < 20) continue;
cells.r[i] = r; // mark real confluences and assign river to cells
if (cells.r[cell]) cells.conf[cell] = 1;
else cells.r[cell] = riverId;
} }
const source = riverPoints[0].cell; const source = riverCells[0];
const mouth = riverPoints[riverPoints.length - 2].cell; const mouth = riverCells[riverCells.length - 2];
const parent = riverParents[key] || 0;
const widthFactor = rn(0.8 + Math.random() * 0.4, 1); // river width modifier [.8, 1.2] const widthFactor = !parent || parent === riverId ? 1.2 : 1;
const sourceWidth = cells.h[source] >= 20 ? 0.1 : rn(Math.min(Math.max((cells.fl[source] / 500) ** 0.4, 0.5), 1.7), 2); const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
const riverCells = riverPoints.map(point => point.cell); pack.rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells});
const riverMeandered = addMeandering(riverCells, sourceWidth * 10, 0.5);
const [path, length, offset] = getPath(riverMeandered, widthFactor, sourceWidth);
riverPaths.push([path, r]);
const parent = riverPoints[0].parent || 0;
const width = rn(offset ** 2, 2); // mounth width in km
const discharge = last(riverPoints).flux; // in m3/s
pack.rivers.push({i: r, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells});
} }
}
// draw rivers function calculateConfluenceFlux() {
rivers.html(riverPaths.map(d => `<path id="river${d[1]}" d="${d[0]}"/>`).join("")); for (const i of cells.i) {
if (!cells.conf[i]) continue;
const sortedInflux = cells.c[i]
.filter(c => cells.r[c] && h[c] > h[i])
.map(c => cells.fl[c])
.sort((a, b) => b - a);
cells.conf[i] = sortedInflux.reduce((acc, flux, index) => (index ? acc + flux : acc), 0);
}
} }
}; };
@ -245,102 +259,131 @@
}; };
// add points at 1/3 and 2/3 of a line between adjacents river cells // add points at 1/3 and 2/3 of a line between adjacents river cells
const addMeandering = function (riverCells, width = 1, meandering = 0.5) { const addMeandering = function (riverCells, riverPoints = null, meandering = 0.5) {
const {fl, conf, h} = pack.cells;
const meandered = []; const meandered = [];
const {p, conf} = pack.cells; const lastStep = riverCells.length - 1;
const lastCell = riverCells.length - 1; const points = getRiverPoints(riverCells, riverPoints);
let step = h[riverCells[0]] < 20 ? 1 : 10;
for (let i = 0; i <= lastCell; i++, width++) { let fluxPrev = 0;
const getFlux = (step, flux) => (step === lastStep ? fluxPrev : flux);
for (let i = 0; i <= lastStep; i++, step++) {
const cell = riverCells[i]; const cell = riverCells[i];
const [x1, y1] = p[cell]; const isLastCell = i === lastStep;
meandered.push([x1, y1, conf[cell]]);
if (i === lastCell) break; const [x1, y1] = points[i];
const flux1 = getFlux(i, fl[cell]);
fluxPrev = flux1;
meandered.push([x1, y1, flux1]);
if (isLastCell) break;
const nextCell = riverCells[i + 1]; const nextCell = riverCells[i + 1];
const [x2, y2] = points[i + 1];
if (nextCell === -1) { if (nextCell === -1) {
meandered.push(getBorderPoint(cell)); meandered.push([x2, y2, fluxPrev]);
break; break;
} }
const [x2, y2] = p[nextCell];
const angle = Math.atan2(y2 - y1, x2 - x1);
const sin = Math.sin(angle);
const cos = Math.cos(angle);
const meander = meandering + 1 / width + Math.random() * Math.max(meandering - width / 100, 0);
const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
if (dist2 <= 25 && riverCells.length >= 6) continue;
if (width < 10 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) { const flux2 = getFlux(i + 1, fl[nextCell]);
const keepInitialFlux = conf[nextCell] || flux1 === flux2;
const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
const angle = Math.atan2(y2 - y1, x2 - x1);
const sinMeander = Math.sin(angle) * meander;
const cosMeander = Math.cos(angle) * meander;
if (step < 10 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
// if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment // if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
const p1x = (x1 * 2 + x2) / 3 + -sin * meander; const p1x = (x1 * 2 + x2) / 3 + -sinMeander;
const p1y = (y1 * 2 + y2) / 3 + cos * meander; const p1y = (y1 * 2 + y2) / 3 + cosMeander;
const p2x = (x1 + x2 * 2) / 3 + sin * meander; const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
const p2y = (y1 + y2 * 2) / 3 + cos * meander; const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
meandered.push([p1x, p1y], [p2x, p2y]); const [p1fl, p2fl] = keepInitialFlux ? [flux1, flux1] : [(flux1 * 2 + flux2) / 3, (flux1 + flux2 * 2) / 3];
meandered.push([p1x, p1y, p1fl], [p2x, p2y, p2fl]);
} else if (dist2 > 25 || riverCells.length < 6) { } else if (dist2 > 25 || riverCells.length < 6) {
// if dist is medium or river is small add 1 extra middlepoint // if dist is medium or river is small add 1 extra middlepoint
const p1x = (x1 + x2) / 2 + -sin * meander; const p1x = (x1 + x2) / 2 + -sinMeander;
const p1y = (y1 + y2) / 2 + cos * meander; const p1y = (y1 + y2) / 2 + cosMeander;
meandered.push([p1x, p1y]); const p1fl = keepInitialFlux ? flux1 : (flux1 + flux2) / 2;
meandered.push([p1x, p1y, p1fl]);
} }
} }
return meandered; return meandered;
}; };
const getPath = function (points, widthFactor = 1, width = 0.1) { const getRiverPoints = (riverCells, riverPoints) => {
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); // sum of segments length const {p} = pack.cells;
const widening = 1000 + riverLength * 30; return riverCells.map((cell, i) => {
const factor = riverLength / points.length; if (riverPoints && riverPoints[i]) return riverPoints[i];
let offset; if (cell === -1) return getBorderPoint(riverCells[i - 1]);
return p[cell];
});
};
// store points on both sides to build a valid polygon const getBorderPoint = i => {
const [x, y] = pack.cells.p[i];
const min = Math.min(y, graphHeight - y, x, graphWidth - x);
if (min === y) return [x, 0];
else if (min === graphHeight - y) return [x, graphHeight];
else if (min === x) return [0, y];
return [graphWidth, y];
};
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 riverPointsLeft = []; const riverPointsLeft = [];
const riverPointsRight = []; const riverPointsRight = [];
for (let p = 0; p < points.length; p++) { for (let p = 0; p < points.length; p++) {
const [x0, y0] = points[p - 1] || points[p]; const [x0, y0] = points[p - 1] || points[p];
const [x1, y1] = points[p]; const [x1, y1, flux] = points[p];
const [x2, y2] = points[p + 1] || points[p]; const [x2, y2] = points[p + 1] || points[p];
offset = width + (Math.atan(Math.pow(p * factor, 2) / widening) / 2) * widthFactor; const offset = getOffset(flux, p, widthFactor, startingWidth);
if (points[p + 2] && points[p + 1][2]) {
const confluence = points[p + 1][2];
width += Math.atan((confluence * 5) / widening);
}
const angle = Math.atan2(y0 - y2, x0 - x2); const angle = Math.atan2(y0 - y2, x0 - x2);
const sinOffset = Math.sin(angle) * offset; const sinOffset = Math.sin(angle) * offset;
const cosOffset = Math.cos(angle) * offset; const cosOffset = Math.cos(angle) * offset;
riverPointsLeft.push([x1 - sinOffset, y1 + cosOffset]); riverPointsLeft.push([x1 - sinOffset, y1 + cosOffset]);
riverPointsRight.unshift([x1 + sinOffset, y1 - cosOffset]); riverPointsRight.push([x1 + sinOffset, y1 - cosOffset]);
} }
// generate polygon path and return const right = lineGen(riverPointsRight.reverse());
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const right = lineGen(riverPointsRight);
let left = lineGen(riverPointsLeft); let left = lineGen(riverPointsLeft);
left = left.substring(left.indexOf("C")); left = left.substring(left.indexOf("C"));
return [round(right + left, 2), rn(riverLength, 2), offset]; return round(right + left, 1);
}; };
const specify = function () { const specify = function () {
const rivers = pack.rivers; const rivers = pack.rivers;
if (!rivers.length) return; if (!rivers.length) return;
Math.random = aleaPRNG(seed);
const thresholdElement = Math.ceil(rivers.length * 0.15);
const smallLength = rivers.map(r => r.length || 0).sort((a, b) => a - b)[thresholdElement];
const smallType = {Creek: 9, River: 3, Brook: 3, Stream: 1}; // weighted small river types
for (const r of rivers) { for (const river of rivers) {
r.basin = getBasin(r.i); river.basin = getBasin(river.i);
r.name = getName(r.mouth); river.name = getName(river.mouth);
const small = r.length < smallLength; river.type = getType(river);
r.type = r.parent && !(r.i % 6) ? (small ? "Branch" : "Fork") : small ? rw(smallType) : "River";
} }
}; };
@ -348,6 +391,36 @@
return Names.getCulture(pack.cells.culture[cell]); return Names.getCulture(pack.cells.culture[cell]);
}; };
// weighted arrays of river type names
const riverTypes = {
main: {
big: {River: 1},
small: {Creek: 9, River: 3, Brook: 3, Stream: 1}
},
fork: {
big: {Fork: 1},
small: {Branch: 1}
}
};
let smallLength = null;
const getType = function ({i, length, parent}) {
if (smallLength === null) {
const threshold = Math.ceil(pack.rivers.length * 0.15);
smallLength = pack.rivers.map(r => r.length || 0).sort((a, b) => a - b)[threshold];
}
const isSmall = length < smallLength;
const isFork = each(3)(i) && parent && parent !== i;
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 // remove river and all its tributaries
const remove = function (id) { const remove = function (id) {
const cells = pack.cells; const cells = pack.cells;
@ -368,14 +441,5 @@
return getBasin(parent); return getBasin(parent);
}; };
const getBorderPoint = i => { return {generate, alterHeights, resolveDepressions, addMeandering, getRiverPath, specify, getName, getType, getBasin, getWidth, getOffset, getApproximateLength, getRiverPoints, remove};
const [x, y] = pack.cells.p[i];
const min = Math.min(y, graphHeight - y, x, graphWidth - x);
if (min === y) return [x, 0];
else if (min === graphHeight - y) return [x, graphHeight];
else if (min === x) return [0, y];
return [graphWidth, y];
};
return {generate, alterHeights, resolveDepressions, addMeandering, getPath, specify, getName, getBasin, remove};
}); });

View file

@ -144,7 +144,7 @@ async function getMapURL(type, options = {}) {
cloneEl.id = "fantasyMap"; cloneEl.id = "fantasyMap";
document.body.appendChild(cloneEl); document.body.appendChild(cloneEl);
const clone = d3.select(cloneEl); const clone = d3.select(cloneEl);
if (debug) clone.select("#debug").remove(); if (!debug) clone.select("#debug").remove();
const cloneDefs = cloneEl.getElementsByTagName("defs")[0]; const cloneDefs = cloneEl.getElementsByTagName("defs")[0];
const svgDefs = document.getElementById("defElements"); const svgDefs = document.getElementById("defElements");

View file

@ -6,10 +6,7 @@ restoreDefaultEvents(); // apply default viewbox events on load
// restore default viewbox events // restore default viewbox events
function restoreDefaultEvents() { function restoreDefaultEvents() {
svg.call(zoom); svg.call(zoom);
viewbox.style("cursor", "default") viewbox.style("cursor", "default").on(".drag", null).on("click", clicked).on("touchmove mousemove", moved);
.on(".drag", null)
.on("click", clicked)
.on("touchmove mousemove", moved);
legend.call(d3.drag().on("start", dragLegendBox)); legend.call(d3.drag().on("start", dragLegendBox));
} }
@ -17,12 +14,14 @@ function restoreDefaultEvents() {
function clicked() { function clicked() {
const el = d3.event.target; const el = d3.event.target;
if (!el || !el.parentElement || !el.parentElement.parentElement) return; if (!el || !el.parentElement || !el.parentElement.parentElement) return;
const parent = el.parentElement, grand = parent.parentElement, great = grand.parentElement; const parent = el.parentElement;
const grand = parent.parentElement;
const great = grand.parentElement;
const p = d3.mouse(this); const p = d3.mouse(this);
const i = findCell(p[0], p[1]); const i = findCell(p[0], p[1]);
if (grand.id === "emblems") editEmblem(); if (grand.id === "emblems") editEmblem();
else if (parent.id === "rivers") editRiver(); else if (parent.id === "rivers") editRiver(el.id);
else if (grand.id === "routes") editRoute(); else if (grand.id === "routes") editRoute();
else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel(); else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel();
else if (grand.id === "burgLabels") editBurg(); else if (grand.id === "burgLabels") editBurg();
@ -33,10 +32,9 @@ function clicked() {
else if (grand.id === "coastline") editCoastline(); else if (grand.id === "coastline") editCoastline();
else if (great.id === "armies") editRegiment(); else if (great.id === "armies") editRegiment();
else if (pack.cells.t[i] === 1) { else if (pack.cells.t[i] === 1) {
const node = document.getElementById("island_"+pack.cells.f[i]); const node = document.getElementById("island_" + pack.cells.f[i]);
editCoastline(node); editCoastline(node);
} } else if (grand.id === "lakes") editLake();
else if (grand.id === "lakes") editLake();
} }
// clear elSelected variable // clear elSelected variable
@ -51,9 +49,11 @@ function unselect() {
// close all dialogs except stated // close all dialogs except stated
function closeDialogs(except = "#except") { function closeDialogs(except = "#except") {
$(".dialog:visible").not(except).each(function() { $(".dialog:visible")
$(this).dialog("close"); .not(except)
}); .each(function () {
$(this).dialog("close");
});
} }
// move brush radius circle // move brush radius circle
@ -79,8 +79,10 @@ function fitContent() {
} }
// apply sorting behaviour for lines on Editor header click // apply sorting behaviour for lines on Editor header click
document.querySelectorAll(".sortable").forEach(function(e) { document.querySelectorAll(".sortable").forEach(function (e) {
e.addEventListener("click", function(e) {sortLines(this);}); e.addEventListener("click", function (e) {
sortLines(this);
});
}); });
function sortLines(header) { function sortLines(header) {
@ -90,7 +92,9 @@ function sortLines(header) {
const headers = header.parentNode; const headers = header.parentNode;
headers.querySelectorAll("div.sortable").forEach(e => { headers.querySelectorAll("div.sortable").forEach(e => {
e.classList.forEach(c => {if(c.includes("icon-sort")) e.classList.remove(c);}); e.classList.forEach(c => {
if (c.includes("icon-sort")) e.classList.remove(c);
});
}); });
header.classList.add("icon-sort-" + type + order); header.classList.add("icon-sort-" + type + order);
applySorting(headers); applySorting(headers);
@ -105,16 +109,19 @@ function applySorting(headers) {
const list = headers.nextElementSibling; const list = headers.nextElementSibling;
const lines = Array.from(list.children); const lines = Array.from(list.children);
lines.sort((a, b) => { lines
const an = name ? a.dataset[sortby] : +a.dataset[sortby]; .sort((a, b) => {
const bn = name ? b.dataset[sortby] : +b.dataset[sortby]; const an = name ? a.dataset[sortby] : +a.dataset[sortby];
return (an > bn ? 1 : an < bn ? -1 : 0) * desc; const bn = name ? b.dataset[sortby] : +b.dataset[sortby];
}).forEach(line => list.appendChild(line)); return (an > bn ? 1 : an < bn ? -1 : 0) * desc;
})
.forEach(line => list.appendChild(line));
} }
function addBurg(point) { function addBurg(point) {
const cells = pack.cells; const cells = pack.cells;
const x = rn(point[0], 2), y = rn(point[1], 2); const x = rn(point[0], 2),
y = rn(point[1], 2);
const cell = findCell(x, point[1]); const cell = findCell(x, point[1]);
const i = pack.burgs.length; const i = pack.burgs.length;
const culture = cells.culture[cell]; const culture = cells.culture[cell];
@ -123,11 +130,11 @@ function addBurg(point) {
const feature = cells.f[cell]; const feature = cells.f[cell];
const temple = pack.states[state].form === "Theocracy"; const temple = pack.states[state].form === "Theocracy";
const population = Math.max((cells.s[cell] + cells.road[cell]) / 3 + i / 1000 + cell % 100 / 1000, .1); const population = Math.max((cells.s[cell] + cells.road[cell]) / 3 + i / 1000 + (cell % 100) / 1000, 0.1);
const type = BurgsAndStates.getType(cell, false); const type = BurgsAndStates.getType(cell, false);
// generate emblem // generate emblem
const coa = COA.generate(pack.states[state].coa, .25, null, type); const coa = COA.generate(pack.states[state].coa, 0.25, null, type);
coa.shield = COA.getShield(culture, state); coa.shield = COA.getShield(culture, state);
COArenderer.add("burg", i, coa, x, y); COArenderer.add("burg", i, coa, x, y);
@ -135,10 +142,23 @@ function addBurg(point) {
cells.burg[cell] = i; cells.burg[cell] = i;
const townSize = burgIcons.select("#towns").attr("size") || 0.5; const townSize = burgIcons.select("#towns").attr("size") || 0.5;
burgIcons.select("#towns").append("circle").attr("id", "burg"+i).attr("data-id", i) burgIcons
.attr("cx", x).attr("cy", y).attr("r", townSize); .select("#towns")
burgLabels.select("#towns").append("text").attr("id", "burgLabel"+i).attr("data-id", i) .append("circle")
.attr("x", x).attr("y", y).attr("dy", `${townSize * -1.5}px`).text(name); .attr("id", "burg" + i)
.attr("data-id", i)
.attr("cx", x)
.attr("cy", y)
.attr("r", townSize);
burgLabels
.select("#towns")
.append("text")
.attr("id", "burgLabel" + i)
.attr("data-id", i)
.attr("x", x)
.attr("y", y)
.attr("dy", `${townSize * -1.5}px`)
.text(name);
BurgsAndStates.defineBurgFeatures(pack.burgs[i]); BurgsAndStates.defineBurgFeatures(pack.burgs[i]);
return i; return i;
@ -148,17 +168,20 @@ function moveBurgToGroup(id, g) {
const label = document.querySelector("#burgLabels [data-id='" + id + "']"); const label = document.querySelector("#burgLabels [data-id='" + id + "']");
const icon = document.querySelector("#burgIcons [data-id='" + id + "']"); const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
const anchor = document.querySelector("#anchors [data-id='" + id + "']"); const anchor = document.querySelector("#anchors [data-id='" + id + "']");
if (!label || !icon) {ERROR && console.error("Cannot find label or icon elements"); return;} if (!label || !icon) {
ERROR && console.error("Cannot find label or icon elements");
return;
}
document.querySelector("#burgLabels > #"+g).appendChild(label); document.querySelector("#burgLabels > #" + g).appendChild(label);
document.querySelector("#burgIcons > #"+g).appendChild(icon); document.querySelector("#burgIcons > #" + g).appendChild(icon);
const iconSize = icon.parentNode.getAttribute("size"); const iconSize = icon.parentNode.getAttribute("size");
icon.setAttribute("r", iconSize); icon.setAttribute("r", iconSize);
label.setAttribute("dy", `${iconSize * -1.5}px`); label.setAttribute("dy", `${iconSize * -1.5}px`);
if (anchor) { if (anchor) {
document.querySelector("#anchors > #"+g).appendChild(anchor); document.querySelector("#anchors > #" + g).appendChild(anchor);
const anchorSize = +anchor.parentNode.getAttribute("size"); const anchorSize = +anchor.parentNode.getAttribute("size");
anchor.setAttribute("width", anchorSize); anchor.setAttribute("width", anchorSize);
anchor.setAttribute("height", anchorSize); anchor.setAttribute("height", anchorSize);
@ -175,7 +198,8 @@ function removeBurg(id) {
if (icon) icon.remove(); if (icon) icon.remove();
if (anchor) anchor.remove(); if (anchor) anchor.remove();
const cells = pack.cells, burg = pack.burgs[id]; const cells = pack.cells,
burg = pack.burgs[id];
burg.removed = true; burg.removed = true;
cells.burg[burg.cell] = 0; cells.burg[burg.cell] = 0;
@ -189,8 +213,14 @@ function removeBurg(id) {
function toggleCapital(burg) { function toggleCapital(burg) {
const state = pack.burgs[burg].state; const state = pack.burgs[burg].state;
if (!state) {tip("Neutral lands cannot have a capital", false, "error"); return;} if (!state) {
if (pack.burgs[burg].capital) {tip("To change capital please assign a capital status to another burg of this state", false, "error"); return;} tip("Neutral lands cannot have a capital", false, "error");
return;
}
if (pack.burgs[burg].capital) {
tip("To change capital please assign a capital status to another burg of this state", false, "error");
return;
}
const old = pack.states[state].capital; const old = pack.states[state].capital;
// change statuses // change statuses
@ -206,7 +236,10 @@ function togglePort(burg) {
const anchor = document.querySelector("#anchors [data-id='" + burg + "']"); const anchor = document.querySelector("#anchors [data-id='" + burg + "']");
if (anchor) anchor.remove(); if (anchor) anchor.remove();
const b = pack.burgs[burg]; const b = pack.burgs[burg];
if (b.port) {b.port = 0; return;} // not a port anymore if (b.port) {
b.port = 0;
return;
} // not a port anymore
const haven = pack.cells.haven[b.cell]; const haven = pack.cells.haven[b.cell];
const port = haven ? pack.cells.f[haven] : -1; const port = haven ? pack.cells.f[haven] : -1;
@ -214,11 +247,16 @@ function togglePort(burg) {
b.port = port; b.port = port;
const g = b.capital ? "cities" : "towns"; const g = b.capital ? "cities" : "towns";
const group = anchors.select("g#"+g); const group = anchors.select("g#" + g);
const size = +group.attr("size"); const size = +group.attr("size");
group.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", burg) group
.attr("x", rn(b.x - size * .47, 2)).attr("y", rn(b.y - size * .47, 2)) .append("use")
.attr("width", size).attr("height", size); .attr("xlink:href", "#icon-anchor")
.attr("data-id", burg)
.attr("x", rn(b.x - size * 0.47, 2))
.attr("y", rn(b.y - size * 0.47, 2))
.attr("width", size)
.attr("height", size);
} }
function toggleBurgLock(burg) { function toggleBurgLock(burg) {
@ -251,38 +289,49 @@ function drawLegend(name, data) {
const vOffset = fontSize / 2; const vOffset = fontSize / 2;
// append items // append items
const boxes = legend.append("g").attr("stroke-width", .5).attr("stroke", "#111111").attr("stroke-dasharray", "none"); const boxes = legend.append("g").attr("stroke-width", 0.5).attr("stroke", "#111111").attr("stroke-dasharray", "none");
const labels = legend.append("g").attr("fill", "#000000").attr("stroke", "none"); const labels = legend.append("g").attr("fill", "#000000").attr("stroke", "none");
const columns = Math.ceil(data.length / itemsInCol); const columns = Math.ceil(data.length / itemsInCol);
for (let column=0, i=0; column < columns; column++) { for (let column = 0, i = 0; column < columns; column++) {
const linesInColumn = Math.ceil(data.length / columns); const linesInColumn = Math.ceil(data.length / columns);
const offset = column ? colOffset * 2 + legend.node().getBBox().width : colOffset; const offset = column ? colOffset * 2 + legend.node().getBBox().width : colOffset;
for (let l=0; l < linesInColumn && data[i]; l++, i++) { for (let l = 0; l < linesInColumn && data[i]; l++, i++) {
boxes.append("rect").attr("fill", data[i][1]) boxes
.attr("x", offset).attr("y", lineHeight + l*lineHeight + vOffset) .append("rect")
.attr("width", colorBoxSize).attr("height", colorBoxSize); .attr("fill", data[i][1])
.attr("x", offset)
.attr("y", lineHeight + l * lineHeight + vOffset)
.attr("width", colorBoxSize)
.attr("height", colorBoxSize);
labels.append("text").text(data[i][2]) labels
.attr("x", offset + colorBoxSize * 1.6).attr("y", fontSize/1.6 + lineHeight + l*lineHeight + vOffset); .append("text")
.text(data[i][2])
.attr("x", offset + colorBoxSize * 1.6)
.attr("y", fontSize / 1.6 + lineHeight + l * lineHeight + vOffset);
} }
} }
// append label // append label
const offset = colOffset + legend.node().getBBox().width / 2; const offset = colOffset + legend.node().getBBox().width / 2;
labels.append("text") labels
.attr("text-anchor", "middle").attr("font-weight", "bold").attr("font-size", "1.2em") .append("text")
.attr("id", "legendLabel").text(name).attr("x", offset).attr("y", fontSize * 1.1 + vOffset / 2); .attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("font-size", "1.2em")
.attr("id", "legendLabel")
.text(name)
.attr("x", offset)
.attr("y", fontSize * 1.1 + vOffset / 2);
// append box // append box
const bbox = legend.node().getBBox(); const bbox = legend.node().getBBox();
const width = bbox.width + colOffset * 2; const width = bbox.width + colOffset * 2;
const height = bbox.height + colOffset / 2 + vOffset; const height = bbox.height + colOffset / 2 + vOffset;
legend.insert("rect", ":first-child").attr("id", "legendBox") legend.insert("rect", ":first-child").attr("id", "legendBox").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", backClr).attr("fill-opacity", opacity);
.attr("x", 0).attr("y", 0).attr("width", width).attr("height", height)
.attr("fill", backClr).attr("fill-opacity", opacity);
fitLegendBox(); fitLegendBox();
} }
@ -293,7 +342,8 @@ function fitLegendBox() {
const px = isNaN(+legend.attr("data-x")) ? 99 : legend.attr("data-x") / 100; const px = isNaN(+legend.attr("data-x")) ? 99 : legend.attr("data-x") / 100;
const py = isNaN(+legend.attr("data-y")) ? 93 : legend.attr("data-y") / 100; const py = isNaN(+legend.attr("data-y")) ? 93 : legend.attr("data-y") / 100;
const bbox = legend.node().getBBox(); const bbox = legend.node().getBBox();
const x = rn(svgWidth * px - bbox.width), y = rn(svgHeight * py - bbox.height); const x = rn(svgWidth * px - bbox.width),
y = rn(svgHeight * py - bbox.height);
legend.attr("transform", `translate(${x},${y})`); legend.attr("transform", `translate(${x},${y})`);
} }
@ -301,19 +351,23 @@ function fitLegendBox() {
function redrawLegend() { function redrawLegend() {
if (!legend.select("rect").size()) return; if (!legend.select("rect").size()) return;
const name = legend.select("#legendLabel").text(); const name = legend.select("#legendLabel").text();
const data = legend.attr("data").split("|").map(l => l.split(",")); const data = legend
.attr("data")
.split("|")
.map(l => l.split(","));
drawLegend(name, data); drawLegend(name, data);
} }
function dragLegendBox() { function dragLegendBox() {
const tr = parseTransform(this.getAttribute("transform")); const tr = parseTransform(this.getAttribute("transform"));
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y; const x = +tr[0] - d3.event.x,
y = +tr[1] - d3.event.y;
const bbox = legend.node().getBBox(); const bbox = legend.node().getBBox();
d3.event.on("drag", function() { d3.event.on("drag", function () {
const px = rn((x + d3.event.x + bbox.width) / svgWidth * 100, 2); const px = rn(((x + d3.event.x + bbox.width) / svgWidth) * 100, 2);
const py = rn((y + d3.event.y + bbox.height) / svgHeight * 100, 2); const py = rn(((y + d3.event.y + bbox.height) / svgHeight) * 100, 2);
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`; const transform = `translate(${x + d3.event.x},${y + d3.event.y})`;
legend.attr("transform", transform).attr("data-x", px).attr("data-y", py); legend.attr("transform", transform).attr("data-x", px).attr("data-y", py);
}); });
} }
@ -330,9 +384,16 @@ function createPicker() {
const closePicker = () => contaiter.style("display", "none"); const closePicker = () => contaiter.style("display", "none");
const contaiter = d3.select("body").append("svg").attr("id", "pickerContainer").attr("width", "100%").attr("height", "100%"); const contaiter = d3.select("body").append("svg").attr("id", "pickerContainer").attr("width", "100%").attr("height", "100%");
contaiter.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("opacity", .2) contaiter.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("opacity", 0.2).on("mousemove", cl).on("click", closePicker);
.on("mousemove", cl).on("click", closePicker); const picker = contaiter
const picker = contaiter.append("g").attr("id", "picker").call(d3.drag().filter(() => event.target.tagName !== "INPUT").on("start", dragPicker)); .append("g")
.attr("id", "picker")
.call(
d3
.drag()
.filter(() => event.target.tagName !== "INPUT")
.on("start", dragPicker)
);
const controls = picker.append("g").attr("id", "pickerControls"); const controls = picker.append("g").attr("id", "pickerControls");
const h = controls.append("g"); const h = controls.append("g");
@ -343,7 +404,7 @@ function createPicker() {
const s = controls.append("g"); const s = controls.append("g");
s.append("text").attr("x", 113).attr("y", 14).text("S:"); s.append("text").attr("x", 113).attr("y", 14).text("S:");
s.append("line").attr("x1", 124).attr("y1", 10).attr("x2", 206).attr("y2", 10) s.append("line").attr("x1", 124).attr("y1", 10).attr("x2", 206).attr("y2", 10);
s.append("circle").attr("cx", 181.4).attr("cy", 10).attr("r", 5).attr("id", "pickerS"); s.append("circle").attr("cx", 181.4).attr("cy", 10).attr("r", 5).attr("id", "pickerS");
s.on("mousemove", () => tip("Set palette saturation")); s.on("mousemove", () => tip("Set palette saturation"));
@ -356,8 +417,13 @@ function createPicker() {
controls.selectAll("line").on("click", clickPickerControl); controls.selectAll("line").on("click", clickPickerControl);
controls.selectAll("circle").call(d3.drag().on("start", dragPickerControl)); controls.selectAll("circle").call(d3.drag().on("start", dragPickerControl));
const spaces = picker.append("foreignObject").attr("id", "pickerSpaces") const spaces = picker
.attr("x", 4).attr("y", 20).attr("width", 303).attr("height", 20) .append("foreignObject")
.attr("id", "pickerSpaces")
.attr("x", 4)
.attr("y", 20)
.attr("width", 303)
.attr("height", 20)
.on("mousemove", () => tip("Color value in different color spaces. Edit to change")); .on("mousemove", () => tip("Color value in different color spaces. Edit to change"));
const html = ` const html = `
<label style="margin-right: 6px">HSL: <label style="margin-right: 6px">HSL:
@ -371,7 +437,7 @@ function createPicker() {
<input type="number" id="pickerRGB_B" data-space="rgb" min=0 max=255 value="232"> <input type="number" id="pickerRGB_B" data-space="rgb" min=0 max=255 value="232">
</label> </label>
<label>HEX: <input type="text" id="pickerHEX" data-space="hex" style="width:42px" autocorrect="off" spellcheck="false" value="#7d8ee8"></label>`; <label>HEX: <input type="text" id="pickerHEX" data-space="hex" style="width:42px" autocorrect="off" spellcheck="false" value="#7d8ee8"></label>`;
spaces.node().insertAdjacentHTML('beforeend', html); spaces.node().insertAdjacentHTML("beforeend", html);
spaces.selectAll("input").on("change", changePickerSpace); spaces.selectAll("input").on("change", changePickerSpace);
const colors = picker.append("g").attr("id", "pickerColors").attr("stroke", "#333333"); const colors = picker.append("g").attr("id", "pickerColors").attr("stroke", "#333333");
@ -379,19 +445,38 @@ function createPicker() {
const hatching = d3.selectAll("g#hatching > pattern"); const hatching = d3.selectAll("g#hatching > pattern");
const number = hatching.size(); const number = hatching.size();
const clr = d3.range(number).map(i => d3.hsl(i/number*360, .7, .7).hex()); const clr = d3.range(number).map(i => d3.hsl((i / number) * 360, 0.7, 0.7).hex());
clr.forEach(function(d, i) { clr.forEach(function (d, i) {
colors.append("rect").attr("id", "picker_" + d).attr("fill", d).attr("class", i?"":"selected") colors
.attr("x", i*22+4).attr("y", 40).attr("width", 16).attr("height", 16); .append("rect")
.attr("id", "picker_" + d)
.attr("fill", d)
.attr("class", i ? "" : "selected")
.attr("x", i * 22 + 4)
.attr("y", 40)
.attr("width", 16)
.attr("height", 16);
}); });
hatching.each(function(d, i) { hatching.each(function (d, i) {
hatches.append("rect").attr("id", "picker_" + this.id).attr("fill", "url(#" + this.id + ")") hatches
.attr("x", i*22+4).attr("y", 61).attr("width", 16).attr("height", 16); .append("rect")
.attr("id", "picker_" + this.id)
.attr("fill", "url(#" + this.id + ")")
.attr("x", i * 22 + 4)
.attr("y", 61)
.attr("width", 16)
.attr("height", 16);
}); });
colors.selectAll("rect").on("click", pickerFillClicked).on("mousemove", () => tip("Click to fill with the color")); colors
hatches.selectAll("rect").on("click", pickerFillClicked).on("mousemove", () => tip("Click to fill with the hatching")); .selectAll("rect")
.on("click", pickerFillClicked)
.on("mousemove", () => tip("Click to fill with the color"));
hatches
.selectAll("rect")
.on("click", pickerFillClicked)
.on("mousemove", () => tip("Click to fill with the hatching"));
// append box // append box
const bbox = picker.node().getBBox(); const bbox = picker.node().getBBox();
@ -403,12 +488,15 @@ function createPicker() {
picker.insert("rect", ":first-child").attr("x", 288).attr("y", -21).attr("id", "pickerCloseRect").attr("width", 14).attr("height", 14).on("mousemove", cl).on("click", closePicker); picker.insert("rect", ":first-child").attr("x", 288).attr("y", -21).attr("id", "pickerCloseRect").attr("width", 14).attr("height", 14).on("mousemove", cl).on("click", closePicker);
picker.insert("text", ":first-child").attr("x", 12).attr("y", -10).attr("id", "pickerLabel").text("Color Picker").on("mousemove", pos); picker.insert("text", ":first-child").attr("x", 12).attr("y", -10).attr("id", "pickerLabel").text("Color Picker").on("mousemove", pos);
picker.insert("rect", ":first-child").attr("x", 0).attr("y", -30).attr("width", width).attr("height", 30).attr("id", "pickerHeader").on("mousemove", pos); picker.insert("rect", ":first-child").attr("x", 0).attr("y", -30).attr("width", width).attr("height", 30).attr("id", "pickerHeader").on("mousemove", pos);
picker.attr("transform", `translate(${(svgWidth-width)/2},${(svgHeight-height)/2})`); picker.attr("transform", `translate(${(svgWidth - width) / 2},${(svgHeight - height) / 2})`);
} }
function updateSelectedRect(fill) { function updateSelectedRect(fill) {
document.getElementById("picker").querySelector("rect.selected").classList.remove("selected"); document.getElementById("picker").querySelector("rect.selected").classList.remove("selected");
document.getElementById("picker").querySelector("rect[fill='"+fill.toLowerCase()+"']").classList.add("selected"); document
.getElementById("picker")
.querySelector("rect[fill='" + fill.toLowerCase() + "']")
.classList.add("selected");
} }
function updateSpaces() { function updateSpaces() {
@ -438,8 +526,8 @@ function updatePickerColors() {
const s = getPickerControl(pickerS, 1); const s = getPickerControl(pickerS, 1);
const l = getPickerControl(pickerL, 1); const l = getPickerControl(pickerL, 1);
colors.each(function(d, i) { colors.each(function (d, i) {
const clr = d3.hsl(i/number*180+h, s, l).hex(); const clr = d3.hsl((i / number) * 180 + h, s, l).hex();
this.setAttribute("id", "picker_" + clr); this.setAttribute("id", "picker_" + clr);
this.setAttribute("fill", clr); this.setAttribute("fill", clr);
}); });
@ -461,11 +549,11 @@ function openPicker(fill, callback) {
updateSelectedRect(fill); updateSelectedRect(fill);
openPicker.updateFill = function() { openPicker.updateFill = function () {
const selected = document.getElementById("picker").querySelector("rect.selected"); const selected = document.getElementById("picker").querySelector("rect.selected");
if (!selected) return; if (!selected) return;
callback(selected.getAttribute("fill")); callback(selected.getAttribute("fill"));
} };
} }
function setPickerControl(control, value, max) { function setPickerControl(control, value, max) {
@ -479,19 +567,20 @@ function getPickerControl(control, max) {
const min = +control.previousSibling.getAttribute("x1"); const min = +control.previousSibling.getAttribute("x1");
const delta = +control.previousSibling.getAttribute("x2") - min; const delta = +control.previousSibling.getAttribute("x2") - min;
const current = +control.getAttribute("cx") - min; const current = +control.getAttribute("cx") - min;
return current / delta * max; return (current / delta) * max;
} }
function dragPicker() { function dragPicker() {
const tr = parseTransform(this.getAttribute("transform")); const tr = parseTransform(this.getAttribute("transform"));
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y; const x = +tr[0] - d3.event.x,
y = +tr[1] - d3.event.y;
const picker = d3.select("#picker"); const picker = d3.select("#picker");
const bbox = picker.node().getBBox(); const bbox = picker.node().getBBox();
d3.event.on("drag", function() { d3.event.on("drag", function () {
const px = rn((x + d3.event.x + bbox.width) / svgWidth * 100, 2); const px = rn(((x + d3.event.x + bbox.width) / svgWidth) * 100, 2);
const py = rn((y + d3.event.y + bbox.height) / svgHeight * 100, 2); const py = rn(((y + d3.event.y + bbox.height) / svgHeight) * 100, 2);
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`; const transform = `translate(${x + d3.event.x},${y + d3.event.y})`;
picker.attr("transform", transform).attr("data-x", px).attr("data-y", py); picker.attr("transform", transform).attr("data-x", px).attr("data-y", py);
}); });
} }
@ -519,7 +608,7 @@ function dragPickerControl() {
const min = +this.previousSibling.getAttribute("x1"); const min = +this.previousSibling.getAttribute("x1");
const max = +this.previousSibling.getAttribute("x2"); const max = +this.previousSibling.getAttribute("x2");
d3.event.on("drag", function() { d3.event.on("drag", function () {
const x = Math.max(Math.min(d3.event.x, max), min); const x = Math.max(Math.min(d3.event.x, max), min);
this.setAttribute("cx", x); this.setAttribute("cx", x);
updateSpaces(); updateSpaces();
@ -530,16 +619,20 @@ function dragPickerControl() {
function changePickerSpace() { function changePickerSpace() {
const valid = this.checkValidity(); const valid = this.checkValidity();
if (!valid) {tip("You must provide a correct value", false, "error"); return;} if (!valid) {
tip("You must provide a correct value", false, "error");
return;
}
const space = this.dataset.space; const space = this.dataset.space;
const i = Array.from(this.parentNode.querySelectorAll("input")).map(input => input.value); // inputs const i = Array.from(this.parentNode.querySelectorAll("input")).map(input => input.value); // inputs
const fill = space === "hex" ? d3.rgb(this.value) const fill = space === "hex" ? d3.rgb(this.value) : space === "rgb" ? d3.rgb(i[0], i[1], i[2]) : d3.hsl(i[0], i[1] / 100, i[2] / 100);
: space === "rgb" ? d3.rgb(i[0], i[1], i[2])
: d3.hsl(i[0], i[1]/100, i[2]/100);
const hsl = d3.hsl(fill); const hsl = d3.hsl(fill);
if (isNaN(hsl.l)) {tip("You must provide a correct value", false, "error"); return;} if (isNaN(hsl.l)) {
tip("You must provide a correct value", false, "error");
return;
}
if (!isNaN(hsl.h)) setPickerControl(pickerH, hsl.h, 360); if (!isNaN(hsl.h)) setPickerControl(pickerH, hsl.h, 360);
if (!isNaN(hsl.s)) setPickerControl(pickerS, hsl.s, 1); if (!isNaN(hsl.s)) setPickerControl(pickerS, hsl.s, 1);
if (!isNaN(hsl.l)) setPickerControl(pickerL, hsl.l, 1); if (!isNaN(hsl.l)) setPickerControl(pickerL, hsl.l, 1);
@ -551,7 +644,7 @@ function changePickerSpace() {
// add fogging // add fogging
function fog(id, path) { function fog(id, path) {
if (defs.select("#fog #"+id).size()) return; if (defs.select("#fog #" + id).size()) return;
const fadeIn = d3.transition().duration(2000).ease(d3.easeSinInOut); const fadeIn = d3.transition().duration(2000).ease(d3.easeSinInOut);
if (defs.select("#fog path").size()) { if (defs.select("#fog path").size()) {
defs.select("#fog").append("path").attr("d", path).attr("id", id).attr("opacity", 0).transition(fadeIn).attr("opacity", 1); defs.select("#fog").append("path").attr("d", path).attr("id", id).attr("opacity", 0).transition(fadeIn).attr("opacity", 1);
@ -564,7 +657,7 @@ function fog(id, path) {
// remove fogging // remove fogging
function unfog(id) { function unfog(id) {
let el = defs.select("#fog #"+id); let el = defs.select("#fog #" + id);
if (!id || !el.size()) el = defs.select("#fog").selectAll("path"); if (!id || !el.size()) el = defs.select("#fog").selectAll("path");
el.remove(); el.remove();
@ -572,7 +665,7 @@ function unfog(id) {
} }
function getFileName(dataType) { function getFileName(dataType) {
const formatTime = time => time < 10 ? "0" + time : time; const formatTime = time => (time < 10 ? "0" + time : time);
const name = mapName.value; const name = mapName.value;
const type = dataType ? dataType + " " : ""; const type = dataType ? dataType + " " : "";
const date = new Date(); const date = new Date();
@ -581,7 +674,7 @@ function getFileName(dataType) {
const day = formatTime(date.getDate()); const day = formatTime(date.getDate());
const hour = formatTime(date.getHours()); const hour = formatTime(date.getHours());
const minutes = formatTime(date.getMinutes()); const minutes = formatTime(date.getMinutes());
const dateString = [year, month, day, hour, minutes].join('-'); const dateString = [year, month, day, hour, minutes].join("-");
return name + " " + type + dateString; return name + " " + type + dateString;
} }
@ -609,12 +702,9 @@ function highlightElement(element) {
const enter = d3.transition().duration(1000).ease(d3.easeBounceOut); const enter = d3.transition().duration(1000).ease(d3.easeBounceOut);
const exit = d3.transition().duration(500).ease(d3.easeLinear); const exit = d3.transition().duration(500).ease(d3.easeLinear);
const highlight = debug.append("rect").attr("x", box.x).attr("y", box.y) const highlight = debug.append("rect").attr("x", box.x).attr("y", box.y).attr("width", box.width).attr("height", box.height).attr("transform", transform);
.attr("width", box.width).attr("height", box.height).attr("transform", transform);
highlight.classed("highlighted", 1) highlight.classed("highlighted", 1).transition(enter).style("outline-offset", "0px").transition(exit).style("outline-color", "transparent").delay(1000).remove();
.transition(enter).style("outline-offset", "0px")
.transition(exit).style("outline-color", "transparent").delay(1000).remove();
const tr = parseTransform(transform); const tr = parseTransform(transform);
let x = box.x + box.width / 2; let x = box.x + box.width / 2;
@ -633,45 +723,239 @@ function selectIcon(initial, callback) {
input.value = initial; input.value = initial;
if (!table.innerHTML) { if (!table.innerHTML) {
const icons = ["⚔️","🏹","🐴","💣","🌊","🎯","⚓","🔮","📯","⚒️","🛡️","👑","⚜️", const icons = [
"☠️","🎆","🗡️","🔪","⛏️","🔥","🩸","💧","🐾","🎪","🏰","🏯","⛓️","❤️","💘","💜","📜","🔔", "⚔️",
"🔱","💎","🌈","🌠","✨","💥","☀️","🌙","⚡","❄️","♨️","🎲","🚨","🌉","🗻","🌋","🧱", "🏹",
"⚖️","✂️","🎵","👗","🎻","🎨","🎭","⛲","💉","📖","📕","🎁","💍","⏳","🕸️","⚗️","☣️","☢️", "🐴",
"🔰","🎖️","🚩","🏳️","🏴","💪","✊","👊","🤜","🤝","🙏","🧙","🧙‍♀️","💂","🤴","🧛","🧟","🧞","🧝","👼", "💣",
"👻","👺","👹","🦄","🐲","🐉","🐎","🦓","🐺","🦊","🐱","🐈","🦁","🐯","🐅","🐆","🐕","🦌","🐵","🐒","🦍", "🌊",
"🦅","🕊️","🐓","🦇","🦜","🐦","🦉","🐮","🐄","🐂","🐃","🐷","🐖","🐗","🐏","🐑","🐐","🐫","🦒","🐘","🦏","🐭","🐁","🐀", "🎯",
"🐹","🐰","🐇","🦔","🐸","🐊","🐢","🦎","🐍","🐳","🐬","🦈","🐠","🐙","🦑","🐌","🦋","🐜","🐝","🐞","🦗","🕷️","🦂","🦀", "⚓",
"🌳","🌲","🎄","🌴","🍂","🍁","🌵","☘️","🍀","🌿","🌱","🌾","🍄","🌽","🌸","🌹","🌻", "🔮",
"🍒","🍏","🍇","🍉","🍅","🍓","🥔","🥕","🥩","🍗","🍞","🍻","🍺","🍲","🍷" "📯",
"⚒️",
"🛡️",
"👑",
"⚜️",
"☠️",
"🎆",
"🗡️",
"🔪",
"⛏️",
"🔥",
"🩸",
"💧",
"🐾",
"🎪",
"🏰",
"🏯",
"⛓️",
"❤️",
"💘",
"💜",
"📜",
"🔔",
"🔱",
"💎",
"🌈",
"🌠",
"✨",
"💥",
"☀️",
"🌙",
"⚡",
"❄️",
"♨️",
"🎲",
"🚨",
"🌉",
"🗻",
"🌋",
"🧱",
"⚖️",
"✂️",
"🎵",
"👗",
"🎻",
"🎨",
"🎭",
"⛲",
"💉",
"📖",
"📕",
"🎁",
"💍",
"⏳",
"🕸️",
"⚗️",
"☣️",
"☢️",
"🔰",
"🎖️",
"🚩",
"🏳️",
"🏴",
"💪",
"✊",
"👊",
"🤜",
"🤝",
"🙏",
"🧙",
"🧙‍♀️",
"💂",
"🤴",
"🧛",
"🧟",
"🧞",
"🧝",
"👼",
"👻",
"👺",
"👹",
"🦄",
"🐲",
"🐉",
"🐎",
"🦓",
"🐺",
"🦊",
"🐱",
"🐈",
"🦁",
"🐯",
"🐅",
"🐆",
"🐕",
"🦌",
"🐵",
"🐒",
"🦍",
"🦅",
"🕊️",
"🐓",
"🦇",
"🦜",
"🐦",
"🦉",
"🐮",
"🐄",
"🐂",
"🐃",
"🐷",
"🐖",
"🐗",
"🐏",
"🐑",
"🐐",
"🐫",
"🦒",
"🐘",
"🦏",
"🐭",
"🐁",
"🐀",
"🐹",
"🐰",
"🐇",
"🦔",
"🐸",
"🐊",
"🐢",
"🦎",
"🐍",
"🐳",
"🐬",
"🦈",
"🐠",
"🐙",
"🦑",
"🐌",
"🦋",
"🐜",
"🐝",
"🐞",
"🦗",
"🕷️",
"🦂",
"🦀",
"🌳",
"🌲",
"🎄",
"🌴",
"🍂",
"🍁",
"🌵",
"☘️",
"🍀",
"🌿",
"🌱",
"🌾",
"🍄",
"🌽",
"🌸",
"🌹",
"🌻",
"🍒",
"🍏",
"🍇",
"🍉",
"🍅",
"🍓",
"🥔",
"🥕",
"🥩",
"🍗",
"🍞",
"🍻",
"🍺",
"🍲",
"🍷"
]; ];
let row = ""; let row = "";
for (let i=0; i < icons.length; i++) { for (let i = 0; i < icons.length; i++) {
if (i%17 === 0) row = table.insertRow(i/17|0); if (i % 17 === 0) row = table.insertRow((i / 17) | 0);
const cell = row.insertCell(i%17); const cell = row.insertCell(i % 17);
cell.innerHTML = icons[i]; cell.innerHTML = icons[i];
} }
} }
table.onclick = e => {if (e.target.tagName === "TD") {input.value = e.target.innerHTML; callback(input.value)}}; table.onclick = e => {
table.onmouseover = e => {if (e.target.tagName === "TD") tip(`Click to select ${e.target.innerHTML} icon`)}; if (e.target.tagName === "TD") {
input.value = e.target.innerHTML;
callback(input.value);
}
};
table.onmouseover = e => {
if (e.target.tagName === "TD") tip(`Click to select ${e.target.innerHTML} icon`);
};
$("#iconSelector").dialog({width: fitContent(), title: "Select Icon", $("#iconSelector").dialog({
buttons: { width: fitContent(),
Apply: function() {callback(input.value||""); $(this).dialog("close")}, title: "Select Icon",
Close: function() {callback(initial); $(this).dialog("close")}} buttons: {
Apply: function () {
callback(input.value || "");
$(this).dialog("close");
},
Close: function () {
callback(initial);
$(this).dialog("close");
}
}
}); });
} }
// Calls the refresh functionality on all editors currently open. // Calls the refresh functionality on all editors currently open.
function refreshAllEditors() { function refreshAllEditors() {
TIME && console.time('refreshAllEditors'); TIME && console.time("refreshAllEditors");
if (document.getElementById('culturesEditorRefresh').offsetParent) culturesEditorRefresh.click(); if (document.getElementById("culturesEditorRefresh").offsetParent) culturesEditorRefresh.click();
if (document.getElementById('biomesEditorRefresh').offsetParent) biomesEditorRefresh.click(); if (document.getElementById("biomesEditorRefresh").offsetParent) biomesEditorRefresh.click();
if (document.getElementById('diplomacyEditorRefresh').offsetParent) diplomacyEditorRefresh.click(); if (document.getElementById("diplomacyEditorRefresh").offsetParent) diplomacyEditorRefresh.click();
if (document.getElementById('provincesEditorRefresh').offsetParent) provincesEditorRefresh.click(); if (document.getElementById("provincesEditorRefresh").offsetParent) provincesEditorRefresh.click();
if (document.getElementById('religionsEditorRefresh').offsetParent) religionsEditorRefresh.click(); if (document.getElementById("religionsEditorRefresh").offsetParent) religionsEditorRefresh.click();
if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click(); if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
if (document.getElementById('zonesEditorRefresh').offsetParent) zonesEditorRefresh.click(); if (document.getElementById("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click();
TIME && console.timeEnd('refreshAllEditors'); TIME && console.timeEnd("refreshAllEditors");
} }

View file

@ -96,10 +96,7 @@ function showMapTooltip(point, e, i, g) {
const land = pack.cells.h[i] >= 20; const land = pack.cells.h[i] >= 20;
// specific elements // specific elements
if (group === "armies") { if (group === "armies") return tip(e.target.parentNode.dataset.name + ". Click to edit");
tip(e.target.parentNode.dataset.name + ". Click to edit");
return;
}
if (group === "emblems" && e.target.tagName === "use") { if (group === "emblems" && e.target.tagName === "use") {
const parent = e.target.parentNode; const parent = e.target.parentNode;
@ -123,14 +120,11 @@ function showMapTooltip(point, e, i, g) {
if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000); if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000);
return; return;
} }
if (group === "routes") {
tip("Click to edit the Route"); if (group === "routes") return tip("Click to edit the Route");
return;
} if (group === "terrain") return tip("Click to edit the Relief Icon");
if (group === "terrain") {
tip("Click to edit the Relief Icon");
return;
}
if (subgroup === "burgLabels" || subgroup === "burgIcons") { if (subgroup === "burgLabels" || subgroup === "burgIcons") {
const burg = +path[path.length - 10].dataset.id; const burg = +path[path.length - 10].dataset.id;
const b = pack.burgs[burg]; const b = pack.burgs[burg];
@ -139,50 +133,25 @@ function showMapTooltip(point, e, i, g) {
if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000); if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
return; return;
} }
if (group === "labels") { if (group === "labels") return tip("Click to edit the Label");
tip("Click to edit the Label");
return; if (group === "markers") return tip("Click to edit the Marker");
}
if (group === "markers") {
tip("Click to edit the Marker");
return;
}
if (group === "ruler") { if (group === "ruler") {
const tag = e.target.tagName; const tag = e.target.tagName;
const className = e.target.getAttribute("class"); const className = e.target.getAttribute("class");
if (tag === "circle" && className === "edge") { if (tag === "circle" && className === "edge") return tip("Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point");
tip("Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point"); if (tag === "circle" && className === "control") return tip("Drag to adjust. Hold Shift and drag to keep axial direction. Click to remove the point");
return; if (tag === "circle") return tip("Drag to adjust the measurer");
} if (tag === "polyline") return tip("Click on drag to add a control point");
if (tag === "circle" && className === "control") { if (tag === "path") return tip("Drag to move the measurer");
tip("Drag to adjust. Hold Shifta and drag to keep axial direction. Click to remove the point"); if (tag === "text") return tip("Drag to move, click to remove the measurer");
return;
}
if (tag === "circle") {
tip("Drag to adjust the measurer");
return;
}
if (tag === "polyline") {
tip("Click on drag to add a control point");
return;
}
if (tag === "path") {
tip("Drag to move the measurer");
return;
}
if (tag === "text") {
tip("Drag to move, click to remove the measurer");
return;
}
}
if (subgroup === "burgIcons") {
tip("Click to edit the Burg");
return;
}
if (subgroup === "burgLabels") {
tip("Click to edit the Burg");
return;
} }
if (subgroup === "burgIcons") return tip("Click to edit the Burg");
if (subgroup === "burgLabels") return tip("Click to edit the Burg");
if (group === "lakes" && !land) { if (group === "lakes" && !land) {
const lakeId = +e.target.dataset.f; const lakeId = +e.target.dataset.f;
const name = pack.features[lakeId]?.name; const name = pack.features[lakeId]?.name;
@ -190,20 +159,16 @@ function showMapTooltip(point, e, i, g) {
tip(`${fullName} lake. Click to edit`); tip(`${fullName} lake. Click to edit`);
return; return;
} }
if (group === "coastline") { if (group === "coastline") return tip("Click to edit the coastline");
tip("Click to edit the coastline");
return;
}
if (group === "zones") { if (group === "zones") {
const zone = path[path.length - 8]; const zone = path[path.length - 8];
tip(zone.dataset.description); tip(zone.dataset.description);
if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000); if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000);
return; return;
} }
if (group === "ice") {
tip("Click to edit the Ice"); if (group === "ice") return tip("Click to edit the Ice");
return;
}
// covering elements // covering elements
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: " + getFriendlyPrecipitation(i)); if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: " + getFriendlyPrecipitation(i));

View file

@ -197,6 +197,7 @@ function editHeightmap() {
} }
} }
drawRivers();
Lakes.defineGroup(); Lakes.defineGroup();
defineBiomes(); defineBiomes();
rankCells(); rankCells();

View file

@ -875,7 +875,6 @@ function toggleStates(event) {
} }
} }
// draw states
function drawStates() { function drawStates() {
TIME && console.time("drawStates"); TIME && console.time("drawStates");
regions.selectAll("path").remove(); regions.selectAll("path").remove();
@ -1015,6 +1014,21 @@ function drawStates() {
TIME && console.timeEnd("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 // draw state and province borders
function drawBorders() { function drawBorders() {
TIME && console.time("drawBorders"); TIME && console.time("drawBorders");
@ -1118,21 +1132,6 @@ function drawBorders() {
TIME && console.timeEnd("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) { function toggleProvinces(event) {
if (!layerIsOn("toggleProvinces")) { if (!layerIsOn("toggleProvinces")) {
turnButtonOn("toggleProvinces"); turnButtonOn("toggleProvinces");
@ -1444,18 +1443,30 @@ function toggleTexture(event) {
function toggleRivers(event) { function toggleRivers(event) {
if (!layerIsOn("toggleRivers")) { if (!layerIsOn("toggleRivers")) {
turnButtonOn("toggleRivers"); turnButtonOn("toggleRivers");
$("#rivers").fadeIn(); drawRivers();
if (event && isCtrlClick(event)) editStyle("rivers"); if (event && isCtrlClick(event)) editStyle("rivers");
} else { } else {
if (event && isCtrlClick(event)) { if (event && isCtrlClick(event)) return editStyle("rivers");
editStyle("rivers"); rivers.selectAll("*").remove();
return;
}
$("#rivers").fadeOut();
turnButtonOff("toggleRivers"); turnButtonOff("toggleRivers");
} }
} }
function drawRivers() {
TIME && console.time("drawRivers");
const {addMeandering, getRiverPath} = Rivers;
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const riverPaths = pack.rivers.map(river => {
const meanderedPoints = addMeandering(river.cells, river.points);
const widthFactor = river.widthFactor || 1;
const startingWidth = river.sourceWidth || 0;
const path = getRiverPath(meanderedPoints, widthFactor, startingWidth);
return `<path id="river${river.i}" d="${path}"/>`;
});
rivers.html(riverPaths.join(""));
TIME && console.timeEnd("drawRivers");
}
function toggleRoutes(event) { function toggleRoutes(event) {
if (!layerIsOn("toggleRoutes")) { if (!layerIsOn("toggleRoutes")) {
turnButtonOn("toggleRoutes"); turnButtonOn("toggleRoutes");

View file

@ -98,7 +98,7 @@ function showSupporters() {
PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram, PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram,
Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee, Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,
Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth, Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,
Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 ,Chris Gray`; Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 ,Chris Gray,Phoenix Boatwright`;
const array = supporters const array = supporters
.replace(/(?:\r\n|\r|\n)/g, "") .replace(/(?:\r\n|\r|\n)/g, "")
@ -777,6 +777,12 @@ document
.forEach(el => el.addEventListener("input", updateTilesOptions)); .forEach(el => el.addEventListener("input", updateTilesOptions));
function updateTilesOptions() { function updateTilesOptions() {
if (this?.tagName === "INPUT") {
const {nextElementSibling: next, previousElementSibling: prev} = this;
if (next?.tagName === "INPUT") next.value = this.value;
if (prev?.tagName === "INPUT") prev.value = this.value;
}
const tileSize = document.getElementById("tileSize"); const tileSize = document.getElementById("tileSize");
const tilesX = +document.getElementById("tileColsOutput").value; const tilesX = +document.getElementById("tileColsOutput").value;
const tilesY = +document.getElementById("tileRowsOutput").value; const tilesY = +document.getElementById("tileRowsOutput").value;

View file

@ -0,0 +1,125 @@
"use strict";
function createRiver() {
if (customization) return;
closeDialogs();
if (!layerIsOn("toggleRivers")) toggleRivers();
document.getElementById("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
if (!layerIsOn("toggleCells")) toggleCells();
tip("Click to add river point, click again to remove", true);
debug.append("g").attr("id", "controlCells");
viewbox.style("cursor", "crosshair").on("click", onCellClick);
createRiver.cells = [];
const body = document.getElementById("riverCreatorBody");
$("#riverCreator").dialog({
title: "Create River",
resizable: false,
position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeRiverCreator
});
if (modules.createRiver) return;
modules.createRiver = true;
// add listeners
document.getElementById("riverCreatorComplete").addEventListener("click", addRiver);
document.getElementById("riverCreatorCancel").addEventListener("click", () => $("#riverCreator").dialog("close"));
body.addEventListener("click", function (ev) {
const el = ev.target;
const cl = el.classList;
const cell = +el.parentNode.dataset.cell;
if (cl.contains("editFlux")) pack.cells.fl[cell] = +el.value;
else if (cl.contains("icon-trash-empty")) removeCell(cell);
});
function onCellClick() {
const cell = findCell(...d3.mouse(this));
if (createRiver.cells.includes(cell)) removeCell(cell);
else addCell(cell);
}
function addCell(cell) {
createRiver.cells.push(cell);
drawCells(createRiver.cells);
const flux = pack.cells.fl[cell];
const line = `<div class="editorLine" data-cell="${cell}">
<span>Cell ${cell}</span>
<span data-tip="Set flux affects river width" style="margin-left: 0.4em">Flux</span>
<input type="number" min=0 value="${flux}" class="editFlux" style="width: 5em"/>
<span data-tip="Remove the cell" class="icon-trash-empty pointer"></span>
</div>`;
body.innerHTML += line;
}
function removeCell(cell) {
createRiver.cells = createRiver.cells.filter(c => c !== cell);
drawCells(createRiver.cells);
body.querySelector(`div[data-cell='${cell}']`)?.remove();
}
function drawCells(cells) {
debug
.select("#controlCells")
.selectAll(`polygon`)
.data(cells)
.join("polygon")
.attr("points", d => getPackPolygon(d))
.attr("class", "current");
}
function addRiver() {
const {rivers, cells} = pack;
const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin} = Rivers;
const riverCells = createRiver.cells;
if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error");
const riverId = last(rivers).i + 1;
const parent = cells.r[last(riverCells)] || riverId;
riverCells.forEach(cell => {
if (!cells.r[cell]) cells.r[cell] = riverId;
});
const source = riverCells[0];
const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2];
const sourceWidth = 0.05;
const widthFactor = 1.2;
const meanderedPoints = addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
const length = getApproximateLength(meanderedPoints);
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
const name = getName(mouth);
const basin = getBasin(parent);
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type: "River"});
// render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
viewbox
.select("#rivers")
.append("path")
.attr("id", "river" + riverId)
.attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
editRiver(riverId);
}
function closeRiverCreator() {
body.innerHTML = "";
debug.select("#controlCells").remove();
restoreDefaultEvents();
clearMainTip();
const forced = +document.getElementById("toggleCells").dataset.forced;
document.getElementById("toggleCells").dataset.forced = 0;
if (forced && layerIsOn("toggleCells")) toggleCells();
}
}

View file

@ -1,20 +1,31 @@
"use strict"; "use strict";
function editRiver(id) { function editRiver(id) {
if (customization) return; if (customization) return;
if (elSelected && d3.event && d3.event.target.id === elSelected.attr("id")) return; if (elSelected && id === elSelected.attr("id")) return;
closeDialogs(".stable"); closeDialogs(".stable");
if (!layerIsOn("toggleRivers")) toggleRivers(); if (!layerIsOn("toggleRivers")) toggleRivers();
const node = id ? document.getElementById(id) : d3.event.target; document.getElementById("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
elSelected = d3.select(node).on("click", addInterimControlPoint); if (!layerIsOn("toggleCells")) toggleCells();
viewbox.on("touchmove mousemove", showEditorTips);
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform")); elSelected = d3.select("#" + id);
tip("Drag control points to change the river course. For major changes please create a new river instead", true);
debug.append("g").attr("id", "controlCells");
debug.append("g").attr("id", "controlPoints");
updateRiverData(); updateRiverData();
drawControlPoints(node);
const river = getRiver();
const {cells, points} = river;
const riverPoints = Rivers.getRiverPoints(cells, points);
drawControlPoints(riverPoints, cells);
drawCells(cells, "current");
$("#riverEditor").dialog({ $("#riverEditor").dialog({
title: "Edit River", resizable: false, title: "Edit River",
position: {my: "center top+80", at: "top", of: node, collision: "fit"}, resizable: false,
position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeRiverEditor close: closeRiverEditor
}); });
@ -22,27 +33,19 @@ function editRiver(id) {
modules.editRiver = true; modules.editRiver = true;
// add listeners // add listeners
document.getElementById("riverCreateSelectingCells").addEventListener("click", createRiver);
document.getElementById("riverEditStyle").addEventListener("click", () => editStyle("rivers"));
document.getElementById("riverElevationProfile").addEventListener("click", showElevationProfile);
document.getElementById("riverLegend").addEventListener("click", editRiverLegend);
document.getElementById("riverRemove").addEventListener("click", removeRiver);
document.getElementById("riverName").addEventListener("input", changeName); document.getElementById("riverName").addEventListener("input", changeName);
document.getElementById("riverType").addEventListener("input", changeType); document.getElementById("riverType").addEventListener("input", changeType);
document.getElementById("riverNameCulture").addEventListener("click", generateNameCulture); document.getElementById("riverNameCulture").addEventListener("click", generateNameCulture);
document.getElementById("riverNameRandom").addEventListener("click", generateNameRandom); document.getElementById("riverNameRandom").addEventListener("click", generateNameRandom);
document.getElementById("riverMainstem").addEventListener("change", changeParent); document.getElementById("riverMainstem").addEventListener("change", changeParent);
document.getElementById("riverSourceWidth").addEventListener("input", changeSourceWidth); document.getElementById("riverSourceWidth").addEventListener("input", changeSourceWidth);
document.getElementById("riverWidthFactor").addEventListener("input", changeWidthFactor); document.getElementById("riverWidthFactor").addEventListener("input", changeWidthFactor);
document.getElementById("riverNew").addEventListener("click", toggleRiverCreationMode);
document.getElementById("riverEditStyle").addEventListener("click", () => editStyle("rivers"));
document.getElementById("riverElevationProfile").addEventListener("click", showElevationProfile);
document.getElementById("riverLegend").addEventListener("click", editRiverLegend);
document.getElementById("riverRemove").addEventListener("click", removeRiver);
function showEditorTips() {
showMainTip();
if (d3.event.target.parentNode.id === elSelected.attr("id")) tip("Drag to move, click to add a control point"); else
if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point");
}
function getRiver() { function getRiver() {
const riverId = +elSelected.attr("id").slice(5); const riverId = +elSelected.attr("id").slice(5);
const river = pack.rivers.find(r => r.i === riverId); const river = pack.rivers.find(r => r.i === riverId);
@ -58,7 +61,7 @@ function editRiver(id) {
const parentSelect = document.getElementById("riverMainstem"); const parentSelect = document.getElementById("riverMainstem");
parentSelect.options.length = 0; parentSelect.options.length = 0;
const parent = r.parent || r.i; const parent = r.parent || r.i;
const sortedRivers = pack.rivers.slice().sort((a, b) => a.name > b.name ? 1 : -1); const sortedRivers = pack.rivers.slice().sort((a, b) => (a.name > b.name ? 1 : -1));
sortedRivers.forEach(river => { sortedRivers.forEach(river => {
const opt = new Option(river.name, river.i, false, river.i === parent); const opt = new Option(river.name, river.i, false, river.i === parent);
parentSelect.options.add(opt); parentSelect.options.add(opt);
@ -66,85 +69,112 @@ function editRiver(id) {
document.getElementById("riverBasin").value = pack.rivers.find(river => river.i === r.basin).name; document.getElementById("riverBasin").value = pack.rivers.find(river => river.i === r.basin).name;
document.getElementById("riverDischarge").value = r.discharge + " m³/s"; document.getElementById("riverDischarge").value = r.discharge + " m³/s";
r.length = elSelected.node().getTotalLength() / 2;
const length = rn(r.length * distanceScaleInput.value) + " " + distanceUnitInput.value;
document.getElementById("riverLength").value = length;
const width = rn(r.width * distanceScaleInput.value, 3) + " " + distanceUnitInput.value;
document.getElementById("riverWidth").value = width;
document.getElementById("riverSourceWidth").value = r.sourceWidth; document.getElementById("riverSourceWidth").value = r.sourceWidth;
document.getElementById("riverWidthFactor").value = r.widthFactor; document.getElementById("riverWidthFactor").value = r.widthFactor;
updateRiverLength(r);
updateRiverWidth(r);
} }
function drawControlPoints(node) { function updateRiverLength(river) {
const length = getRiver().length; river.length = rn(elSelected.node().getTotalLength() / 2, 2);
const segments = Math.ceil(length / 4); const length = `${river.length * distanceScaleInput.value} ${distanceUnitInput.value}`;
const increment = rn(length / segments * 1e5); document.getElementById("riverLength").value = length;
for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i / 1e5);
const p2 = node.getPointAtLength(c / 1e5);
addControlPoint([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2]);
}
} }
function addControlPoint(point, before = null) { function updateRiverWidth(river) {
debug.select("#controlPoints").insert("circle", before) const {addMeandering, getWidth, getOffset} = Rivers;
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .6) const {cells, discharge, widthFactor, sourceWidth} = river;
.call(d3.drag().on("drag", dragControlPoint)) const meanderedPoints = addMeandering(cells);
.on("click", clickControlPoint); river.width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
const width = `${rn(river.width * distanceScaleInput.value, 3)} ${distanceUnitInput.value}`;
document.getElementById("riverWidth").value = width;
}
function drawControlPoints(points, cells) {
debug
.select("#controlPoints")
.selectAll("circle")
.data(points)
.enter()
.append("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 0.6)
.attr("data-cell", (d, i) => cells[i])
.attr("data-i", (d, i) => i)
.call(d3.drag().on("start", dragControlPoint));
}
function drawCells(cells, type) {
debug
.select("#controlCells")
.selectAll(`polygon.${type}`)
.data(cells)
.join("polygon")
.attr("points", d => getPackPolygon(d))
.attr("class", type);
} }
function dragControlPoint() { function dragControlPoint() {
this.setAttribute("cx", d3.event.x); const {i, r, fl} = pack.cells;
this.setAttribute("cy", d3.event.y); const river = getRiver();
redrawRiver();
const initCell = +this.dataset.cell;
const index = +this.dataset.i;
const occupiedCells = i.filter(i => r[i] && !river.cells.includes(i));
drawCells(occupiedCells, "occupied");
let movedToCell = null;
d3.event.on("drag", function () {
const {x, y} = d3.event;
const currentCell = findCell(x, y);
if (initCell !== currentCell) {
if (occupiedCells.includes(currentCell)) return;
movedToCell = currentCell;
} else movedToCell = null;
this.setAttribute("cx", x);
this.setAttribute("cy", y);
this.__data__ = [rn(x, 1), rn(y, 1)];
redrawRiver();
});
d3.event.on("end", () => {
if (movedToCell) {
this.dataset.cell = movedToCell;
river.cells[index] = movedToCell;
drawCells(river.cells, "current");
// swap river data
r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
}
debug.select("#controlCells").selectAll("polygon.available, polygon.occupied").remove();
});
} }
function redrawRiver() { function redrawRiver() {
const points = []; const river = getRiver();
debug.select("#controlPoints").selectAll("circle").each(function() { river.points = debug.selectAll("#controlPoints > *").data();
points.push([+this.getAttribute("cx"), +this.getAttribute("cy")]); const {cells, widthFactor, sourceWidth} = river;
}); const meanderedPoints = Rivers.addMeandering(cells, river.points);
if (points.length < 2) return; lineGen.curve(d3.curveCatmullRom.alpha(0.1));
if (points.length === 2) { const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
const p0 = points[0], p1 = points[1];
const angle = Math.atan2(p1[1] - p0[1], p1[0] - p0[0]);
const sin = Math.sin(angle), cos = Math.cos(angle);
elSelected.attr("d", `M${p0[0]},${p0[1]} L${p1[0]},${p1[1]} l${-sin/2},${cos/2} Z`);
return;
}
const widthFactor = +document.getElementById("riverWidthFactor").value;
const sourceWidth = +document.getElementById("riverSourceWidth").value;
const [path, length, offset] = Rivers.getPath(points, widthFactor, sourceWidth);
elSelected.attr("d", path); elSelected.attr("d", path);
const r = getRiver(); updateRiverLength(river);
if (r) {
r.width = rn(offset ** 2, 2);
r.length = length;
updateRiverData();
}
if (modules.elevation) showEPForRiver(elSelected.node()); if (modules.elevation) showEPForRiver(elSelected.node());
} }
function clickControlPoint() {
this.remove();
redrawRiver();
}
function addInterimControlPoint() {
const point = d3.mouse(this);
const controls = document.getElementById("controlPoints").querySelectorAll("circle");
const points = Array.from(controls).map(circle => [+circle.getAttribute("cx"), +circle.getAttribute("cy")]);
const index = getSegmentId(points, point, 2);
addControlPoint(point, ":nth-child(" + (index+1) + ")");
redrawRiver();
}
function changeName() { function changeName() {
getRiver().name = this.value; getRiver().name = this.value;
} }
@ -160,7 +190,7 @@ function editRiver(id) {
function generateNameRandom() { function generateNameRandom() {
const r = getRiver(); const r = getRiver();
if (r) r.name = riverName.value = Names.getBase(rand(nameBases.length-1)); if (r) r.name = riverName.value = Names.getBase(rand(nameBases.length - 1));
} }
function changeParent() { function changeParent() {
@ -171,12 +201,16 @@ function editRiver(id) {
} }
function changeSourceWidth() { function changeSourceWidth() {
getRiver().sourceWidth = +this.value; const river = getRiver();
river.sourceWidth = +this.value;
updateRiverWidth(river);
redrawRiver(); redrawRiver();
} }
function changeWidthFactor() { function changeWidthFactor() {
getRiver().widthFactor = +this.value; const river = getRiver();
river.widthFactor = +this.value;
updateRiverWidth(river);
redrawRiver(); redrawRiver();
} }
@ -191,83 +225,35 @@ function editRiver(id) {
editNotes(id, river.name + " " + river.type); editNotes(id, river.name + " " + river.type);
} }
function toggleRiverCreationMode() {
if (document.getElementById("riverNew").classList.contains("pressed")) exitRiverCreationMode();
else {
document.getElementById("riverNew").classList.add("pressed");
tip("Click on map to add control points", true, "warn");
viewbox.on("click", addPointOnClick).style("cursor", "crosshair");
elSelected.on("click", null);
}
}
function addPointOnClick() {
if (!elSelected.attr("data-new")) {
debug.select("#controlPoints").selectAll("circle").remove();
const id = getNextId("river");
elSelected = d3.select(elSelected.node().parentNode).append("path").attr("id", id).attr("data-new", 1);
}
// add control point
const point = d3.mouse(this);
addControlPoint([point[0], point[1]]);
redrawRiver();
}
function exitRiverCreationMode() {
riverNew.classList.remove("pressed");
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
elSelected.on("click", addInterimControlPoint);
if (!elSelected.attr("data-new")) return; // no need to create a new river
elSelected.attr("data-new", null);
// add a river
const r = +elSelected.attr("id").slice(5);
const node = elSelected.node(), length = node.getTotalLength() / 2;
const cells = [];
const segments = Math.ceil(length / 4), increment = rn(length / segments * 1e5);
for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) {
const p = node.getPointAtLength(i / 1e5);
const cell = findCell(p.x, p.y);
if (!pack.cells.r[cell]) pack.cells.r[cell] = r;
cells.push(cell);
}
const source = cells[0], mouth = last(cells);
const name = Rivers.getName(mouth);
const smallLength = pack.rivers.map(r => r.length||0).sort((a,b) => a-b)[Math.ceil(pack.rivers.length * .15)];
const type = length < smallLength ? rw({"Creek":9, "River":3, "Brook":3, "Stream":1}) : "River";
const discharge = rn(cells.length * 20 * Math.random());
const widthFactor = +document.getElementById("riverWidthFactor").value;
const sourceWidth = +document.getElementById("riverSourceWidth").value;
pack.rivers.push({i:r, source, mouth, discharge, length, width: sourceWidth, widthFactor, sourceWidth, parent:0, name, type, basin:r});
}
function removeRiver() { function removeRiver() {
alertMessage.innerHTML = "Are you sure you want to remove the river? All tributaries will be auto-removed"; alertMessage.innerHTML = "Are you sure you want to remove the river and all its tributaries";
$("#alert").dialog({resizable: false, width: "22em", title: "Remove river", $("#alert").dialog({
resizable: false,
width: "22em",
title: "Remove river and tributaries",
buttons: { buttons: {
Remove: function() { Remove: function () {
$(this).dialog("close"); $(this).dialog("close");
const river = +elSelected.attr("id").slice(5); const river = +elSelected.attr("id").slice(5);
Rivers.remove(river); Rivers.remove(river);
elSelected.remove(); // keep if river if missed in pack.rivers elSelected.remove();
$("#riverEditor").dialog("close"); $("#riverEditor").dialog("close");
}, },
Cancel: function() {$(this).dialog("close");} Cancel: function () {
$(this).dialog("close");
}
} }
}); });
} }
function closeRiverEditor() { function closeRiverEditor() {
exitRiverCreationMode();
elSelected.on("click", null);
debug.select("#controlPoints").remove(); debug.select("#controlPoints").remove();
debug.select("#controlCells").remove();
unselect(); unselect();
clearMainTip();
const forced = +document.getElementById("toggleCells").dataset.forced;
document.getElementById("toggleCells").dataset.forced = 0;
if (forced && layerIsOn("toggleCells")) toggleCells();
} }
} }

View file

@ -21,6 +21,7 @@ function overviewRivers() {
// add listeners // add listeners
document.getElementById("riversOverviewRefresh").addEventListener("click", riversOverviewAddLines); document.getElementById("riversOverviewRefresh").addEventListener("click", riversOverviewAddLines);
document.getElementById("addNewRiver").addEventListener("click", toggleAddRiver); document.getElementById("addNewRiver").addEventListener("click", toggleAddRiver);
document.getElementById("riverCreateNew").addEventListener("click", createRiver);
document.getElementById("riversBasinHighlight").addEventListener("click", toggleBasinsHightlight); document.getElementById("riversBasinHighlight").addEventListener("click", toggleBasinsHightlight);
document.getElementById("riversExport").addEventListener("click", downloadRiversData); document.getElementById("riversExport").addEventListener("click", downloadRiversData);
document.getElementById("riversRemoveAll").addEventListener("click", triggerAllRiversRemove); document.getElementById("riversRemoveAll").addEventListener("click", triggerAllRiversRemove);
@ -129,7 +130,8 @@ function overviewRivers() {
} }
function openRiverEditor() { function openRiverEditor() {
editRiver("river" + this.parentNode.dataset.id); const id = "river" + this.parentNode.dataset.id;
editRiver(id);
} }
function triggerRiverRemove() { function triggerRiverRemove() {

View file

@ -531,22 +531,22 @@ function toggleAddRiver() {
function addRiverOnClick() { function addRiverOnClick() {
const {cells, rivers} = pack; const {cells, rivers} = pack;
const point = d3.mouse(this); let i = findCell(...d3.mouse(this));
let i = findCell(point[0], point[1]);
if (cells.r[i]) return tip("There already a river here", false, "error"); if (cells.r[i]) return tip("There is already a river here", false, "error");
if (cells.h[i] < 20) return tip("Cannot create river in water cell", false, "error"); if (cells.h[i] < 20) return tip("Cannot create river in water cell", false, "error");
if (cells.b[i]) return; if (cells.b[i]) return;
const {alterHeights, resolveDepressions, addMeandering, getRiverPath, getBasin, getName, getType, getWidth, getOffset, getApproximateLength} = Rivers;
const riverCells = []; const riverCells = [];
let riverId = +getNextId("river").slice(5); let riverId = last(rivers).i + 1;
let parent = 0; let parent = riverId;
const initialFlux = grid.cells.prec[cells.g[i]]; const initialFlux = grid.cells.prec[cells.g[i]];
cells.fl[i] = initialFlux; cells.fl[i] = initialFlux;
const h = Rivers.alterHeights(); const h = alterHeights();
Rivers.resolveDepressions(h); resolveDepressions(h);
while (i) { while (i) {
cells.r[i] = riverId; cells.r[i] = riverId;
@ -555,15 +555,13 @@ function addRiverOnClick() {
const min = cells.c[i].sort((a, b) => h[a] - h[b])[0]; // downhill cell const min = cells.c[i].sort((a, b) => h[a] - h[b])[0]; // downhill cell
if (h[i] <= h[min]) return tip(`Cell ${i} is depressed, river cannot flow further`, false, "error"); if (h[i] <= h[min]) return tip(`Cell ${i} is depressed, river cannot flow further`, false, "error");
const [tx, ty] = cells.p[min];
// pour to water body // pour to water body
if (h[min] < 20) { if (h[min] < 20) {
riverCells.push(min); riverCells.push(min);
const feature = pack.features[cells.f[min]]; const feature = pack.features[cells.f[min]];
if (feature.type === "lake") { if (feature.type === "lake") {
parent = feature.outlet || 0; if (feature.outlet) parent = feature.outlet;
feature.inlets ? feature.inlets.push(riverId) : (feature.inlets = [riverId]); feature.inlets ? feature.inlets.push(riverId) : (feature.inlets = [riverId]);
} }
break; break;
@ -615,22 +613,15 @@ function addRiverOnClick() {
} }
const river = rivers.find(r => r.i === riverId); const river = rivers.find(r => r.i === riverId);
const sourceWidth = 0.1;
const widthFactor = river?.widthFactor || rn(0.8 + Math.random() * 0.4, 1);
const riverMeandered = Rivers.addMeandering(riverCells, sourceWidth * 10, 0.5);
const [path, length, offset] = Rivers.getPath(riverMeandered, widthFactor, sourceWidth);
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 source = riverCells[0];
const mouth = last(riverCells); const mouth = riverCells[riverCells.length - 2];
const discharge = cells.fl[mouth]; // in m3/s const widthFactor = river?.widthFactor || (!parent || parent === riverId ? 1.2 : 1);
const width = rn(offset ** 2, 2); // mounth 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));
if (river) { if (river) {
river.source = source; river.source = source;
@ -639,14 +630,20 @@ function addRiverOnClick() {
river.width = width; river.width = width;
river.cells = riverCells; river.cells = riverCells;
} else { } else {
const basin = Rivers.getBasin(parent); const basin = getBasin(parent);
const name = Rivers.getName(mouth); const name = getName(mouth);
const smallLength = rivers.map(r => r.length || 0).sort((a, b) => a - b)[Math.ceil(pack.rivers.length * 0.15)]; const type = getType({i: riverId, length, parent});
const type = length < smallLength ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River";
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type}); 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(meanderedPoints, widthFactor);
const id = "river" + riverId;
const riversG = viewbox.select("#rivers");
riversG.append("path").attr("id", id).attr("d", path);
if (d3.event.shiftKey === false) { if (d3.event.shiftKey === false) {
Lakes.cleanupLakeData(); Lakes.cleanupLakeData();
unpressClickToAddButton(); unpressClickToAddButton();

View file

@ -236,6 +236,10 @@ function P(probability) {
return Math.random() < probability; return Math.random() < probability;
} }
function each(n) {
return i => i % n === 0;
}
// random number (normal or gaussian distribution) // random number (normal or gaussian distribution)
function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) { function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
return rn(Math.max(Math.min(d3.randomNormal(expected, deviation)(), max), min), round); return rn(Math.max(Math.min(d3.randomNormal(expected, deviation)(), max), min), round);