-
+
@@ -2863,8 +2865,8 @@
-
+
diff --git a/libs/jquery-ui.css b/libs/jquery-ui.css
index 927bfc9b..8d1b6fcc 100644
--- a/libs/jquery-ui.css
+++ b/libs/jquery-ui.css
@@ -434,6 +434,9 @@ body .ui-dialog {
font-family: Arial,Helvetica,sans-serif;
font-size: 1em;
}
+.ui-widget button[class^="icon-"] {
+ padding: 1px 6px;
+}
.ui-widget.ui-widget-content {
border: 1px solid #5e4fa2;
color: #333333;
diff --git a/main.js b/main.js
index 85edae18..37d89739 100644
--- a/main.js
+++ b/main.js
@@ -338,7 +338,7 @@ function applyDefaultBiomesSystem() {
}
function showWelcomeMessage() {
- const post = link("https://www.reddit.com/r/FantasyMapGenerator/comments/ft5b41/update_v15/", "Main changes:"); // announcement on Reddit
+ const post = "Main changes:" //link("https://www.reddit.com/r/FantasyMapGenerator/comments/ft5b41/update_v15/", "Main changes:");
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 discord = link("https://discordapp.com/invite/X7E84HU", "Discord server");
@@ -347,18 +347,19 @@ function showWelcomeMessage() {
alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version
${version}.
This version is compatible with ${changelog}, loaded
.map files will be auto-updated.
${post}
- - Emblems generation
- - Emblem editor integrated with ${link("https://azgaar.github.io/Armoria", "Armoria")}
+ - State, province and burg Emblems generation
+ - Emblem editor integrated with ${link("https://azgaar.github.io/Armoria", "Armoria")} — our new dedicated Heraldry generator and editor
- Burg editor screen update
- Speak name functionality
+
Join our ${discord} and ${reddit} to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.
Thanks for all supporters on ${patreon}!`;
$("#alert").dialog(
{resizable: false, title: "Fantasy Map Generator update", width: "28em",
buttons: {OK: function() {$(this).dialog("close")}},
- position: {my: "center", at: "center", of: "svg"},
+ position: {my: "center center-80", at: "center", of: "svg"},
close: () => localStorage.setItem("version", version)}
);
}
@@ -545,7 +546,6 @@ function generate() {
reGraph();
drawCoastline();
- elevateLakes();
Rivers.generate();
defineBiomes();
@@ -570,7 +570,7 @@ function generate() {
Names.getMapName();
WARN && console.warn(`TOTAL: ${rn((performance.now()-timeStart)/1000,2)}s`);
- INFO && showStatistics();
+ showStatistics();
INFO && console.groupEnd("Generated Map " + seed);
}
catch(error) {
@@ -626,7 +626,7 @@ function calculateVoronoi(graph, points) {
TIME && console.timeEnd("calculateDelaunay");
TIME && console.time("calculateVoronoi");
- const voronoi = Voronoi(delaunay, allPoints, n);
+ const voronoi = new Voronoi(delaunay, allPoints, n);
graph.cells = voronoi.cells;
graph.cells.i = n < 65535 ? Uint16Array.from(d3.range(n)) : Uint32Array.from(d3.range(n)); // array of indexes
graph.vertices = voronoi.vertices;
@@ -1137,22 +1137,6 @@ function reMarkFeatures() {
TIME && console.timeEnd("reMarkFeatures");
}
-// temporary elevate some lakes to resolve depressions and flux the water to form an open (exorheic) lake
-function elevateLakes() {
- if (templateInput.value === "Atoll") return; // no need for Atolls
- TIME && console.time('elevateLakes');
- const cells = pack.cells, features = pack.features;
- const maxCells = cells.i.length / 100; // size limit; let big lakes be closed (endorheic)
- cells.i.forEach(i => {
- if (cells.h[i] >= 20) return;
- if (features[cells.f[i]].group !== "freshwater" || features[cells.f[i]].cells > maxCells) return;
- cells.h[i] = 20;
- //debug.append("circle").attr("cx", cells.p[i][0]).attr("cy", cells.p[i][1]).attr("r", .5).attr("fill", "blue");
- });
-
- TIME && console.timeEnd('elevateLakes');
-}
-
// assign biome id for each cell
function defineBiomes() {
TIME && console.time("defineBiomes");
@@ -1160,7 +1144,6 @@ function defineBiomes() {
cells.biome = new Uint8Array(cells.i.length); // biomes array
for (const i of cells.i) {
- if (f[cells.f[i]].group === "freshwater") cells.h[i] = 19; // de-elevate lakes; here to save some resources
const t = temp[cells.g[i]]; // cell temperature
const h = cells.h[i]; // cell height
const m = h < 20 ? 0 : calculateMoisture(i); // cell moisture
@@ -1718,11 +1701,7 @@ function addZones(number = 1) {
function showStatistics() {
const template = templateInput.value;
const templateRandom = locked("template") ? "" : "(random)";
-
- mapId = Date.now(); // unique map id is it's creation date number
- mapHistory.push({seed, width:graphWidth, height:graphHeight, template, created:mapId});
- console.log(`
- Seed: ${seed}
+ const stats = ` Seed: ${seed}
Canvas size: ${graphWidth}x${graphHeight}
Template: ${template} ${templateRandom}
Points: ${grid.points.length}
@@ -1733,7 +1712,11 @@ function showStatistics() {
Burgs: ${pack.burgs.length-1}
Religions: ${pack.religions.length-1}
Culture set: ${culturesSet.selectedOptions[0].innerText}
- Cultures: ${pack.cultures.length-1}`);
+ Cultures: ${pack.cultures.length-1}`;
+
+ mapId = Date.now(); // unique map id is it's creation date number
+ mapHistory.push({seed, width:graphWidth, height:graphHeight, template, created:mapId});
+ INFO && console.log(stats);
}
const regenerateMap = debounce(function() {
diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js
index 6120c27a..d3d75e97 100644
--- a/modules/burgs-and-states.js
+++ b/modules/burgs-and-states.js
@@ -208,8 +208,12 @@
if (cells.haven[i] && pack.features[cells.f[cells.haven[i]]].type === "lake") return "Lake";
if (cells.h[i] > 60) return "Highland";
if (cells.r[i] && cells.r[i].length > 100 && cells.r[i].length >= pack.rivers[0].length) return "River";
- if ([1, 2, 3, 4].includes(cells.biome[i])) return "Nomadic";
- if (cells.biome[i] > 4 && cells.biome[i] < 10) return "Hunting";
+
+ if (!cells.burg[i] || pack.burgs[cells.burg[i]].population < 6) {
+ if (population < 5 && [1, 2, 3, 4].includes(cells.biome[i])) return "Nomadic";
+ if (cells.biome[i] > 4 && cells.biome[i] < 10) return "Hunting";
+ }
+
return "Generic";
}
@@ -419,7 +423,7 @@
const hull = getHull(start, s.i, s.cells / 10);
const points = [...hull].map(v => pack.vertices.p[v]);
const delaunay = Delaunator.from(points);
- const voronoi = Voronoi(delaunay, points, points.length);
+ const voronoi = new Voronoi(delaunay, points, points.length);
const chain = connectCenters(voronoi.vertices, s.pole[1]);
const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i%15 === 0 || i+1 === chain.length);
paths.push([s.i, relaxed]);
diff --git a/modules/coa-generator.js b/modules/coa-generator.js
index e2cf6493..22265084 100644
--- a/modules/coa-generator.js
+++ b/modules/coa-generator.js
@@ -11,7 +11,14 @@
metals: { argent: 3, or: 2 },
colours: { gules: 5, azure: 4, sable: 3, purpure: 3, vert: 2 },
stains: { murrey: 1, sanguine: 1, tenné: 1 },
- patterns: { semy: 1, vair: 2, vairInPale: 1, vairEnPointe: 2, ermine: 2, chequy: 5, lozengy: 2, fusily: 1, pally: 4, barry: 4, gemelles: 1, bendy: 3, bendySinister: 2, palyBendy: 1, pappellony: 2, masoned: 3, fretty: 2 }
+ patterns: {
+ semy: 8, ermine: 6,
+ vair: 4, counterVair: 1, vairInPale: 1, vairEnPointe: 2, vairAncien: 2,
+ potent: 2, counterPotent: 1, potentInPale: 1, potentEnPointe: 1,
+ chequy: 8, lozengy: 5, fusily: 2, pally: 8, barry: 10, gemelles: 1,
+ bendy: 8, bendySinister: 4, palyBendy: 2, barryBendy: 1,
+ pappellony: 2, pappellony2: 3, scaly: 1, plumetty: 1,
+ masoned: 6, fretty: 3, grillage: 1, chainy: 1, maily: 2, honeycombed: 1 }
}
const charges = {
@@ -23,7 +30,7 @@
conventional: {
lozenge: 2, fusil: 4, mascle: 4, rustre: 2, lozengeFaceted: 3, lozengePloye: 1, roundel: 4, roundel2: 3, annulet: 4,
mullet: 5, mulletPierced: 1, mulletFaceted: 1, mullet4: 3, mullet6: 4, mullet6Pierced: 1, mullet6Faceted: 1, mullet7: 1, mullet8: 1, mullet10: 1,
- estoile: 1, compassRose: 1, billet: 5, delf: 0, triangle: 3, trianglePierced: 1, goutte: 4, heart: 4, pique: 2, сarreau: 1, trefle: 2,
+ estoile: 1, compassRose: 1, billet: 5, delf: 0, triangle: 3, trianglePierced: 1, goutte: 4, heart: 4, pique: 2, carreau: 1, trefle: 2,
fleurDeLis: 6, sun: 3, sunInSplendour: 1, crescent: 5, fountain: 1
},
crosses: {
@@ -189,6 +196,18 @@
}
};
+ const shields = {
+ types: {basic: 10, regional: 2, historical: 1, specific: 1, banner: 1, simple: 2, fantasy: 1, middleEarth: 0},
+ basic: {heater: 12, spanish: 6, french: 1},
+ regional: {horsehead: 1, horsehead2: 1, polish: 1, hessen: 1, swiss: 1},
+ historical: {boeotian: 1, roman: 2, kite: 1, oldFrench: 5, renaissance: 2, baroque: 2},
+ specific: {targe: 1, targe2: 0, pavise: 5, wedged: 10},
+ banner: {flag: 1, pennon: 0, guidon: 0, banner: 0, dovetail: 1, gonfalon: 5, pennant: 0},
+ simple: {round: 12, oval: 6, vesicaPiscis: 1, square: 1, diamond: 2, no: 0},
+ fantasy: {fantasy1: 2, fantasy2: 2, fantasy3: 1, fantasy4: 1, fantasy5: 3},
+ middleEarth: {noldor: 1, gondor: 1, easterling: 1, erebor: 1, ironHills: 1, urukHai: 1, moriaOrc: 1}
+ }
+
const generate = function(parent, kinship, dominion, type) {
if (parent === "custom") parent = null;
let usedPattern = null, usedTinctures = [];
@@ -398,10 +417,10 @@
function definePattern(pattern, element, size = "") {
let t1 = null, t2 = null;
- if (P(.15)) size = "-small";
- else if (P(.05)) size = "-smaller";
- else if (P(.035)) size = "-big";
- else if (P(.001)) size = "-smallest";
+ if (P(.1)) size = "-small";
+ else if (P(.1)) size = "-smaller";
+ else if (P(.01)) size = "-big";
+ else if (P(.005)) size = "-smallest";
// apply standard tinctures
if (P(.5) && ["vair", "vairInPale", "vairEnPointe"].includes(pattern)) {t1 = "azure"; t2 = "argent";}
@@ -464,16 +483,19 @@
}
const getShield = function(culture, state) {
- const emblemShape = document.getElementById("emblemShape").value;
- if (emblemShape === "state" && state && pack.states[state].coa) return pack.states[state].coa.shield;
+ const emblemShape = document.getElementById("emblemShape");
+ const shapeGroup = emblemShape.selectedOptions[0].parentNode.label;
+ if (shapeGroup !== "Diversiform") return emblemShape.value;
+
+ if (emblemShape.value === "state" && state && pack.states[state].coa) return pack.states[state].coa.shield;
if (pack.cultures[culture].shield) return pack.cultures[culture].shield;
- console.error("Emblem shape is not defined on culture level", pack.cultures[culture]);
+ console.error("Shield shape is not defined on culture level", pack.cultures[culture]);
return "heater";
}
const toString = coa => JSON.stringify(coa).replaceAll("#", "%23");
const copy = coa => JSON.parse(JSON.stringify(coa));
- return {generate, toString, copy, getShield};
+ return {generate, toString, copy, getShield, shields};
})));
\ No newline at end of file
diff --git a/modules/coa-renderer.js b/modules/coa-renderer.js
index e956f322..3a451782 100644
--- a/modules/coa-renderer.js
+++ b/modules/coa-renderer.js
@@ -756,72 +756,112 @@
}
const templates = {
- // divisions
- perFess: line => `
`,
- perPale: line => `
`,
- perBend: line => `
`,
- perBendSinister: line => `
`,
- perChevron: line => `
`,
- perChevronReversed: line => `
`,
- perCross: line => `
`,
- perPile: line => `
`,
- perSaltire: () => `
`,
- gyronny: () => `
`,
- chevronny: () => `
`,
- // oprinaries
- fess: line => `
`,
- pale: line => `
`,
- bend: line => `
`,
- bendSinister: line => `
`,
- chief: line => `
`,
- bar: line => `
`,
- gemelle: line => `
`,
- fessCotissed: line => `
`,
- fessDoubleCotissed: line => `
`,
- bendlet: line => `
`,
- bendletSinister: line => `
`,
- terrace: line => `
`,
- cross: line => `
`,
- crossParted: line => `
`,
- saltire: line => `
`,
- saltireParted: line => `
`,
- mount: () => `
`,
- point: () => `
`,
- flaunches: () => `
`,
- gore: () => `
`,
- pall: () => `
`,
- pallReversed: () => `
`,
- chevron: () => `
`,
- chevronReversed: () => `
`,
- gyron: () => `
`,
- quarter: () => `
`,
- canton: () => `
`,
- pile: () => `
`,
- pileInBend: () => `
`,
- pileInBendSinister: () => `
`,
- piles: () => `
`,
- pilesInPoint: () => `
`,
- label: () => `
`
+ // straight divisions
+ perFess: `
`,
+ perPale: `
`,
+ perBend: `
`,
+ perBendSinister: `
`,
+ perChevron: `
`,
+ perChevronReversed: `
`,
+ perCross: `
`,
+ perPile: `
`,
+ perSaltire: `
`,
+ gyronny: `
`,
+ chevronny: `
`,
+ // lined divisions
+ perFessLined: line => `
`,
+ perPaleLined: line => `
`,
+ perBendLined: line => `
`,
+ perBendSinisterLined: line => `
`,
+ perChevronLined: line => `
`,
+ perChevronReversedLined: line => `
`,
+ perCrossLined: line => `
`,
+ perPileLined: line => `
`,
+ // straight ordinaries
+ fess: `
`,
+ pale: `
`,
+ bend: `
`,
+ bendSinister: `
`,
+ chief: `
`,
+ bar: `
`,
+ gemelle: `
`,
+ fessCotissed: `
`,
+ fessDoubleCotissed: `
`,
+ bendlet: `
`,
+ bendletSinister: `
`,
+ terrace: `
`,
+ cross: `
`,
+ crossParted: `
`,
+ saltire: `
`,
+ saltireParted: `
`,
+ mount: `
`,
+ point: `
`,
+ flaunches: `
`,
+ gore: `
`,
+ pall: `
`,
+ pallReversed: `
`,
+ chevron: `
`,
+ chevronReversed: `
`,
+ gyron: `
`,
+ quarter: `
`,
+ canton: `
`,
+ pile: `
`,
+ pileInBend: `
`,
+ pileInBendSinister: `
`,
+ piles: `
`,
+ pilesInPoint: `
`,
+ label: `
`,
+ // lined ordinaries
+ fessLined: line => `
`,
+ paleLined: line => `
`,
+ bendLined: line => `
`,
+ bendSinisterLined: line => `
`,
+ chiefLined: line => `
`,
+ barLined: line => `
`,
+ gemelleLined: line => `
`,
+ fessCotissedLined: line => `
`,
+ fessDoubleCotissedLined: line => `
`,
+ bendletLined: line => `
`,
+ bendletSinisterLined: line => `
`,
+ terraceLined: line => `
`,
+ crossLined: line => `
`,
+ crossPartedLined: line => `
`,
+ saltireLined: line => `
`,
+ saltirePartedLined: line => `
`
}
const patterns = {
- semy: (p, c1, c2, size, chargeId) => `
`,
- vair: (p, c1, c2, size) => `
`,
- vairInPale: (p, c1, c2, size) => `
`,
- vairEnPointe: (p, c1, c2, size) => `
`,
- ermine: (p, c1, c2, size) => `
`,
- chequy: (p, c1, c2, size) => `
`,
- lozengy: (p, c1, c2, size) => `
`,
- fusily: (p, c1, c2, size) => `
`,
- pally: (p, c1, c2, size) => `
`,
- barry: (p, c1, c2, size) => `
`,
- gemelles: (p, c1, c2, size) => `
`,
- bendy: (p, c1, c2, size) => `
`,
- bendySinister: (p, c1, c2, size) => `
`,
- palyBendy: (p, c1, c2, size) => `
`,
- pappellony: (p, c1, c2, size) => `
`,
- masoned: (p, c1, c2, size) => `
`,
- fretty: (p, c1, c2, size) => `
`
+ semy: (p, c1, c2, size, chargeId) => `
`,
+ vair: (p, c1, c2, size) => `
`,
+ counterVair: (p, c1, c2, size) => `
`,
+ vairInPale: (p, c1, c2, size) => `
`,
+ vairEnPointe: (p, c1, c2, size) => `
`,
+ vairAncien: (p, c1, c2, size) => `
`,
+ potent: (p, c1, c2, size) => `
`,
+ counterPotent: (p, c1, c2, size) => `
`,
+ potentInPale: (p, c1, c2, size) => `
`,
+ potentEnPointe: (p, c1, c2, size) => `
`,
+ ermine: (p, c1, c2, size) => `
`,
+ chequy: (p, c1, c2, size) => `
`,
+ lozengy: (p, c1, c2, size) => `
`,
+ fusily: (p, c1, c2, size) => `
`,
+ pally: (p, c1, c2, size) => `
`,
+ barry: (p, c1, c2, size) => `
`,
+ gemelles: (p, c1, c2, size) => `
`,
+ bendy: (p, c1, c2, size) => `
`,
+ bendySinister: (p, c1, c2, size) => `
`,
+ palyBendy: (p, c1, c2, size) => `
`,
+ barryBendy: (p, c1, c2, size) => `
`,
+ pappellony: (p, c1, c2, size) => `
`,
+ pappellony2: (p, c1, c2, size) => `
`,
+ scaly: (p, c1, c2, size) => `
`,
+ plumetty: (p, c1, c2, size) => `
`,
+ masoned: (p, c1, c2, size) => `
`,
+ fretty: (p, c1, c2, size) => `
`,
+ grillage: (p, c1, c2, size) => `
`,
+ chainy: (p, c1, c2, size) => `
`,
+ maily: (p, c1, c2, size) => `
`,
+ honeycombed: (p, c1, c2, size) => `
`
}
const draw = async function(id, coa) {
@@ -839,11 +879,7 @@
const divisionClip = division ? `
${getTemplate(division.division, division.line)}` : "";
const loadedCharges = await getCharges(coa, id, shieldPath);
const loadedPatterns = getPatterns(coa, id);
- const blacklight = `
-
-
-
- `;
+ const blacklight = `
`;
const field = `
`;
const divisionGroup = division ? templateDivision() : "";
const overlay = `
`;
@@ -855,6 +891,7 @@
// insert coa svg to defs
document.getElementById("coas").insertAdjacentHTML("beforeend", svg);
+ return true;
function templateDivision() {
let svg = "";
@@ -1000,17 +1037,18 @@
}
function getSizeMod(size) {
- if (size === "small") return .5;
- if (size === "smaller") return .25;
- if (size === "smallest") return .125;
- if (size === "big") return 2;
+ if (size === "small") return .8;
+ if (size === "smaller") return .5;
+ if (size === "smallest") return .25;
+ if (size === "big") return 1.6;
return 1;
}
- function getTemplate(templateId, lineId) {
- if (!lineId) return templates[templateId]();
- const line = lines[lineId] || lines.straight;
- return templates[templateId](line);
+ function getTemplate(id, line) {
+ const linedId = id+"Lined";
+ if (!line || line === "straight" || !templates[linedId]) return templates[id];
+ const linePath = lines[line];
+ return templates[linedId](linePath);
}
// get color or link to pattern
@@ -1027,7 +1065,7 @@
}
// render coa if does not exist
- const trigger = function(id, coa) {
+ const trigger = async function(id, coa) {
if (coa === "custom") {
console.warn("Cannot render custom emblem", coa);
return;
@@ -1036,7 +1074,7 @@
console.warn(`Emblem ${id} is undefined`);
return;
}
- if (!document.getElementById(id)) draw(id, coa);
+ if (!document.getElementById(id)) return draw(id, coa);
}
const add = function(type, i, coa, x, y) {
@@ -1051,6 +1089,6 @@
if (layerIsOn("toggleEmblems")) trigger(id, coa);
}
- return {trigger, add};
+ return {trigger, add, shieldPaths};
})));
diff --git a/modules/cultures-generator.js b/modules/cultures-generator.js
index 3fcf8170..d0e37161 100644
--- a/modules/cultures-generator.js
+++ b/modules/cultures-generator.js
@@ -55,26 +55,9 @@
c.origin = 0;
c.code = getCode(c.name);
cells.culture[cell] = i+1;
- if (emblemShape === "random") c.shield = getRandomShiled();
- else if (emblemShape !== "culture" && emblemShape !== "state") c.shield = emblemShape;
+ if (emblemShape === "random") c.shield = getRandomShield();
});
- function getRandomShiled() {
- const shields = {
- types: {basic: 10, regional: 2, historical: 1, specific: 1, banner: 1, simple: 2, fantasy: 1, middleEarth: 0},
- basic: {heater: 12, spanish: 6, french: 1},
- regional: {horsehead: 1, horsehead2: 1, polish: 1, hessen: 1, swiss: 1},
- historical: {boeotian: 1, roman: 2, kite: 1, oldFrench: 5, renaissance: 2, baroque: 2},
- specific: {targe: 1, targe2: 0, pavise: 5, wedged: 10},
- banner: {flag: 1, pennon: 0, guidon: 0, banner: 0, dovetail: 1, gonfalon: 5, pennant: 0},
- simple: {round: 12, oval: 6, vesicaPiscis: 1, square: 1, diamond: 2, no: 0},
- fantasy: {fantasy1: 2, fantasy2: 2, fantasy3: 1, fantasy4: 1, fantasy5: 3},
- middleEarth: {noldor: 1, gondor: 1, easterling: 1, erebor: 1, ironHills: 1, urukHai: 1, moriaOrc: 1}
- }
- const type = rw(shields.types);
- return rw(shields[type]);
- }
-
function placeCenter(v) {
let c, spacing = (graphWidth + graphHeight) / 2 / count;
const sorted = [...populated].sort((a, b) => v(b) - v(a)), max = Math.floor(sorted.length / 2);
@@ -161,7 +144,13 @@
const code = getCode(name);
const i = pack.cultures.length;
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
- pack.cultures.push({name, color, base, center, i, expansionism:1, type:"Generic", cells:0, area:0, rural:0, urban:0, origin:0, code});
+
+ // define emblem shape
+ let shield = culture.shield;
+ const emblemShape = document.getElementById("emblemShape").value;
+ if (emblemShape === "random") shield = getRandomShield();
+
+ pack.cultures.push({name, color, base, center, i, expansionism:1, type:"Generic", cells:0, area:0, rural:0, urban:0, origin:0, code, shield});
}
const getDefault = function(count) {
@@ -428,6 +417,11 @@
return 0;
}
- return {generate, add, expand, getDefault};
+ const getRandomShield = function() {
+ const type = rw(COA.shields.types);
+ return rw(COA.shields[type]);
+ }
+
+ return {generate, add, expand, getDefault, getRandomShield};
})));
diff --git a/modules/river-generator.js b/modules/river-generator.js
index e0c5cdbe..2e6c5022 100644
--- a/modules/river-generator.js
+++ b/modules/river-generator.js
@@ -1,296 +1,376 @@
(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global.Rivers = factory());
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.Rivers = factory());
}(this, (function () {'use strict';
- const generate = function(changeHeights = true) {
- TIME && console.time('generateRivers');
- Math.random = aleaPRNG(seed);
- const cells = pack.cells, p = cells.p, features = pack.features;
+const generate = function(changeHeights = true) {
+ TIME && console.time('generateRivers');
+ Math.random = aleaPRNG(seed);
+ const cells = pack.cells, p = cells.p, features = pack.features;
- // build distance field in cells from water (cells.t)
- void function markupLand() {
- const q = t => cells.i.filter(i => cells.t[i] === t);
- for (let t = 2, queue = q(t); queue.length; t++, queue = q(t)) {
- queue.forEach(i => cells.c[i].forEach(c => {
- if (!cells.t[c]) cells.t[c] = t+1;
- }));
+ // build distance field in cells from water (cells.t)
+ void function markupLand() {
+ const q = t => cells.i.filter(i => cells.t[i] === t);
+ for (let t = 2, queue = q(t); queue.length; t++, queue = q(t)) {
+ queue.forEach(i => cells.c[i].forEach(c => {
+ if (!cells.t[c]) cells.t[c] = t+1;
+ }));
+ }
+ }()
+
+ // height with added t value to make map less depressed
+ const h = Array.from(cells.h)
+ .map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100)
+ .map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000);
+
+ resolveDepressions(h);
+ features.forEach(f => {delete f.river; delete f.flux; delete f.inlets});
+
+ const riversData = []; // rivers data
+ cells.fl = new Uint16Array(cells.i.length); // water flux array
+ cells.r = new Uint16Array(cells.i.length); // rivers array
+ cells.conf = new Uint8Array(cells.i.length); // confluences array
+ let riverNext = 1; // first river id is 1, not 0
+
+ void function drainWater() {
+ const land = cells.i.filter(i => h[i] >= 20).sort((a,b) => h[b] - h[a]);
+ const outlets = new Uint32Array(features.length);
+ // enumerate lake outlet positions
+ features.filter(f => f.type === "lake" && (f.group === "freshwater" || f.group === "frozen")).forEach(l => {
+ let outlet = 0;
+ if (l.shoreline) {
+ outlet = l.shoreline[d3.scan(l.shoreline, (a,b) => h[a] - h[b])];
+ } else { // in case it got missed or deleted
+ WARN && console.warn('Re-scanning shoreline of a lake');
+ const shallows = cells.i.filter(j => cells.t[j] === -1 && cells.f[j] === l.i);
+ let shoreline = [];
+ shallows.map(w => cells.c[w]).forEach(cList => cList.forEach(s => shoreline.push(s)));
+ outlet = shoreline[d3.scan(shoreline, (a,b) => h[a] - h[b])];
}
- }()
+ outlets[l.i] = outlet;
+ delete l.shoreline // cleanup temp data once used
+ });
- // height with added t value to make map less depressed
- const h = Array.from(cells.h)
- .map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100)
- .map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000);
-
- resolveDepressions(h);
- features.forEach(f => {delete f.river; delete f.flux;});
-
- const riversData = []; // rivers data
- cells.fl = new Uint16Array(cells.i.length); // water flux array
- cells.r = new Uint16Array(cells.i.length); // rivers array
- cells.conf = new Uint8Array(cells.i.length); // confluences array
- let riverNext = 1; // first river id is 1, not 0
-
- void function drainWater() {
- const land = cells.i.filter(i => h[i] >= 20).sort((a,b) => h[b] - h[a]);
- land.forEach(function(i) {
- cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation
- const x = p[i][0], y = p[i][1];
-
- // near-border cell: pour out of the screen
- if (cells.b[i]) {
- if (cells.r[i]) {
- const to = [];
- const min = Math.min(y, graphHeight - y, x, graphWidth - x);
- if (min === y) {to[0] = x; to[1] = 0;} else
- if (min === graphHeight - y) {to[0] = x; to[1] = graphHeight;} else
- if (min === x) {to[0] = 0; to[1] = y;} else
- if (min === graphWidth - x) {to[0] = graphWidth; to[1] = y;}
- riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1]});
- }
- return;
- }
-
- //const min = cells.c[i][d3.scan(cells.c[i], (a, b) => h[a] - h[b])]; // downhill cell
- let min = cells.c[i][d3.scan(cells.c[i], (a, b) => h[a] - h[b])]; // downhill cell
-
- // allow only one river can flow through a lake
- const cf = features[cells.f[i]]; // current cell feature
- if (cf.river && cf.river !== cells.r[i]) {
- cells.fl[i] = 0;
- }
-
- if (cells.fl[i] < 30) {
- if (h[min] >= 20) cells.fl[min] += cells.fl[i];
- return; // flux is too small to operate as river
- }
-
- // Proclaim a new river
- if (!cells.r[i]) {
- cells.r[i] = riverNext;
- riversData.push({river: riverNext, cell: i, x, y});
- riverNext++;
- }
-
- if (cells.r[min]) { // downhill cell already has river assigned
- if (cells.fl[min] < cells.fl[i]) {
- cells.conf[min] = cells.fl[min]; // mark confluence
- if (h[min] >= 20) riversData.find(r => r.river === cells.r[min]).parent = cells.r[i]; // min river is a tributary of current river
- cells.r[min] = cells.r[i]; // re-assign river if downhill part has less flux
- } else {
- cells.conf[min] += cells.fl[i]; // mark confluence
- if (h[min] >= 20) riversData.find(r => r.river === cells.r[i]).parent = cells.r[min]; // current river is a tributary of min river
- }
- } else cells.r[min] = cells.r[i]; // assign the river to the downhill cell
-
- const nx = p[min][0], ny = p[min][1];
- if (h[min] < 20) {
- // pour water to the sea haven
- riversData.push({river: cells.r[i], cell: cells.haven[i], x: nx, y: ny});
+ const flowDown = function(min, mFlux, iFlux, ri, i = 0){
+ if (cells.r[min]) { // downhill cell already has river assigned
+ if (mFlux < iFlux) {
+ cells.conf[min] = cells.fl[min]; // mark confluence
+ if (h[min] >= 20) riversData.find(r => r.river === cells.r[min]).parent = ri; // min river is a tributary of current river
+ cells.r[min] = ri; // re-assign river if downhill part has less flux
} else {
- const mf = features[cells.f[min]]; // feature of min cell
- if (mf.type === "lake") {
- if (!mf.river || cells.fl[i] > mf.flux) {
- mf.river = cells.r[i]; // pour water to temporaly elevated lake
- mf.flux = cells.fl[i]; // entering flux
+ cells.conf[min] += iFlux; // mark confluence
+ if (h[min] >= 20) riversData.find(r => r.river === ri).parent = cells.r[min]; // current river is a tributary of min river
+ }
+ } else cells.r[min] = ri; // assign the river to the downhill cell
+
+ if (h[min] < 20) {
+ // pour water to the sea haven
+ const oh = i ? cells.haven[i] : min;
+ riversData.push({river: ri, cell: oh, x: p[min][0], y: p[min][1]});
+ const mf = features[cells.f[min]]; // feature of min cell
+ if (mf.type === "lake") {
+ if (!mf.river || iFlux > mf.flux) {
+ mf.river = ri; // pour water to temporaly elevated lake
+ mf.flux = iFlux; // entering flux
+ }
+ mf.totalFlux += iFlux;
+ if (mf.inlets) {
+ mf.inlets.push(ri);
+ } else {
+ mf.inlets = [ri];
+ }
+ }
+ } else {
+ cells.fl[min] += iFlux; // propagate flux
+ riversData.push({river: ri, cell: min, x: p[min][0], y: p[min][1]}); // add next River segment
+ }
+ }
+
+ land.forEach(function(i) {
+ cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation
+ const x = p[i][0], y = p[i][1];
+
+ // lake outlets draw from lake
+ let n = -1, out2 = 0;
+ while (outlets.includes(i, n+1)) {
+ n = outlets.indexOf(i, n+1);
+ const l = features[n];
+ if ( ! l ) {continue;}
+ const j = cells.haven[i];
+ // allow chain lakes to retain identity
+ if(cells.r[j] !== l.river) {
+ let touch = false;
+ for (const c of cells.c[j]){
+ if (cells.r[c] === l.river) {
+ touch = true;
+ break;
}
}
- cells.fl[min] += cells.fl[i]; // propagate flux
- riversData.push({river: cells.r[i], cell: min, x: nx, y: ny}); // add next River segment
- }
-
- });
- }()
-
- void function defineRivers() {
- pack.rivers = []; // rivers data
- const riverPaths = []; // temporary data for all rivers
-
- for (let r = 1; r <= riverNext; r++) {
- const riverSegments = riversData.filter(d => d.river === r);
-
- if (riverSegments.length > 2) {
- const riverEnhanced = addMeandring(riverSegments);
- const width = rn(.8 + Math.random() * .4, 1); // river width modifier
- const increment = rn(.8 + Math.random() * .6, 1); // river bed widening modifier
- const [path, length] = getPath(riverEnhanced, width, increment);
- riverPaths.push([r, path, width, increment]);
- const source = riverSegments[0], mouth = riverSegments[riverSegments.length-2];
- const parent = source.parent || 0;
- pack.rivers.push({i:r, parent, length, source:source.cell, mouth:mouth.cell});
- } else {
- // remove too short rivers
- riverSegments.filter(s => cells.r[s.cell] === r).forEach(s => cells.r[s.cell] = 0);
+ if (touch) {
+ cells.r[j] = l.river;
+ riversData.push({river: l.river, cell: j, x: p[j][0], y: p[j][1]});
+ } else {
+ cells.r[j] = riverNext;
+ riversData.push({river: riverNext, cell: j, x: p[j][0], y: p[j][1]});
+ riverNext++;
+ }
}
+ cells.fl[j] = l.totalFlux; // signpost river size
+ flowDown(i, cells.fl[i], l.totalFlux, cells.r[j]);
+ // prevent dropping imediately back into the lake
+ out2 = cells.c[i].filter(c => (h[c] >= 20 || cells.f[c] !== cells.f[j])).sort((a,b) => h[a] - h[b])[0]; // downhill cell not in the source lake
+ // assign all to outlet basin
+ if (l.inlets) l.inlets.forEach(fork => riversData.find(r => r.river === fork).parent = cells.r[j]);
}
- const html = riverPaths.map(r =>`
`).join("");
- rivers.html(html);
- }()
-
- // apply change heights as basic one
- if (changeHeights) cells.h = Uint8Array.from(h);
-
- TIME && console.timeEnd('generateRivers');
- }
-
- // depression filling algorithm (for a correct water flux modeling)
- const resolveDepressions = function(h) {
- const cells = pack.cells;
- const land = cells.i.filter(i => h[i] >= 20 && h[i] < 100 && !cells.b[i]); // exclude near-border cells
- land.sort((a,b) => h[b] - h[a]); // highest cells go first
- let depressed = false;
-
- for (let l = 0, depression = Infinity; depression && l < 100; l++) {
- depression = 0;
- for (const i of land) {
- const minHeight = d3.min(cells.c[i].map(c => h[c]));
- if (minHeight === 100) continue; // already max height
- if (h[i] <= minHeight) {
- h[i] = Math.min(minHeight + 1, 100);
- depression++;
- depressed = true;
+ // near-border cell: pour out of the screen
+ if (cells.b[i]) {
+ if (cells.r[i]) {
+ const to = [];
+ const min = Math.min(y, graphHeight - y, x, graphWidth - x);
+ if (min === y) {to[0] = x; to[1] = 0;} else
+ if (min === graphHeight - y) {to[0] = x; to[1] = graphHeight;} else
+ if (min === x) {to[0] = 0; to[1] = y;} else
+ if (min === graphWidth - x) {to[0] = graphWidth; to[1] = y;}
+ riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1]});
}
+ return;
+ }
+
+ const min = out2 ? out2 : cells.c[i][d3.scan(cells.c[i], (a, b) => h[a] - h[b])]; // downhill cell
+
+ if (cells.fl[i] < 30) {
+ if (h[min] >= 20) cells.fl[min] += cells.fl[i];
+ return; // flux is too small to operate as river
+ }
+
+ // Proclaim a new river
+ if (!cells.r[i]) {
+ cells.r[i] = riverNext;
+ riversData.push({river: riverNext, cell: i, x, y});
+ riverNext++;
+ }
+
+ flowDown(min, cells.fl[min], cells.fl[i], cells.r[i], i);
+
+ });
+ }()
+
+ void function defineRivers() {
+ pack.rivers = []; // rivers data
+ const riverPaths = []; // temporary data for all rivers
+
+ for (let r = 1; r <= riverNext; r++) {
+ const riverSegments = riversData.filter(d => d.river === r);
+
+ if (riverSegments.length > 2) {
+ const source = riverSegments[0], mouth = riverSegments[riverSegments.length-2];
+ const riverEnhanced = addMeandring(riverSegments);
+ let width = rn(.8 + Math.random() * .4, 1); // river width modifier [.2, 10]
+ let increment = rn(.8 + Math.random() * .6, 1); // river bed widening modifier [.01, 3]
+ const [path, length] = getPath(riverEnhanced, width, increment, cells.h[source.cell] >= 20 ? .1 : .6);
+ riverPaths.push([r, path, width, increment]);
+ const parent = source.parent || 0;
+ pack.rivers.push({i:r, parent, length, source:source.cell, mouth:mouth.cell});
+ } else {
+ // remove too short rivers
+ riverSegments.filter(s => cells.r[s.cell] === r).forEach(s => cells.r[s.cell] = 0);
}
}
- return depressed;
- }
+ // drawRivers
+ rivers.selectAll("path").remove();
+ rivers.selectAll("path").data(riverPaths).enter()
+ .append("path").attr("d", d => d[1]).attr("id", d => "river"+d[0])
+ .attr("data-width", d => d[2]).attr("data-increment", d => d[3]);
+ }()
- // add more river points on 1/3 and 2/3 of length
- const addMeandring = function(segments, rndFactor = 0.3) {
- const riverEnhanced = []; // to store enhanced segments
- let side = 1; // to control meandring direction
+ // apply change heights as basic one
+ if (changeHeights) cells.h = Uint8Array.from(h);
- for (let s = 0; s < segments.length; s++) {
- const sX = segments[s].x, sY = segments[s].y; // segment start coordinates
- const c = pack.cells.conf[segments[s].cell] || 0; // if segment is river confluence
- riverEnhanced.push([sX, sY, c]);
+ TIME && console.timeEnd('generateRivers');
+}
- if (s+1 === segments.length) break; // do not enhance last segment
-
- const eX = segments[s+1].x, eY = segments[s+1].y; // segment end coordinates
- const angle = Math.atan2(eY - sY, eX - sX);
- const sin = Math.sin(angle), cos = Math.cos(angle);
- const serpentine = 1 / (s + 1) + 0.3;
- const meandr = serpentine + Math.random() * rndFactor;
- if (P(.5)) side *= -1; // change meandring direction in 50%
- const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2;
- // if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
- if (dist2 > 64 || (dist2 > 16 && segments.length < 6)) {
- const p1x = (sX * 2 + eX) / 3 + side * -sin * meandr;
- const p1y = (sY * 2 + eY) / 3 + side * cos * meandr;
- if (P(.2)) side *= -1; // change 2nd extra point meandring direction in 20%
- const p2x = (sX + eX * 2) / 3 + side * sin * meandr;
- const p2y = (sY + eY * 2) / 3 + side * cos * meandr;
- riverEnhanced.push([p1x, p1y], [p2x, p2y]);
- // if dist is medium or river is small add 1 extra middlepoint
- } else if (dist2 > 16 || segments.length < 6) {
- const p1x = (sX + eX) / 2 + side * -sin * meandr;
- const p1y = (sY + eY) / 2 + side * cos * meandr;
- riverEnhanced.push([p1x, p1y]);
+// depression filling algorithm (for a correct water flux modeling)
+const resolveDepressions = function(h) {
+ const cells = pack.cells;
+ const land = cells.i.filter(i => h[i] >= 20 && h[i] < 100 && !cells.b[i]); // exclude near-border cells
+ const lakes = pack.features.filter(f => f.type === "lake" && (f.group === "freshwater" || f.group === "frozen")); // to keep lakes flat
+ lakes.forEach(l => {
+ l.shoreline = [];
+ l.height = 21;
+ l.totalFlux = grid.cells.prec[cells.g[l.firstCell]];
+ });
+ for (let i of land.filter(i => cells.t[i] === 1)) { // select shoreline cells
+ cells.c[i].map(c => pack.features[cells.f[c]]).forEach(cf => {
+ if (lakes.includes(cf) && !cf.shoreline.includes(i)) {
+ cf.shoreline.push(i);
}
+ })
+ }
+ land.sort((a,b) => h[b] - h[a]); // highest cells go first
+ let depressed = false;
+ for (let l = 0, depression = Infinity; depression && l < 100; l++) {
+ depression = 0;
+ for (const l of lakes) {
+ const minHeight = d3.min(l.shoreline.map(s => h[s]));
+ if (minHeight === 100) continue; // already max height
+ if (l.height <= minHeight) {
+ l.height = Math.min(minHeight + 1, 100);
+ depression++;
+ depressed = true;
+ }
+ }
+ for (const i of land) {
+ const minHeight = d3.min(cells.c[i].map(c => cells.t[c] > 0 ? h[c] :
+ pack.features[cells.f[c]].height || h[c] // NB undefined is falsy (a || b is short for a ? a : b)
+ ));
+ if (minHeight === 100) continue; // already max height
+ if (h[i] <= minHeight) {
+ h[i] = Math.min(minHeight + 1, 100);
+ depression++;
+ depressed = true;
+ }
}
- return riverEnhanced;
}
- const getPath = function(points, width = 1, increment = 1) {
- let offset, extraOffset = .1; // starting river width (to make river source visible)
- 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); // summ of segments length
- const widening = rn((1000 + (riverLength * 30)) * increment);
- const riverPointsLeft = [], riverPointsRight = []; // store points on both sides to build a valid polygon
- const last = points.length - 1;
- const factor = riverLength / points.length;
+ return depressed;
+}
- // first point
- let x = points[0][0], y = points[0][1], c;
- let angle = Math.atan2(y - points[1][1], x - points[1][0]);
- let sin = Math.sin(angle), cos = Math.cos(angle);
- let xLeft = x + -sin * extraOffset, yLeft = y + cos * extraOffset;
- riverPointsLeft.push([xLeft, yLeft]);
- let xRight = x + sin * extraOffset, yRight = y + -cos * extraOffset;
- riverPointsRight.unshift([xRight, yRight]);
+// add more river points on 1/3 and 2/3 of length
+const addMeandring = function(segments, rndFactor = 0.3) {
+ const riverEnhanced = []; // to store enhanced segments
+ let side = 1; // to control meandring direction
- // middle points
- for (let p = 1; p < last; p++) {
- x = points[p][0], y = points[p][1], c = points[p][2] || 0;
- const xPrev = points[p-1][0], yPrev = points[p - 1][1];
- const xNext = points[p+1][0], yNext = points[p + 1][1];
- angle = Math.atan2(yPrev - yNext, xPrev - xNext);
- sin = Math.sin(angle), cos = Math.cos(angle);
- offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2 * width) + extraOffset;
- const confOffset = Math.atan(c * 5 / widening);
- extraOffset += confOffset;
- xLeft = x + -sin * offset, yLeft = y + cos * (offset + confOffset);
- riverPointsLeft.push([xLeft, yLeft]);
- xRight = x + sin * offset, yRight = y + -cos * offset;
- riverPointsRight.unshift([xRight, yRight]);
+ for (let s = 0; s < segments.length; s++) {
+ const sX = segments[s].x, sY = segments[s].y; // segment start coordinates
+ const c = pack.cells.conf[segments[s].cell] || 0; // if segment is river confluence
+ riverEnhanced.push([sX, sY, c]);
+
+ if (s+1 === segments.length) break; // do not enhance last segment
+
+ const eX = segments[s+1].x, eY = segments[s+1].y; // segment end coordinates
+ const angle = Math.atan2(eY - sY, eX - sX);
+ const sin = Math.sin(angle), cos = Math.cos(angle);
+ const serpentine = 1 / (s + 1) + 0.3;
+ const meandr = serpentine + Math.random() * rndFactor;
+ if (P(.5)) side *= -1; // change meandring direction in 50%
+ const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2;
+ // if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
+ if (dist2 > 64 || (dist2 > 16 && segments.length < 6)) {
+ const p1x = (sX * 2 + eX) / 3 + side * -sin * meandr;
+ const p1y = (sY * 2 + eY) / 3 + side * cos * meandr;
+ if (P(.2)) side *= -1; // change 2nd extra point meandring direction in 20%
+ const p2x = (sX + eX * 2) / 3 + side * sin * meandr;
+ const p2y = (sY + eY * 2) / 3 + side * cos * meandr;
+ riverEnhanced.push([p1x, p1y], [p2x, p2y]);
+ // if dist is medium or river is small add 1 extra middlepoint
+ } else if (dist2 > 16 || segments.length < 6) {
+ const p1x = (sX + eX) / 2 + side * -sin * meandr;
+ const p1y = (sY + eY) / 2 + side * cos * meandr;
+ riverEnhanced.push([p1x, p1y]);
}
- // end point
- x = points[last][0], y = points[last][1], c = points[last][2];
- if (c) extraOffset += Math.atan(c * 10 / widening); // add extra width on river confluence
- angle = Math.atan2(points[last-1][1] - y, points[last-1][0] - x);
+ }
+ return riverEnhanced;
+}
+
+const getPath = function(points, width = 1, increment = 1, starting = .1) {
+ let offset, extraOffset = starting; // starting river width (to make river source visible)
+ 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); // summ of segments length
+ const widening = rn((1000 + (riverLength * 30)) * increment);
+ const riverPointsLeft = [], riverPointsRight = []; // store points on both sides to build a valid polygon
+ const last = points.length - 1;
+ const factor = riverLength / points.length;
+
+ // first point
+ let x = points[0][0], y = points[0][1], c;
+ let angle = Math.atan2(y - points[1][1], x - points[1][0]);
+ let sin = Math.sin(angle), cos = Math.cos(angle);
+ let xLeft = x + -sin * extraOffset, yLeft = y + cos * extraOffset;
+ riverPointsLeft.push([xLeft, yLeft]);
+ let xRight = x + sin * extraOffset, yRight = y + -cos * extraOffset;
+ riverPointsRight.unshift([xRight, yRight]);
+
+ // middle points
+ for (let p = 1; p < last; p++) {
+ x = points[p][0], y = points[p][1], c = points[p][2] || 0;
+ const xPrev = points[p-1][0], yPrev = points[p - 1][1];
+ const xNext = points[p+1][0], yNext = points[p + 1][1];
+ angle = Math.atan2(yPrev - yNext, xPrev - xNext);
sin = Math.sin(angle), cos = Math.cos(angle);
- xLeft = x + -sin * offset, yLeft = y + cos * offset;
+ offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2 * width) + extraOffset;
+ const confOffset = Math.atan(c * 5 / widening);
+ extraOffset += confOffset;
+ xLeft = x + -sin * offset, yLeft = y + cos * (offset + confOffset);
riverPointsLeft.push([xLeft, yLeft]);
xRight = x + sin * offset, yRight = y + -cos * offset;
riverPointsRight.unshift([xRight, yRight]);
-
- // generate polygon path and return
- lineGen.curve(d3.curveCatmullRom.alpha(0.1));
- const right = lineGen(riverPointsRight);
- let left = lineGen(riverPointsLeft);
- left = left.substring(left.indexOf("C"));
- return [round(right + left, 2), rn(riverLength, 2)];
}
- const specify = function() {
- if (!pack.rivers.length) return;
- Math.random = aleaPRNG(seed);
- const smallLength = pack.rivers.map(r => r.length||0).sort((a,b) => a-b)[Math.ceil(pack.rivers.length * .15)];
- const smallType = {"Creek":9, "River":3, "Brook":3, "Stream":1}; // weighted small river types
+ // end point
+ x = points[last][0], y = points[last][1], c = points[last][2];
+ if (c) extraOffset += Math.atan(c * 10 / widening); // add extra width on river confluence
+ angle = Math.atan2(points[last-1][1] - y, points[last-1][0] - x);
+ sin = Math.sin(angle), cos = Math.cos(angle);
+ xLeft = x + -sin * offset, yLeft = y + cos * offset;
+ riverPointsLeft.push([xLeft, yLeft]);
+ xRight = x + sin * offset, yRight = y + -cos * offset;
+ riverPointsRight.unshift([xRight, yRight]);
- for (const r of pack.rivers) {
- r.basin = getBasin(r.i, r.parent);
- r.name = getName(r.mouth);
- //debug.append("circle").attr("cx", pack.cells.p[r.mouth][0]).attr("cy", pack.cells.p[r.mouth][1]).attr("r", 2);
- const small = r.length < smallLength;
- r.type = r.parent && !(r.i%6) ? small ? "Branch" : "Fork" : small ? rw(smallType) : "River";
- }
+ // generate polygon path and return
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ const right = lineGen(riverPointsRight);
+ let left = lineGen(riverPointsLeft);
+ left = left.substring(left.indexOf("C"));
+ return [round(right + left, 2), rn(riverLength, 2)];
+}
+
+const specify = function() {
+ if (!pack.rivers.length) return;
+ Math.random = aleaPRNG(seed);
+ const smallLength = pack.rivers.map(r => r.length||0).sort((a,b) => a-b)[Math.ceil(pack.rivers.length * .15)];
+ const smallType = {"Creek":9, "River":3, "Brook":3, "Stream":1}; // weighted small river types
+
+ for (const r of pack.rivers) {
+ r.basin = getBasin(r.i, r.parent);
+ r.name = getName(r.mouth);
+ //debug.append("circle").attr("cx", pack.cells.p[r.mouth][0]).attr("cy", pack.cells.p[r.mouth][1]).attr("r", 2);
+ const small = r.length < smallLength;
+ r.type = r.parent && !(r.i%6) ? small ? "Branch" : "Fork" : small ? rw(smallType) : "River";
}
+}
- const getName = function(cell) {
- return Names.getCulture(pack.cells.culture[cell]);
+const getName = function(cell) {
+ return Names.getCulture(pack.cells.culture[cell]);
+}
+
+// remove river and all its tributaries
+const remove = function(id) {
+ const cells = pack.cells;
+ const riversToRemove = pack.rivers.filter(r => r.i === id || getBasin(r.i, r.parent, id) === id).map(r => r.i);
+ riversToRemove.forEach(r => rivers.select("#river"+r).remove());
+ cells.r.forEach((r, i) => {
+ if (!r || !riversToRemove.includes(r)) return;
+ cells.r[i] = 0;
+ cells.fl[i] = grid.cells.prec[cells.g[i]];
+ cells.conf[i] = 0;
+ });
+ pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
+}
+
+const getBasin = function(r, p, e) {
+ while (p && r !== p && r !== e) {
+ const parent = pack.rivers.find(r => r.i === p);
+ if (!parent) return r;
+ r = parent.i;
+ p = parent.parent;
}
+ return r;
+}
- // remove river and all its tributaries
- const remove = function(id) {
- const cells = pack.cells;
- const riversToRemove = pack.rivers.filter(r => r.i === id || getBasin(r.i, r.parent, id) === id).map(r => r.i);
- riversToRemove.forEach(r => rivers.select("#river"+r).remove());
- cells.r.forEach((r, i) => {
- if (!r || !riversToRemove.includes(r)) return;
- cells.r[i] = 0;
- cells.fl[i] = grid.cells.prec[cells.g[i]];
- cells.conf[i] = 0;
- });
- pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
- }
+return {generate, resolveDepressions, addMeandring, getPath, specify, getName, getBasin, remove};
- const getBasin = function(r, p, e) {
- while (p && r !== p && r !== e) {
- const parent = pack.rivers.find(r => r.i === p);
- if (!parent) return r;
- r = parent.i;
- p = parent.parent;
- }
- return r;
- }
-
- return {generate, resolveDepressions, addMeandring, getPath, specify, getName, getBasin, remove};
-
-})));
+})));
\ No newline at end of file
diff --git a/modules/save-and-load.js b/modules/save-and-load.js
index 3a42be49..f725a6e8 100644
--- a/modules/save-and-load.js
+++ b/modules/save-and-load.js
@@ -1060,27 +1060,32 @@ function parseLoadedData(data) {
}
if (version < 1.5) {
- // v 1.5 added emblems
- emblems = viewbox.append("g").attr("id", "emblems").style("display", "none");
- emblems.append("g").attr("id", "burgEmblems");
- emblems.append("g").attr("id", "provinceEmblems");
- emblems.append("g").attr("id", "stateEmblems");
- regenerateEmblems();
- toggleEmblems();
-
// not need to store default styles from v 1.5
localStorage.removeItem("styleClean");
localStorage.removeItem("styleGloom");
localStorage.removeItem("styleAncient");
localStorage.removeItem("styleMonochrome");
+ // v 1.5 cultures has shield attribute
+ pack.cultures.forEach(culture => {
+ if (culture.removed) return;
+ culture.shield = Cultures.getRandomShield();
+ });
+
// v 1.5 added burg type value
pack.burgs.forEach(burg => {
if (!burg.i || burg.removed) return;
burg.type = BurgsAndStates.getType(burg.cell, burg.port);
});
- BurgsAndStates.getType(cell, false);
+ // v 1.5 added emblems
+ defs.append("g").attr("id", "defs-emblems");
+ emblems = viewbox.insert("g", "#population").attr("id", "emblems").style("display", "none");
+ emblems.append("g").attr("id", "burgEmblems");
+ emblems.append("g").attr("id", "provinceEmblems");
+ emblems.append("g").attr("id", "stateEmblems");
+ regenerateEmblems();
+ toggleEmblems();
}
}()
@@ -1155,7 +1160,7 @@ function parseLoadedData(data) {
invokeActiveZooming();
WARN && console.warn(`TOTAL: ${rn((performance.now()-uploadMap.timeStart)/1000,2)}s`);
- INFO && showStatistics();
+ showStatistics();
INFO && console.groupEnd("Loaded Map " + seed);
tip("Map is successfully loaded", true, "success", 7000);
}
diff --git a/modules/ui/cultures-editor.js b/modules/ui/cultures-editor.js
index c7a6c45e..5f25fb38 100644
--- a/modules/ui/cultures-editor.js
+++ b/modules/ui/cultures-editor.js
@@ -60,6 +60,9 @@ function editCultures() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "", totalArea = 0, totalPopulation = 0;
+ const emblemShapeGroup = document.getElementById("emblemShape").selectedOptions[0].parentNode.label;
+ const selectShape = emblemShapeGroup === "Diversiform"
+
for (const c of pack.cultures) {
if (c.removed) continue;
const area = c.area * (distanceScaleInput.value ** 2);
@@ -73,7 +76,7 @@ function editCultures() {
if (!c.i) {
// Uncultured (neutral) line
lines += `
+ data-area=${area} data-population=${population} data-base=${c.base} data-type="" data-expansionism="" data-emblems="${c.shield}">
@@ -86,26 +89,28 @@ function editCultures() {
${si(population)}
-
+
+ ${selectShape ? `
` : ""}
`;
continue;
}
lines += `
+ data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism} data-emblems="${c.shield}">
${c.cells}
-
+
${si(area) + unit}
${si(population)}
-
+
+ ${selectShape ? `
` : ""}
`;
}
@@ -128,6 +133,7 @@ function editCultures() {
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("change", cultureChangeType));
body.querySelectorAll("div > select.cultureBase").forEach(el => el.addEventListener("change", cultureChangeBase));
+ body.querySelectorAll("div > select.cultureShape").forEach(el => el.addEventListener("change", cultureChangeShape));
body.querySelectorAll("div > div.culturePopulation").forEach(el => el.addEventListener("click", changePopulation));
body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.addEventListener("click", cultureRegenerateBurgs));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", cultureRemove));
@@ -150,6 +156,11 @@ function editCultures() {
return options;
}
+ function getShapeOptions(selected) {
+ const shapes = Object.keys(COA.shields.types).map(type => Object.keys(COA.shields[type])).flat();
+ return shapes.map(shape => `
`);
+ }
+
function cultureHighlightOn(event) {
const culture = +event.target.dataset.id;
const info = document.getElementById("cultureInfo");
@@ -225,6 +236,40 @@ function editCultures() {
this.parentNode.dataset.base = pack.cultures[culture].base = v;
}
+ function cultureChangeShape() {
+ const culture = +this.parentNode.dataset.id;
+ const shape = this.value;
+ this.parentNode.dataset.emblems = pack.cultures[culture].shield = shape;
+
+ const rerenderCOA = (id, coa) => {
+ const coaEl = document.getElementById(id);
+ if (!coaEl) return; // not rendered
+ coaEl.remove();
+ COArenderer.trigger(id, coa);
+ }
+
+ pack.states.forEach(state => {
+ if (state.culture !== culture || !state.i || state.removed || !state.coa || state.coa === "custom") return;
+ if (shape === state.coa.shield) return;
+ state.coa.shield = shape;
+ rerenderCOA("stateCOA" + state.i, state.coa);
+ });
+
+ pack.provinces.forEach(province => {
+ if (pack.cells.culture[province.center] !== culture || !province.i || province.removed || !province.coa || province.coa === "custom") return;
+ if (shape === province.coa.shield) return;
+ province.coa.shield = shape;
+ rerenderCOA("provinceCOA" + province.i, province.coa);
+ });
+
+ pack.burgs.forEach(burg => {
+ if (burg.culture !== culture || !burg.i || burg.removed || !burg.coa || burg.coa === "custom") return;
+ if (shape === burg.coa.shield) return;
+ burg.coa.shield = shape
+ rerenderCOA("burgCOA" + burg.i, burg.coa);
+ });
+ }
+
function changePopulation() {
const culture = +this.parentNode.dataset.id;
const c = pack.cultures[culture];
@@ -293,7 +338,7 @@ function editCultures() {
b.name = Names.getCulture(culture);
labels.select("[data-id='" + b.i +"']").text(b.name);
});
- tip(`Names for ${cBurgs.length} burgs are re-generated`);
+ tip(`Names for ${cBurgs.length} burgs are regenerated`, false, "success");
}
function cultureRemove() {
@@ -306,12 +351,12 @@ function editCultures() {
Remove: function() {
cults.select("#culture"+culture).remove();
debug.select("#cultureCenter"+culture).remove();
-
+
pack.burgs.filter(b => b.culture == culture).forEach(b => b.culture = 0);
pack.states.forEach((s, i) => {if(s.culture === culture) s.culture = 0;});
pack.cells.culture.forEach((c, i) => {if(c === culture) pack.cells.culture[i] = 0;});
pack.cultures[culture].removed = true;
-
+
const origin = pack.cultures[culture].origin;
pack.cultures.forEach(c => {if(c.origin === culture) c.origin = origin;});
refreshCulturesEditor();
@@ -493,8 +538,8 @@ function editCultures() {
culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
culturesHeader.querySelector("div[data-sortby='type']").style.left = "8.8em";
+ culturesHeader.querySelector("div[data-sortby='base']").style.left = "13.6em";
culturesFooter.style.display = "none";
- culturesHeader.querySelector("div[data-sortby='base']").style.marginLeft = "20px";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
$("#culturesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
@@ -589,8 +634,8 @@ function editCultures() {
culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
culturesHeader.querySelector("div[data-sortby='type']").style.left = "18.6em";
+ culturesHeader.querySelector("div[data-sortby='base']").style.left = "35.8em";
culturesFooter.style.display = "block";
- culturesHeader.querySelector("div[data-sortby='base']").style.marginLeft = "2px";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if(!close) $("#culturesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
@@ -634,7 +679,7 @@ function editCultures() {
function downloadCulturesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
- let data = "Id,Culture,Color,Cells,Expansionism,Type,Area "+unit+",Population,Namesbase\n"; // headers
+ let data = "Id,Culture,Color,Cells,Expansionism,Type,Area "+unit+",Population,Namesbase,Emblems Shape\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
data += el.dataset.id + ",";
@@ -646,7 +691,8 @@ function editCultures() {
data += el.dataset.area + ",";
data += el.dataset.population + ",";
const base = +el.dataset.base;
- data += nameBases[base].name + "\n";
+ data += nameBases[base].name + ",";
+ data += el.dataset.emblems + "\n";
});
const name = getFileName("Cultures") + ".csv";
diff --git a/modules/ui/emblems-editor.js b/modules/ui/emblems-editor.js
index 6a8a945b..a2a14520 100644
--- a/modules/ui/emblems-editor.js
+++ b/modules/ui/emblems-editor.js
@@ -13,7 +13,7 @@ function editEmblem(type, id, el) {
updateElementSelectors(type, id, el);
$("#emblemEditor").dialog({
- title: "Edit Emblem", resizable: true, width: "18em", height: "auto",
+ title: "Edit Emblem", resizable: true, width: "18.2em", height: "auto",
position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"},
close: closeEmblemEditor
});
@@ -150,7 +150,8 @@ function editEmblem(type, id, el) {
function changeShape() {
el.coa.shield = this.value;
- document.getElementById(id).remove();
+ const coaEl = document.getElementById(id);
+ if (coaEl) coaEl.remove();
COArenderer.trigger(id, el.coa);
}
@@ -180,7 +181,7 @@ function editEmblem(type, id, el) {
function openInArmoria() {
const coa = el.coa && el.coa !== "custom" ? el.coa : {t1: "sable"};
const json = JSON.stringify(coa).replaceAll("#", "%23");
- const url = `http://azgaar.github.io/Armoria/?coa=${json}`;
+ const url = `https://azgaar.github.io/Armoria/?coa=${json}&from=FMG`;
openURL(url);
}
@@ -232,10 +233,10 @@ function editEmblem(type, id, el) {
buttons.classList.toggle("hidden");
}
- function download(format) {
+ async function download(format) {
const coa = document.getElementById(id);
const size = +emblemsDownloadSize.value;
- const url = getURL(coa, el.coa, size);
+ const url = await getURL(coa, size);
const link = document.createElement("a");
link.download = getFileName(`Emblem ${el.fullName || el.name}`) + "." + format;
@@ -246,7 +247,6 @@ function editEmblem(type, id, el) {
function downloadSVG(url, link) {
link.href = url;
link.click();
- window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
}
function downloadRaster(format, url, link, size) {
@@ -263,52 +263,49 @@ function editEmblem(type, id, el) {
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
- const URL = canvas.toDataURL("image/" + format, .92);
- link.href = URL;
+ const dataURL = canvas.toDataURL("image/" + format, .92);
+ link.href = dataURL;
link.click();
- window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
+ window.setTimeout(() => window.URL.revokeObjectURL(dataURL), 6000);
}
}
- function getURL(svg, coa, size) {
- const serialized = getSVG(svg, coa, size);
- const blob = new Blob([serialized], { type: 'image/svg+xml;charset=utf-8' });
+ async function getURL(svg, size) {
+ const serialized = getSVG(svg, size);
+ const blob = new Blob([serialized], {type: 'image/svg+xml;charset=utf-8'});
const url = window.URL.createObjectURL(blob);
+ window.setTimeout(() => window.URL.revokeObjectURL(url), 6000);
return url;
}
function getSVG(svg, size) {
- const clone = svg.cloneNode(true); // clone svg
+ const clone = svg.cloneNode(true);
clone.setAttribute("width", size);
clone.setAttribute("height", size);
return (new XMLSerializer()).serializeToString(clone);
}
- function downloadGallery() {
+ async function downloadGallery() {
const name = getFileName("Emblems Gallery");
const validStates = pack.states.filter(s => s.i && !s.removed && s.coa);
const validProvinces = pack.provinces.filter(p => p.i && !p.removed && p.coa);
const validBurgs = pack.burgs.filter(b => b.i && !b.removed && b.coa);
- triggerCOALoad(validStates, validProvinces, validBurgs);
- const timeout = (validStates.length + validProvinces.length + validBurgs.length) * 8;
- tip("Preparing to download...", true, "warn", timeout);
- d3.timeout(runDownload, timeout);
+ await renderAllEmblems(validStates, validProvinces, validBurgs);
+ runDownload();
function runDownload() {
const back = `
Go Back`;
const stateSection = `
`;
const provinceSections = validStates.map(state => {
const stateProvinces = validProvinces.filter(p => p.state === state.i);
const figures = stateProvinces.map(province => {
const el = document.getElementById("provinceCOA"+province.i);
- const svg = getSVG(el, province.coa, 200);
- return `
${province.fullName}${svg}`;
+ return `
${province.fullName}${getSVG(el, 200)}`;
}).join("");
return stateProvinces.length ? `
${back}
${state.fullName} provinces
${figures}` : "";
}).join("");
@@ -319,8 +316,7 @@ function editEmblem(type, id, el) {
const provinceBurgs = stateBurgs.filter(b => pack.cells.province[b.cell] === province.i);
const provinceBurgFigures = provinceBurgs.map(burg => {
const el = document.getElementById("burgCOA"+burg.i);
- const svg = getSVG(el, burg.coa, 200);
- return `
${burg.name}${svg}`;
+ return `
${burg.name}${getSVG(el, 200)}`;
}).join("");
return provinceBurgs.length ? `
${back}
${province.fullName} burgs
${provinceBurgFigures}` : "";
}).join("");
@@ -328,8 +324,7 @@ function editEmblem(type, id, el) {
const stateBurgOutOfProvinces = stateBurgs.filter(b => !pack.cells.province[b.cell]);
const stateBurgOutOfProvincesFigures = stateBurgOutOfProvinces.map(burg => {
const el = document.getElementById("burgCOA"+burg.i);
- const svg = getSVG(el, burg.coa, 200);
- return `
${burg.name}${svg}`;
+ return `
${burg.name}${getSVG(el, 200)}`;
}).join("");
if (stateBurgOutOfProvincesFigures) stateBurgSections += `
${state.fullName} burgs under direct control
${stateBurgOutOfProvincesFigures}`;
return stateBurgSections;
@@ -338,8 +333,7 @@ function editEmblem(type, id, el) {
const neutralBurgs = validBurgs.filter(b => !b.state);
const neutralsSection = neutralBurgs.length ? "
Independent burgs
" + neutralBurgs.map(burg => {
const el = document.getElementById("burgCOA"+burg.i);
- const svg = getSVG(el, burg.coa, 200);
- return `${burg.name}${svg}`;
+ return `${burg.name}${getSVG(el, 200)}`;
}).join("") + "" : "";
const FMG = `
Azgaar's Fantasy Map Generator`;
@@ -370,10 +364,15 @@ function editEmblem(type, id, el) {
}
}
- function triggerCOALoad(states, provinces, burgs) {
- states.forEach(state => COArenderer.trigger("stateCOA"+state.i, state.coa));
- provinces.forEach(province => COArenderer.trigger("provinceCOA"+province.i, province.coa));
- burgs.forEach(burg => COArenderer.trigger("burgCOA"+burg.i, burg.coa));
+ async function renderAllEmblems(states, provinces, burgs) {
+ tip("Preparing for download...", true, "warn");
+
+ const statePromises = states.map(state => COArenderer.trigger("stateCOA"+state.i, state.coa));
+ const provincePromises = provinces.map(province => COArenderer.trigger("provinceCOA"+province.i, province.coa));
+ const burgPromises = burgs.map(burg => COArenderer.trigger("burgCOA"+burg.i, burg.coa));
+ const promises = [...statePromises, ...provincePromises, ...burgPromises];
+
+ return Promise.allSettled(promises).then(res => clearMainTip());
}
function dragEmblem() {
diff --git a/modules/ui/general.js b/modules/ui/general.js
index 90070057..99c51095 100644
--- a/modules/ui/general.js
+++ b/modules/ui/general.js
@@ -19,6 +19,12 @@ document.getElementById("dialogs").addEventListener("mousemove", showDataTip);
document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip);
document.getElementById("exitCustomization").addEventListener("mousemove", showDataTip);
+/**
+ * @param {string} tip Tooltip text
+ * @param {boolean} main Show above other tooltips
+ * @param {string} type Message type (color): error, warn, success
+ * @param {number} time Timeout to auto hide, ms
+ */
function tip(tip = "Tip is undefined", main, type, time) {
tooltip.innerHTML = tip;
tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)";
@@ -100,13 +106,13 @@ function showMapTooltip(point, e, i, g) {
parent.id === "provinceEmblems" ? [pack.provinces, "province"] :
[pack.states, "state"];
const i = +e.target.dataset.i;
- highlightEmblemElement(type, g[i]);
+ if (event.shiftKey) highlightEmblemElement(type, g[i]);
d3.select(e.target).raise();
d3.select(parent).raise();
const name = g[i].fullName || g[i].name;
- tip(`${name} ${type} emblem. Click to edit`);
+ tip(`${name} ${type} emblem. Click to edit. Hold Shift to show associated area or place`);
return;
}
if (group === "rivers") {
@@ -297,13 +303,12 @@ function getPopulationTip(i) {
}
function highlightEmblemElement(type, el) {
- if (emblems.selectAll("line, circle").size()) return;
const i = el.i, cells = pack.cells;
const animation = d3.transition().duration(1000).ease(d3.easeSinIn);
if (type === "burg") {
const {x, y} = el;
- emblems.append("circle").attr("cx", x).attr("cy", y).attr("r", 0)
+ debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0)
.attr("fill", "none").attr("stroke", "#d0240f").attr("stroke-width", 1).attr("opacity", 1)
.transition(animation).attr("r", 20).attr("opacity", .1).attr("stroke-width", 0).remove();
return;
@@ -314,7 +319,7 @@ function highlightEmblemElement(type, el) {
const borderCells = cells.i.filter(id => obj[id] === i && cells.c[id].some(n => obj[n] !== i));
const data = Array.from(borderCells).filter((c, i) => !(i%2)).map(i => cells.p[i]).map(i => [i[0], i[1], Math.hypot(i[0]-x, i[1]-y)]);
- emblems.selectAll("line").data(data).enter().append("line")
+ debug.selectAll("line").data(data).enter().append("line")
.attr("x1", x).attr("y1", y).attr("x2", d => d[0]).attr("y2", d => d[1])
.attr("stroke", "#d0240f").attr("stroke-width", .5).attr("opacity", .2)
.attr("stroke-dashoffset", d => d[2]).attr("stroke-dasharray", d => d[2])
@@ -474,6 +479,7 @@ document.addEventListener("keyup", event => {
else if (shift && key === 78) editNamesbase(); // Shift + "N" to edit Namesbase
else if (shift && key === 90) editZones(); // Shift + "Z" to edit Zones
else if (shift && key === 82) editReligions(); // Shift + "R" to edit Religions
+ else if (shift && key === 89) openEmblemEditor(); // Shift + "Y" to edit Emblems
else if (shift && key === 81) editUnits(); // Shift + "Q" to edit Units
else if (shift && key === 79) editNotes(); // Shift + "O" to edit Notes
else if (shift && key === 84) overviewBurgs(); // Shift + "T" to open Burgs overview
diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js
index 73a8006e..34a77e1d 100644
--- a/modules/ui/heightmap-editor.js
+++ b/modules/ui/heightmap-editor.js
@@ -176,7 +176,6 @@ function editHeightmap() {
reGraph();
drawCoastline();
- elevateLakes();
Rivers.generate(change);
if (!change) {
@@ -288,10 +287,7 @@ function editHeightmap() {
reGraph();
drawCoastline();
- if (changeHeights.checked) {
- elevateLakes();
- Rivers.generate(changeHeights.checked);
- }
+ if (changeHeights.checked) Rivers.generate(changeHeights.checked);
// assign saved pack data from grid back to pack
const n = pack.cells.i.length;
@@ -314,7 +310,6 @@ function editHeightmap() {
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
- if (pack.features[pack.cells.f[i]].group === "freshwater") pack.cells.h[i] = 19; // de-elevate lakes
const land = pack.cells.h[i] >= 20;
// check biome
diff --git a/modules/ui/options.js b/modules/ui/options.js
index 50473d62..875438be 100644
--- a/modules/ui/options.js
+++ b/modules/ui/options.js
@@ -289,15 +289,45 @@ function changeCultureSet() {
if (+culturesOutput.value > +max) culturesInput.value = culturesOutput.value = max;
}
-function changeEmblemShape(value) {
+function changeEmblemShape(emblemShape) {
const image = document.getElementById("emblemShapeImage");
- const shapeEl = document.getElementById(value);
- if (shapeEl) {
- const shape = shapeEl.querySelector("path").getAttribute("d");
- image.setAttribute("d", shape);
- } else {
- image.removeAttribute("d");
+ const shapePath = window.COArenderer && COArenderer.shieldPaths[emblemShape];
+ shapePath ? image.setAttribute("d", shapePath) : image.removeAttribute("d");
+
+ const specificShape = ["culture", "state", "random"].includes(emblemShape) ? null : emblemShape;
+ if (emblemShape === "random") pack.cultures.filter(c => !c.removed).forEach(c => c.shield = Cultures.getRandomShield());
+
+ const rerenderCOA = (id, coa) => {
+ const coaEl = document.getElementById(id);
+ if (!coaEl) return; // not rendered
+ coaEl.remove();
+ COArenderer.trigger(id, coa);
}
+
+ pack.states.forEach(state => {
+ if (!state.i || state.removed || !state.coa || state.coa === "custom") return;
+ const newShield = specificShape || COA.getShield(state.culture, null);
+ if (newShield === state.coa.shield) return;
+ state.coa.shield = newShield;
+ rerenderCOA("stateCOA" + state.i, state.coa);
+ });
+
+ pack.provinces.forEach(province => {
+ if (!province.i || province.removed || !province.coa || province.coa === "custom") return;
+ const culture = pack.cells.culture[province.center];
+ const newShield = specificShape || COA.getShield(culture, province.state);
+ if (newShield === province.coa.shield) return;
+ province.coa.shield = newShield;
+ rerenderCOA("provinceCOA" + province.i, province.coa);
+ });
+
+ pack.burgs.forEach(burg => {
+ if (!burg.i || burg.removed || !burg.coa || burg.coa === "custom") return;
+ const newShield = specificShape || COA.getShield(burg.culture, burg.state);
+ if (newShield === burg.coa.shield) return;
+ burg.coa.shield = newShield
+ rerenderCOA("burgCOA" + burg.i, burg.coa);
+ });
}
function changeStatesNumber(value) {
@@ -419,7 +449,6 @@ function randomizeOptions() {
// World settings
generateEra();
- changeEmblemShape(emblemShape.value); // change emblem shape image
}
// select heightmap template pseudo-randomly
diff --git a/modules/ui/tools.js b/modules/ui/tools.js
index d41fbc91..51abcab0 100644
--- a/modules/ui/tools.js
+++ b/modules/ui/tools.js
@@ -14,6 +14,7 @@ toolsContent.addEventListener("click", function(event) {
if (button === "editDiplomacyButton") editDiplomacy(); else
if (button === "editCulturesButton") editCultures(); else
if (button === "editReligions") editReligions(); else
+ if (button === "editEmblemButton") openEmblemEditor(); else
if (button === "editNamesBaseButton") editNamesbase(); else
if (button === "editUnitsButton") editUnits(); else
if (button === "editNotesButton") editNotes(); else
@@ -72,13 +73,28 @@ function processFeatureRegeneration(event, button) {
if (button === "regenerateZones") regenerateZones(event);
}
-function regenerateRivers() {
- elevateLakes();
- Rivers.generate();
- for (const i of pack.cells.i) {
- const f = pack.features[pack.cells.f[i]]; // feature
- if (f.group === "freshwater") pack.cells.h[i] = 19; // de-elevate lakes
+async function openEmblemEditor() {
+ let type, id, el;
+
+ if (pack.states[1]?.coa) {
+ type = "state";
+ id = "stateCOA1";
+ el = pack.states[1];
+ } else if (pack.burgs[1]?.coa) {
+ type = "burg";
+ id = "burgCOA1";
+ el = pack.burgs[1];
+ } else {
+ tip("No emblems to edit, please generate states and burgs first", false, "error");
+ return;
}
+
+ await COArenderer.trigger(id, el.coa);
+ editEmblem(type, id, el);
+}
+
+function regenerateRivers() {
+ Rivers.generate();
Rivers.specify();
if (!layerIsOn("toggleRivers")) toggleRivers();
}
diff --git a/modules/ui/world-configurator.js b/modules/ui/world-configurator.js
index 11eef897..913e4f77 100644
--- a/modules/ui/world-configurator.js
+++ b/modules/ui/world-configurator.js
@@ -45,7 +45,6 @@ function editWorld() {
updateGlobePosition();
calculateTemperatures();
generatePrecipitation();
- elevateLakes();
const heights = new Uint8Array(pack.cells.h);
Rivers.generate();
Rivers.specify();
diff --git a/modules/voronoi.js b/modules/voronoi.js
index f7b82292..f5e4b45c 100644
--- a/modules/voronoi.js
+++ b/modules/voronoi.js
@@ -1,82 +1,135 @@
-(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global.Voronoi = factory());
-}(this, (function () { 'use strict';
+class Voronoi {
+ /**
+ * Creates a Voronoi diagram from the given Delaunator, a list of points, and the number of points. The Voronoi diagram is constructed using (I think) the {@link https://en.wikipedia.org/wiki/Bowyer%E2%80%93Watson_algorithm |Bowyer-Watson Algorithm}
+ * The {@link https://github.com/mapbox/delaunator/ |Delaunator} library uses {@link https://en.wikipedia.org/wiki/Doubly_connected_edge_list |half-edges} to represent the relationship between points and triangles.
+ * @param {{triangles: Uint32Array, halfedges: Int32Array}} delaunay A {@link https://github.com/mapbox/delaunator/blob/master/index.js |Delaunator} instance.
+ * @param {[number, number][]} points A list of coordinates.
+ * @param {number} pointsN The number of points.
+ */
+ constructor(delaunay, points, pointsN) {
+ this.delaunay = delaunay;
+ this.points = points;
+ this.pointsN = pointsN;
+ this.cells = { v: [], c: [], b: [] }; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
+ this.vertices = { p: [], v: [], c: [] }; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
- var Voronoi = function Voronoi(delaunay, points, pointsN) {
- const cells = {v: [], c: [], b: []}; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
- const vertices = {p: [], v: [], c: []}; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
+ // Half-edges are the indices into the delaunator outputs:
+ // delaunay.triangles[e] gives the point ID where the half-edge starts
+ // delaunay.triangles[e] returns either the opposite half-edge in the adjacent triangle, or -1 if there's not an adjacent triangle.
+ for (let e = 0; e < this.delaunay.triangles.length; e++) {
- for (let e=0; e < delaunay.triangles.length; e++) {
-
- const p = delaunay.triangles[nextHalfedge(e)];
- if (p < pointsN && !cells.c[p]) {
- const edges = edgesAroundPoint(e);
- cells.v[p] = edges.map(e => triangleOfEdge(e)); // cell: adjacent vertex
- cells.c[p] = edges.map(e => delaunay.triangles[e]).filter(c => c < pointsN); // cell: adjacent valid cells
- cells.b[p] = edges.length > cells.c[p].length ? 1 : 0; // cell: is border
+ const p = this.delaunay.triangles[this.nextHalfedge(e)];
+ if (p < this.pointsN && !this.cells.c[p]) {
+ const edges = this.edgesAroundPoint(e);
+ this.cells.v[p] = edges.map(e => this.triangleOfEdge(e)); // cell: adjacent vertex
+ this.cells.c[p] = edges.map(e => this.delaunay.triangles[e]).filter(c => c < this.pointsN); // cell: adjacent valid cells
+ this.cells.b[p] = edges.length > this.cells.c[p].length ? 1 : 0; // cell: is border
}
- const t = triangleOfEdge(e);
- if (!vertices.p[t]) {
- vertices.p[t] = triangleCenter(t); // vertex: coordinates
- vertices.v[t] = trianglesAdjacentToTriangle(t); // vertex: adjacent vertices
- vertices.c[t] = pointsOfTriangle(t); // vertex: adjacent cells
+ const t = this.triangleOfEdge(e);
+ if (!this.vertices.p[t]) {
+ this.vertices.p[t] = this.triangleCenter(t); // vertex: coordinates
+ this.vertices.v[t] = this.trianglesAdjacentToTriangle(t); // vertex: adjacent vertices
+ this.vertices.c[t] = this.pointsOfTriangle(t); // vertex: adjacent cells
}
}
-
- function pointsOfTriangle(t) {
- return edgesOfTriangle(t).map(e => delaunay.triangles[e]);
- }
-
- function trianglesAdjacentToTriangle(t) {
- let triangles = [];
- for (let e of edgesOfTriangle(t)) {
- let opposite = delaunay.halfedges[e];
- triangles.push(triangleOfEdge(opposite));
- }
- return triangles;
- }
-
- function edgesAroundPoint(start) {
- let result = [], incoming = start;
- do {
- result.push(incoming);
- const outgoing = nextHalfedge(incoming);
- incoming = delaunay.halfedges[outgoing];
- } while (incoming !== -1 && incoming !== start && result.length < 20);
- return result;
- }
-
- function triangleCenter(t) {
- let vertices = pointsOfTriangle(t).map(p => points[p]);
- return circumcenter(vertices[0], vertices[1], vertices[2]);
- }
-
- return {cells, vertices}
-
}
- function edgesOfTriangle(t) {return [3*t, 3*t+1, 3*t+2];}
+ /**
+ * Gets the IDs of the points comprising the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-points| the Delaunator docs.}
+ * @param {number} t The index of the triangle
+ * @returns {[number, number, number]} The IDs of the points comprising the given triangle.
+ */
+ pointsOfTriangle(t) {
+ return this.edgesOfTriangle(t).map(edge => this.delaunay.triangles[edge]);
+ }
- function triangleOfEdge(e) {return Math.floor(e/3);}
+ /**
+ * Identifies what triangles are adjacent to the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-triangles| the Delaunator docs.}
+ * @param {number} t The index of the triangle
+ * @returns {number[]} The indices of the triangles that share half-edges with this triangle.
+ */
+ trianglesAdjacentToTriangle(t) {
+ let triangles = [];
+ for (let edge of this.edgesOfTriangle(t)) {
+ let opposite = this.delaunay.halfedges[edge];
+ triangles.push(this.triangleOfEdge(opposite));
+ }
+ return triangles;
+ }
- function nextHalfedge(e) {return (e % 3 === 2) ? e-2 : e+1;}
+ /**
+ * Gets the indices of all the incoming and outgoing half-edges that touch the given point. Taken from {@link https://mapbox.github.io/delaunator/#point-to-edges| the Delaunator docs.}
+ * @param {number} start The index of an incoming half-edge that leads to the desired point
+ * @returns {number[]} The indices of all half-edges (incoming or outgoing) that touch the point.
+ */
+ edgesAroundPoint(start) {
+ const result = [];
+ let incoming = start;
+ do {
+ result.push(incoming);
+ const outgoing = this.nextHalfedge(incoming);
+ incoming = this.delaunay.halfedges[outgoing];
+ } while (incoming !== -1 && incoming !== start && result.length < 20);
+ return result;
+ }
- function prevHalfedge(e) {return (e % 3 === 0) ? e+2 : e-1;}
+ /**
+ * Returns the center of the triangle located at the given index.
+ * @param {number} t The index of the triangle
+ * @returns {[number, number]}
+ */
+ triangleCenter(t) {
+ let vertices = this.pointsOfTriangle(t).map(p => this.points[p]);
+ return this.circumcenter(vertices[0], vertices[1], vertices[2]);
+ }
- function circumcenter(a, b, c) {
- let ad = a[0]*a[0] + a[1]*a[1],
- bd = b[0]*b[0] + b[1]*b[1],
- cd = c[0]*c[0] + c[1]*c[1];
- let D = 2 * (a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1]));
+ /**
+ * Retrieves all of the half-edges for a specific triangle `t`. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
+ * @param {number} t The index of the triangle
+ * @returns {[number, number, number]} The edges of the triangle.
+ */
+ edgesOfTriangle(t) { return [3 * t, 3 * t + 1, 3 * t + 2]; }
+
+ /**
+ * Enables lookup of a triangle, given one of the half-edges of that triangle. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
+ * @param {number} e The index of the edge
+ * @returns {number} The index of the triangle
+ */
+ triangleOfEdge(e) { return Math.floor(e / 3); }
+
+ /**
+ * Moves to the next half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
+ * @param {number} e The index of the current half edge
+ * @returns {number} The index of the next half edge
+ */
+ nextHalfedge(e) { return (e % 3 === 2) ? e - 2 : e + 1; }
+
+ /**
+ * Moves to the previous half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
+ * @param {number} e The index of the current half edge
+ * @returns {number} The index of the previous half edge
+ */
+ prevHalfedge(e) { return (e % 3 === 0) ? e + 2 : e - 1; }
+
+ /**
+ * Finds the circumcenter of the triangle identified by points a, b, and c. Taken from {@link https://en.wikipedia.org/wiki/Circumscribed_circle#Circumcenter_coordinates| Wikipedia}
+ * @param {[number, number]} a The coordinates of the first point of the triangle
+ * @param {[number, number]} b The coordinates of the second point of the triangle
+ * @param {[number, number]} c The coordinates of the third point of the triangle
+ * @return {[number, number]} The coordinates of the circumcenter of the triangle.
+ */
+ circumcenter(a, b, c) {
+ const [ax, ay] = a;
+ const [bx, by] = b;
+ const [cx, cy] = c;
+ const ad = ax * ax + ay * ay;
+ const bd = bx * bx + by * by;
+ const cd = cx * cx + cy * cy;
+ const D = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
return [
- Math.floor(1/D * (ad * (b[1] - c[1]) + bd * (c[1] - a[1]) + cd * (a[1] - b[1]))),
- Math.floor(1/D * (ad * (c[0] - b[0]) + bd * (a[0] - c[0]) + cd * (b[0] - a[0])))
+ Math.floor(1 / D * (ad * (by - cy) + bd * (cy - ay) + cd * (ay - by))),
+ Math.floor(1 / D * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax)))
];
}
-
- return Voronoi;
-
-})));
\ No newline at end of file
+}
\ No newline at end of file