diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1104b06f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +run_php_server.bat +.vscode \ No newline at end of file diff --git a/Fantasy Map Generator.lnk b/Fantasy Map Generator.lnk deleted file mode 100644 index 1e1f2012..00000000 Binary files a/Fantasy Map Generator.lnk and /dev/null differ diff --git a/README.md b/README.md index d5fe4479..3161d67c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Fantasy Map Generator -Azgaar's _Fantasy Map Generator_. Online tool generating interactive and highly customizable svg maps based on voronoi diagram. +Azgaar's _Fantasy Map Generator_ is a free client-side web application generating interactive and highly customizable svg maps based on voronoi diagram. -Project is under development, check out the current version [here](https://azgaar.github.io/Fantasy-Map-Generator). You can also try an Electron desktop application - download [an archive](https://github.com/Azgaar/Fantasy-Map-Generator/releases) for your architecture, unzip and run the _Azgaar's Fantasy Map Generator.exe_. +Project is under development, the current version is available on [Github Pages](https://azgaar.github.io/Fantasy-Map-Generator). -Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for a guidance. Some details are covered in my blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com), you may also keep an eye on my [Trello devboard](https://trello.com/b/7x832DG4/fantasy-map-generator). +Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for a guidance. Some details are covered in my old blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com), you may also keep an eye on my [Trello devboard](https://trello.com/b/7x832DG4/fantasy-map-generator). [![preview](https://cdn.discordapp.com/attachments/587406457725779968/594840629213659136/preview1.png)](https://i.redd.it/8bf81ir2cy631.png) @@ -14,7 +14,11 @@ Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki Join our [Reddit community](https://www.reddit.com/r/FantasyMapGenerator) and [Discord server](https://discordapp.com/invite/X7E84HU) to share the created maps, discuss the Generator, suggest ideas and get a most recent updates. You may also contact me directly via [email](mailto:azgaar.fmg@yandex.by). For bug reports please use the project [issues page](https://github.com/Azgaar/Fantasy-Map-Generator/issues) or Discord "Bugs" channel. If you are facing performance issues, please read [the tips](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Tips#performance-tips). -You can support the project [on Patreon](https://www.patreon.com/azgaar). +Electron desktop application is available in [releases](https://github.com/Azgaar/Fantasy-Map-Generator/releases). Download archive for your architecture, unzip and run. + +Pull requests are welcomed. The Tool codebase is messy and requires re-design, but I will appreciate if you start with minor changes. + +You can support the project on [Patreon](https://www.patreon.com/azgaar). _Inspiration:_ diff --git a/_config.yml b/_config.yml deleted file mode 100644 index c7418817..00000000 --- a/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-slate \ No newline at end of file diff --git a/index.css b/index.css index 948b66d5..2fac268a 100644 --- a/index.css +++ b/index.css @@ -833,7 +833,7 @@ body button.noicon { } #brushesButtons > button { - padding: 0; + padding: .3em; } #brushesButtons svg { @@ -1088,6 +1088,7 @@ i.resetButton:active { box-shadow: inset 1px 1px 0 0 #ccc; border-color: #a6a6da; background-color: #ecd8d8; + border-radius: 10%; } .ui-dialog input[type="range"] { @@ -1187,7 +1188,7 @@ div.slider .ui-slider-handle { #brushPower, #brushRadius { - width: 8em; + width: 12em; } #rescaleHigher, @@ -1261,7 +1262,7 @@ div.states { line-height: 1.5em; } -div.states:hover { +div.states:hover, div.states.hovered { border: 1px solid #c4c4c4; background-image: linear-gradient(to right, #dedede 100%, #f2f2f2 50%, #fcfcfc 0%); } diff --git a/index.html b/index.html index 963a72d8..e94c3839 100644 --- a/index.html +++ b/index.html @@ -7,8 +7,6 @@ - - @@ -34,13 +32,9 @@ #loading-text span:nth-child(3), #mapOverlay > span:nth-child(3) {animation-delay: 2s;} @keyframes blink {0% {opacity: 0;} 20% {opacity: 1;} 100% {opacity: .1;}} - - - - @@ -2649,8 +2643,8 @@ -
+
diff --git a/modules/save-and-load.js b/modules/save-and-load.js index 7ee574a5..4cd42ad8 100644 --- a/modules/save-and-load.js +++ b/modules/save-and-load.js @@ -286,7 +286,6 @@ function getMapData() { TIME && console.timeEnd("createMapDataBlob"); resolve(blob); }); - } // Download .map file @@ -306,116 +305,103 @@ async function saveMap() { } function saveGeoJSON_Cells() { - let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n"; - const cells = pack.cells, v = pack.vertices; + const json = {type: "FeatureCollection", features: []}; + const cells = pack.cells; const getPopulation = i => {const [r, u] = getCellPopulation(i); return rn(r+u)}; + const getHeight = i => parseInt(getFriendlyHeight([cells.p[i][0],cells.p[i][1]])); cells.i.forEach(i => { - data += "{\n \"type\": \"Feature\",\n \"geometry\": { \"type\": \"Polygon\", \"coordinates\": [["; - cells.v[i].forEach(n => { - let x = mapCoordinates.lonW + (v.p[n][0] / graphWidth) * mapCoordinates.lonT; - let y = mapCoordinates.latN - (v.p[n][1] / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise - data += "["+x+","+y+"],"; - }); - // close the ring - let x = mapCoordinates.lonW + (v.p[cells.v[i][0]][0] / graphWidth) * mapCoordinates.lonT; - let y = mapCoordinates.latN - (v.p[cells.v[i][0]][1] / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise - data += "["+x+","+y+"]"; - data += "]] },\n \"properties\": {\n"; + const coordinates = getCellPoints(cells.v[i]); + const height = getHeight(i); + const biome = cells.biome[i]; + const type = pack.features[cells.f[i]].type; + const population = getPopulation(i); + const state = cells.state[i]; + const province = cells.province[i]; + const culture = cells.culture[i]; + const religion = cells.religion[i]; + const neighbors = cells.c[i]; - const height = parseInt(getFriendlyHeight([cells.p[i][0],cells.p[i][1]])); - - data += " \"id\": \""+i+"\",\n"; - data += " \"height\": \""+height+"\",\n"; - data += " \"biome\": \""+cells.biome[i]+"\",\n"; - data += " \"type\": \""+pack.features[cells.f[i]].type+"\",\n"; - data += " \"population\": \""+getPopulation(i)+"\",\n"; - data += " \"state\": \""+cells.state[i]+"\",\n"; - data += " \"province\": \""+cells.province[i]+"\",\n"; - data += " \"culture\": \""+cells.culture[i]+"\",\n"; - data += " \"religion\": \""+cells.religion[i]+"\",\n"; - data += " \"neighbors\": ["+cells.c[i]+"]\n"; - data +=" }\n},\n"; + const properties = {id:i, height, biome, type, population, state, province, culture, religion, neighbors} + const feature = {type: "Feature", geometry: {type: "Polygon", coordinates}, properties}; + json.features.push(feature); }); - data = data.substring(0, data.length - 2)+"\n"; // remove trailing comma - data += "]}"; - const name = getFileName("Cells") + ".geojson"; - downloadFile(data, name, "application/json"); + downloadFile(JSON.stringify(json), name, "application/json"); } -function saveGeoJSON_Roads() { - let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n"; +function saveGeoJSON_Routes() { + const json = {type: "FeatureCollection", features: []}; - routes._groups[0][0].childNodes.forEach(n => { - n.childNodes.forEach(r => { - data += "{\n \"type\": \"Feature\",\n \"geometry\": { \"type\": \"LineString\", \"coordinates\": "; - data += JSON.stringify(getRoadPoints(r)); - data += " },\n \"properties\": {\n"; - data += " \"id\": \""+r.id+"\",\n"; - data += " \"type\": \""+n.id+"\"\n"; - data +=" }\n},\n"; - }); + routes.selectAll("g > path").each(function() { + const coordinates = getRoutePoints(this); + const id = this.id; + const type = this.parentElement.id; + + const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, type}}; + json.features.push(feature); }); - data = data.substring(0, data.length - 2)+"\n"; // remove trailing comma - data += "]}"; const name = getFileName("Routes") + ".geojson"; - downloadFile(data, name, "application/json"); + downloadFile(JSON.stringify(json), name, "application/json"); } function saveGeoJSON_Rivers() { - let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n"; + const json = {type: "FeatureCollection", features: []}; - rivers._groups[0][0].childNodes.forEach(n => { - data += "{\n \"type\": \"Feature\",\n \"geometry\": { \"type\": \"LineString\", \"coordinates\": "; - data += JSON.stringify(getRiverPoints(n)); - data += " },\n \"properties\": {\n"; - data += " \"id\": \""+n.id+"\",\n"; - data += " \"width\": \""+n.dataset.width+"\",\n"; - data += " \"increment\": \""+n.dataset.increment+"\"\n"; - data +=" }\n},\n"; + rivers.selectAll("path").each(function() { + const coordinates = getRiverPoints(this); + const id = this.id; + const width = +this.dataset.increment; + const increment = +this.dataset.increment; + const river = pack.rivers.find(r => r.i === +id.slice(5)); + const name = river ? river.name : ""; + const type = river ? river.type : ""; + const i = river ? river.i : ""; + const basin = river ? river.basin : ""; + + const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, i, basin, name, type, width, increment}}; + json.features.push(feature); }); - data = data.substring(0, data.length - 2)+"\n"; // remove trailing comma - data += "]}"; const name = getFileName("Rivers") + ".geojson"; - downloadFile(data, name, "application/json"); + downloadFile(JSON.stringify(json), name, "application/json"); } function saveGeoJSON_Markers() { - let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n"; + const json = {type: "FeatureCollection", features: []}; - markers._groups[0][0].childNodes.forEach(n => { - let x = mapCoordinates.lonW + (n.dataset.x / graphWidth) * mapCoordinates.lonT; - let y = mapCoordinates.latN - (n.dataset.y / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise - - data += "{\n \"type\": \"Feature\",\n \"geometry\": { \"type\": \"Point\", \"coordinates\": ["+x+", "+y+"]"; - data += " },\n \"properties\": {\n"; - data += " \"id\": \""+n.id+"\",\n"; - data += " \"type\": \""+n.dataset.id.substring(8)+"\"\n"; - data +=" }\n},\n"; + markers.selectAll("use").each(function() { + const coordinates = getQGIScoordinates(this.dataset.x, this.dataset.y); + const id = this.id; + const type = (this.dataset.id).substring(1); + const icon = document.getElementById(type).textContent; + const note = notes.length ? notes.find(note => note.id === this.id) : null; + const name = note ? note.name : ""; + const legend = note ? note.legend : ""; + const feature = {type: "Feature", geometry: {type: "Point", coordinates}, properties: {id, type, icon, name, legend}}; + json.features.push(feature); }); - data = data.substring(0, data.length - 2)+"\n"; // remove trailing comma - data += "]}"; const name = getFileName("Markers") + ".geojson"; - downloadFile(data, name, "application/json"); + downloadFile(JSON.stringify(json), name, "application/json"); } -function getRoadPoints(node) { +function getCellPoints(vertices) { + const p = pack.vertices.p; + const points = vertices.map(n => getQGIScoordinates(p[n][0] / graphWidth, p[n][1] / graphHeight)); + return points.concat([points[0]]); +} + +function getRoutePoints(node) { let points = []; const l = node.getTotalLength(); const increment = l / Math.ceil(l / 2); for (let i=0; i <= l; i += increment) { const p = node.getPointAtLength(i); - - let x = mapCoordinates.lonW + (p.x / graphWidth) * mapCoordinates.lonT; - let y = mapCoordinates.latN - (p.y / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise - - points.push([x,y]); + points.push(getQGIScoordinates(p.x, p.y)); } return points; } @@ -427,9 +413,7 @@ function getRiverPoints(node) { for (let i=l, c=i; i >= 0; i -= increment, c += increment) { const p1 = node.getPointAtLength(i); const p2 = node.getPointAtLength(c); - - let x = mapCoordinates.lonW + (((p1.x+p2.x)/2) / graphWidth) * mapCoordinates.lonT; - let y = mapCoordinates.latN - (((p1.y+p2.y)/2) / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise + const [x, y] = getQGIScoordinates((p1.x + p2.x) / 2, (p1.y + p2.y) / 2); points.push([x,y]); } return points; diff --git a/modules/ui/general.js b/modules/ui/general.js index 7689b1b5..ca6ee9f7 100644 --- a/modules/ui/general.js +++ b/modules/ui/general.js @@ -27,7 +27,7 @@ function tip(tip = "Tip is undefined", main, type, time) { if (type === "success") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #127912cc, #ffffff00)"; if (main) tooltip.dataset.main = tip; // set main tip - if (time) setTimeout(tooltip.dataset.main = "", time); // clear main in some time + if (time) setTimeout(() => tooltip.dataset.main = "", time); // clear main in some time } function showMainTip() { @@ -49,7 +49,8 @@ function showDataTip(e) { tip(dataTip); } -function moved() { +const moved = debounce(mouseMove, 100); +function mouseMove() { const point = d3.mouse(this); const i = findCell(point[0], point[1]); // pack cell id if (i === undefined) return; @@ -89,11 +90,28 @@ function showMapTooltip(point, e, i, g) { const land = pack.cells.h[i] >= 20; // specific elements - if (group === "armies") {tip(e.target.parentNode.dataset.name + ". Click to edit"); return;} - if (group === "rivers") {tip(getRiverName(e.target.id) + "Click to edit"); return;} + if (group === "armies") { + tip(e.target.parentNode.dataset.name + ". Click to edit"); + return; + } + if (group === "rivers") { + const river = +e.target.id.slice(5); + const r = pack.rivers.find(r => r.i === river); + const name = r ? r.name + " " + r.type : ""; + tip(name + ". Click to edit"); + if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000); + return; + } if (group === "routes") {tip("Click to edit the Route"); return;} if (group === "terrain") {tip("Click to edit the Relief Icon"); return;} - if (subgroup === "burgLabels" || subgroup === "burgIcons") {tip("Click to open Burg Editor"); return;} + if (subgroup === "burgLabels" || subgroup === "burgIcons") { + const burg = +path[path.length - 10].dataset.id; + const b = pack.burgs[burg]; + const population = si(b.population * populationRate.value * urbanization.value); + tip(`${b.name}. Population: ${population}. Click to edit`); + if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000); + return; + } if (group === "labels") {tip("Click to edit the Label"); return;} if (group === "markers") {tip("Click to edit the Marker"); return;} if (group === "ruler") { @@ -106,32 +124,54 @@ function showMapTooltip(point, e, i, g) { if (subgroup === "burgLabels") {tip("Click to edit the Burg"); return;} if (group === "lakes" && !land) {tip(`${capitalize(subgroup)} lake. Click to edit`); return;} if (group === "coastline") {tip("Click to edit the coastline"); return;} - if (group === "zones") {tip(path[path.length-8].dataset.description); return;} + if (group === "zones") { + const zone = path[path.length-8]; + tip(zone.dataset.description); + if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000); + return; + } if (group === "ice") {tip("Click to edit the Ice"); return;} // covering elements if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else if (layerIsOn("togglePopulation")) tip(getPopulationTip(i)); else if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g])); else - if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) tip("Biome: " + biomesData.name[pack.cells.biome[i]]); else + if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) { + const biome = pack.cells.biome[i] + tip("Biome: " + biomesData.name[biome]); + if (biomesEditor.offsetParent) highlightEditorLine(biomesEditor, biome); + } else if (layerIsOn("toggleReligions") && pack.cells.religion[i]) { - const religion = pack.religions[pack.cells.religion[i]]; - const type = religion.type === "Cult" || religion.type == "Heresy" ? religion.type : religion.type + " religion"; - tip(type + ": " + religion.name); + const religion = pack.cells.religion[i]; + const r = pack.religions[religion]; + const type = r.type === "Cult" || r.type == "Heresy" ? r.type : r.type + " religion"; + tip(type + ": " + r.name); + if (religionsEditor.offsetParent) highlightEditorLine(religionsEditor, religion); } else if (pack.cells.state[i] && (layerIsOn("toggleProvinces") || layerIsOn("toggleStates"))) { - const state = pack.states[pack.cells.state[i]].fullName; + const state = pack.cells.state[i]; + const stateName = pack.states[state].fullName; const province = pack.cells.province[i]; const prov = province ? pack.provinces[province].fullName + ", " : ""; - tip(prov + state); + tip(prov + stateName); + if (statesEditor.offsetParent) highlightEditorLine(statesEditor, state); + if (diplomacyEditor.offsetParent) highlightEditorLine(diplomacyEditor, state); + if (militaryOverview.offsetParent) highlightEditorLine(militaryOverview, state); + if (provincesEditor.offsetParent) highlightEditorLine(provincesEditor, province); + } else + if (layerIsOn("toggleCultures") && pack.cells.culture[i]) { + const culture = pack.cells.culture[i]; + tip("Culture: " + pack.cultures[culture].name); + if (culturesEditor.offsetParent) highlightEditorLine(culturesEditor, culture); } else - if (layerIsOn("toggleCultures") && pack.cells.culture[i]) tip("Culture: " + pack.cultures[pack.cells.culture[i]].name); else if (layerIsOn("toggleHeight")) tip("Height: " + getFriendlyHeight(point)); } -function getRiverName(id) { - const r = pack.rivers.find(r => r.i == id.slice(5)); - return r ? r.name + " " + r.type + ". " : ""; +function highlightEditorLine(editor, id, timeout = 15000) { + Array.from(editor.getElementsByClassName("states hovered")).forEach(el => el.classList.remove("hovered")); // clear all hovered + const hovered = Array.from(editor.querySelectorAll("div")).find(el => el.dataset.id == id); + if (hovered) hovered.classList.add("hovered"); // add hovered class + if (timeout) setTimeout(() => hovered.classList.remove("hovered"), timeout); } // get cell info on mouse move diff --git a/modules/ui/options.js b/modules/ui/options.js index b46b541d..4abe22ea 100644 --- a/modules/ui/options.js +++ b/modules/ui/options.js @@ -86,7 +86,9 @@ function showSupporters() { Seth Fusion,Adam Butler,Gus,StroboWolf,Sadie Blackthorne,Zewen Senpai,Dell McKnight,Oneiris,Darinius Dragonclaw Studios,Christopher Whitney,Rhodes HvZ, Jeppe Skov Jensen,María Martín López,Martin Seeger,Annie Rishor,Aram Sabatés,MadNomadMedia,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta, Thirty-OneR ,ThatGuyGW ,Dee Chiu,MontyBoosh ,Achillain ,Jaden ,SashaTK,Steve Johnson,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,Thirty-OneR, - ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,Andrew Rostaing,Daniel Gill`; + ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,Andrew Rostaing,Daniel Gill, + Char, Jack, Barna Csíkos, Ian Rousseau, Nicholas Grabstas, Tom Van Orden jr, Bryan Brake, Akylos, Riley Seaman`; + const array = supporters.replace(/(?:\r\n|\r|\n)/g, "").split(",").map(v => capitalize(v.trim())).sort(); alertMessage.innerHTML = ""; $("#alert").dialog({resizable: false,title: "Patreon Supporters",width: "54vw",position: {my: "center",at: "center",of: "svg"}}); @@ -481,7 +483,7 @@ function saveGeoJSON() { $("#alert").dialog({title: "GIS data export", resizable: false, width: "35em", position: {my: "center", at: "center", of: "svg"}, buttons: { Cells: saveGeoJSON_Cells, - Routes: saveGeoJSON_Roads, + Routes: saveGeoJSON_Routes, Rivers: saveGeoJSON_Rivers, Markers: saveGeoJSON_Markers, Close: function() {$(this).dialog("close");} diff --git a/modules/utils.js b/modules/utils.js index 352c7b59..005d0738 100644 --- a/modules/utils.js +++ b/modules/utils.js @@ -516,26 +516,15 @@ function getNextId(core, i = 1) { return core + i; } -// from https://davidwalsh.name/javascript-debounce-function -function debounce(func, wait, immediate) { - var timeout; - return function() { - var context = this, args = arguments; - var later = function() { - timeout = null; - if (!immediate) func.apply(context, args); - } - var callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) func.apply(context, args); - } -} +function debounce(f, ms) { + let isCooldown = false; -// pause/block JS execution for a while -function sleep(delay) { - const start = new Date().getTime(); - while (new Date().getTime() < start + delay); + return function() { + if (isCooldown) return; + f.apply(this, arguments); + isCooldown = true; + setTimeout(() => isCooldown = false, ms); + }; } // parse error to get the readable string in Chrome and Firefox @@ -597,6 +586,12 @@ function generateDate(from = 100, to = 1000) { return new Date(rand(from, to),rand(12),rand(31)).toLocaleDateString("en", {year:'numeric', month:'long', day:'numeric'}); } +function getQGIScoordinates(x, y) { + const cx = mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT; + const cy = mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise + return [cx, cy]; +} + // prompt replacer (prompt does not work in Electron) void function() { const prompt = document.getElementById("prompt");