mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-16 17:31:24 +01:00
Merge pull request #821 from Azgaar/heightmap-select-dialog
Heightmap select dialog
This commit is contained in:
commit
b76a0b83dc
34 changed files with 1455 additions and 926 deletions
|
|
@ -1,6 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
window.HeightmapTemplates = (function () {
|
||||
const heightmapTemplates = (function () {
|
||||
const volcano = `Hill 1 90-100 44-56 40-60
|
||||
Multiply 0.8 50-100 0 0
|
||||
Range 1.5 30-55 45-55 40-60
|
||||
|
|
@ -148,20 +148,19 @@ window.HeightmapTemplates = (function () {
|
|||
Range 6-8 40-50 5-95 10-90`;
|
||||
|
||||
return {
|
||||
volcano,
|
||||
highIsland,
|
||||
lowIsland,
|
||||
continents,
|
||||
archipelago,
|
||||
atoll,
|
||||
mediterranean,
|
||||
peninsula,
|
||||
peninsula,
|
||||
pangea,
|
||||
isthmus,
|
||||
shattered,
|
||||
taklamakan,
|
||||
oldWorld,
|
||||
fractious
|
||||
volcano: {id: 0, name: "Volcano", template: volcano, probability: 3},
|
||||
highIsland: {id: 1, name: "High Island", template: highIsland, probability: 19},
|
||||
lowIsland: {id: 2, name: "Low Island", template: lowIsland, probability: 9},
|
||||
continents: {id: 3, name: "Continents", template: continents, probability: 16},
|
||||
archipelago: {id: 4, name: "Archipelago", template: archipelago, probability: 18},
|
||||
atoll: {id: 5, name: "Atoll", template: atoll, probability: 1},
|
||||
mediterranean: {id: 6, name: "Mediterranean", template: mediterranean, probability: 5},
|
||||
peninsula: {id: 7, name: "Peninsula", template: peninsula, probability: 3},
|
||||
pangea: {id: 8, name: "Pangea", template: pangea, probability: 5},
|
||||
isthmus: {id: 9, name: "Isthmus", template: isthmus, probability: 2},
|
||||
shattered: {id: 10, name: "Shattered", template: shattered, probability: 7},
|
||||
taklamakan: {id: 11, name: "Taklamakan", template: taklamakan, probability: 1},
|
||||
oldWorld: {id: 12, name: "Old World", template: oldWorld, probability: 8},
|
||||
fractious: {id: 13, name: "Fractious", template: fractious, probability: 3}
|
||||
};
|
||||
})();
|
||||
27
config/precreated-heightmaps.js
Normal file
27
config/precreated-heightmaps.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"use strict";
|
||||
|
||||
const precreatedHeightmaps = {
|
||||
"africa-centric": {id: 0, name: "Africa Centric"},
|
||||
arabia: {id: 1, name: "Arabia"},
|
||||
atlantics: {id: 2, name: "Atlantics"},
|
||||
britain: {id: 3, name: "Britain"},
|
||||
caribbean: {id: 4, name: "Caribbean"},
|
||||
"east-asia": {id: 5, name: "East Asia"},
|
||||
eurasia: {id: 6, name: "Eurasia"},
|
||||
europe: {id: 7, name: "Europe"},
|
||||
"europe-accented": {id: 8, name: "Europe Accented"},
|
||||
"europe-and-central-asia": {id: 9, name: "Europe and Central Asia"},
|
||||
"europe-central": {id: 10, name: "Europe Central"},
|
||||
"europe-north": {id: 11, name: "Europe North"},
|
||||
greenland: {id: 12, name: "Greenland"},
|
||||
hellenica: {id: 13, name: "Hellenica"},
|
||||
iceland: {id: 14, name: "Iceland"},
|
||||
"indian-ocean": {id: 15, name: "Indian Ocean"},
|
||||
"mediterranean-sea": {id: 16, name: "Mediterranean Sea"},
|
||||
"middle-east": {id: 17, name: "Middle East"},
|
||||
"north-america": {id: 18, name: "North America"},
|
||||
"us-centric": {id: 19, name: "US-centric"},
|
||||
"us-mainland": {id: 20, name: "US Mainland"},
|
||||
world: {id: 21, name: "World"},
|
||||
"world-from-pacific": {id: 22, name: "World from Pacific"}
|
||||
};
|
||||
77
index.css
77
index.css
|
|
@ -347,6 +347,20 @@ text.drag {
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
#optionsTrigger {
|
||||
padding: 0.6em 0.45em;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
#optionsTrigger {
|
||||
font-size: 2em;
|
||||
padding: 0;
|
||||
width: 1.3em;
|
||||
height: 1.6em;
|
||||
border: solid 1px #5e4fa2;
|
||||
}
|
||||
}
|
||||
|
||||
#options {
|
||||
position: absolute;
|
||||
font-family: Consolas, monospace;
|
||||
|
|
@ -929,7 +943,7 @@ fieldset {
|
|||
padding: 0.1em 0.5em;
|
||||
float: left;
|
||||
font-size: 1.2em;
|
||||
font-family: Copperplate, monospace;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#brushesButtons > button {
|
||||
|
|
@ -1318,6 +1332,13 @@ div.slider .ui-slider-handle {
|
|||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.table {
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog::-webkit-scrollbar,
|
||||
#alertMessage::-webkit-scrollbar,
|
||||
.table::-webkit-scrollbar,
|
||||
.matrix-table::-webkit-scrollbar {
|
||||
|
|
@ -1326,6 +1347,7 @@ div.slider .ui-slider-handle {
|
|||
background-color: transparent;
|
||||
}
|
||||
|
||||
.dialog::-webkit-scrollbar-thumb,
|
||||
#alertMessage::-webkit-scrollbar-thumb,
|
||||
.table::-webkit-scrollbar-thumb,
|
||||
.matrix-table::-webkit-scrollbar-thumb {
|
||||
|
|
@ -1333,30 +1355,30 @@ div.slider .ui-slider-handle {
|
|||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.dialog::-webkit-scrollbar-thumb:hover,
|
||||
#alertMessage::-webkit-scrollbar-thumb:hover,
|
||||
.table::-webkit-scrollbar-thumb:hover,
|
||||
.matrix-table::-webkit-scrollbar-thumb:hover {
|
||||
background: #666;
|
||||
}
|
||||
|
||||
.overflow {
|
||||
.dialog {
|
||||
max-width: 93vw;
|
||||
overflow: auto;
|
||||
max-height: 75vh;
|
||||
}
|
||||
|
||||
.overflow > div {
|
||||
.dialog > div {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
div.header > div {
|
||||
div.header {
|
||||
display: grid;
|
||||
width: 0;
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
display: inline-block;
|
||||
position: sticky;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
div.header > div:first-child {
|
||||
margin-left: 1.8em;
|
||||
}
|
||||
|
||||
.sortable {
|
||||
|
|
@ -1655,11 +1677,6 @@ div.states > div.biomeArea {
|
|||
width: 5em;
|
||||
}
|
||||
|
||||
#militaryHeader > div,
|
||||
#regimentsHeader > div {
|
||||
width: 5.2em;
|
||||
}
|
||||
|
||||
#militaryBody div.states > input {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
|
@ -1978,12 +1995,9 @@ input[type="checkbox"] {
|
|||
}
|
||||
|
||||
div.textual select,
|
||||
div.textual textarea {
|
||||
font-family: Copperplate, monospace;
|
||||
}
|
||||
|
||||
div.textual textarea,
|
||||
div.textual input {
|
||||
font-family: Copperplate, monospace;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
div.textual fieldset {
|
||||
|
|
@ -1998,7 +2012,7 @@ div.textual span,
|
|||
}
|
||||
|
||||
#namesbaseExamples {
|
||||
font-family: Copperplate, monospace;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
@ -2063,7 +2077,7 @@ div.textual span,
|
|||
outline: 0;
|
||||
overflow-y: auto;
|
||||
padding: 0.6em;
|
||||
font-family: Copperplate, monospace;
|
||||
font-family: monospace;
|
||||
background-color: #fff;
|
||||
border: 1px solid #dedede;
|
||||
color: #000;
|
||||
|
|
@ -2092,6 +2106,7 @@ svg.button {
|
|||
#reliefIconsDiv {
|
||||
margin-top: 2px;
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#reliefIconsDiv svg {
|
||||
|
|
@ -2301,11 +2316,8 @@ svg.button {
|
|||
|
||||
#mapOverlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
|
|
@ -2340,17 +2352,6 @@ svg.button {
|
|||
}
|
||||
|
||||
@media only screen and (max-width: 420px) {
|
||||
#collapsible,
|
||||
#options {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#options {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
802
index.html
802
index.html
File diff suppressed because it is too large
Load diff
2
libs/jquery-ui.css
vendored
2
libs/jquery-ui.css
vendored
|
|
@ -359,7 +359,7 @@ body .ui-dialog {
|
|||
padding: 0.5em 1em;
|
||||
background: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.ui-dialog .ui-dialog-buttonpane {
|
||||
text-align: left;
|
||||
|
|
|
|||
148
main.js
148
main.js
|
|
@ -10,6 +10,9 @@ const TIME = DEBUG || !PRODUCTION;
|
|||
const WARN = true;
|
||||
const ERROR = true;
|
||||
|
||||
// detect device
|
||||
const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile;
|
||||
|
||||
if (PRODUCTION && "serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker.register("./sw.js").catch(err => {
|
||||
|
|
@ -201,9 +204,6 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
d3.select("#loading-text").transition().duration(1000).style("opacity", 0);
|
||||
d3.select("#init-rose").transition().duration(4000).style("opacity", 0);
|
||||
} else {
|
||||
hideLoading();
|
||||
await checkLoadParameters();
|
||||
|
|
@ -212,15 +212,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||
});
|
||||
|
||||
function hideLoading() {
|
||||
d3.select("#loading").transition().duration(4000).style("opacity", 0);
|
||||
d3.select("#initial").transition().duration(4000).attr("opacity", 0);
|
||||
d3.select("#optionsContainer").transition().duration(3000).style("opacity", 1);
|
||||
d3.select("#tooltip").transition().duration(4000).style("opacity", 1);
|
||||
d3.select("#loading").transition().duration(3000).style("opacity", 0);
|
||||
d3.select("#optionsContainer").transition().duration(2000).style("opacity", 1);
|
||||
d3.select("#tooltip").transition().duration(3000).style("opacity", 1);
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
d3.select("#loading").transition().duration(200).style("opacity", 1);
|
||||
d3.select("#initial").transition().duration(200).attr("opacity", 1);
|
||||
d3.select("#optionsContainer").transition().duration(100).style("opacity", 0);
|
||||
d3.select("#tooltip").transition().duration(200).style("opacity", 0);
|
||||
}
|
||||
|
|
@ -628,18 +626,22 @@ void (function addDragToUpload() {
|
|||
});
|
||||
})();
|
||||
|
||||
async function generate() {
|
||||
async function generate(options) {
|
||||
try {
|
||||
const timeStart = performance.now();
|
||||
const {seed: precreatedSeed, graph: precreatedGraph} = options || {};
|
||||
|
||||
invokeActiveZooming();
|
||||
generateSeed();
|
||||
setSeed(precreatedSeed);
|
||||
INFO && console.group("Generated Map " + seed);
|
||||
|
||||
applyMapSize();
|
||||
randomizeOptions();
|
||||
placePoints();
|
||||
calculateVoronoi(grid, grid.points);
|
||||
drawScaleBar(scale);
|
||||
await HeightmapGenerator.generate();
|
||||
|
||||
if (shouldRegenerateGrid(grid)) grid = precreatedGraph || generateGrid();
|
||||
else delete grid.cells.h;
|
||||
grid.cells.h = await HeightmapGenerator.generate(grid);
|
||||
|
||||
markFeatures();
|
||||
markupGridOcean();
|
||||
addLakesInDeepDepressions();
|
||||
|
|
@ -650,6 +652,7 @@ async function generate() {
|
|||
calculateMapCoordinates();
|
||||
calculateTemperatures();
|
||||
generatePrecipitation();
|
||||
|
||||
reGraph();
|
||||
drawCoastline();
|
||||
|
||||
|
|
@ -677,6 +680,8 @@ async function generate() {
|
|||
Military.generate();
|
||||
Markers.generate();
|
||||
addZones();
|
||||
|
||||
drawScaleBar(scale);
|
||||
Names.getMapName();
|
||||
|
||||
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
|
||||
|
|
@ -711,57 +716,32 @@ async function generate() {
|
|||
}
|
||||
}
|
||||
|
||||
// generate map seed (string!) or get it from URL searchParams
|
||||
function generateSeed() {
|
||||
const first = !mapHistory[0];
|
||||
const url = new URL(window.location.href);
|
||||
const params = url.searchParams;
|
||||
const urlSeed = url.searchParams.get("seed");
|
||||
if (first && params.get("from") === "MFCG" && urlSeed.length === 13) seed = urlSeed.slice(0, -4);
|
||||
else if (first && urlSeed) seed = urlSeed;
|
||||
else if (optionsSeed.value && optionsSeed.value != seed) seed = optionsSeed.value;
|
||||
else seed = Math.floor(Math.random() * 1e9).toString();
|
||||
optionsSeed.value = seed;
|
||||
// set map seed (string!)
|
||||
function setSeed(precreatedSeed) {
|
||||
if (!precreatedSeed) {
|
||||
const first = !mapHistory[0];
|
||||
const url = new URL(window.location.href);
|
||||
const params = url.searchParams;
|
||||
const urlSeed = url.searchParams.get("seed");
|
||||
if (first && params.get("from") === "MFCG" && urlSeed.length === 13) seed = urlSeed.slice(0, -4);
|
||||
else if (first && urlSeed) seed = urlSeed;
|
||||
else if (optionsSeed.value && optionsSeed.value != seed) seed = optionsSeed.value;
|
||||
else seed = generateSeed();
|
||||
} else {
|
||||
seed = precreatedSeed;
|
||||
}
|
||||
|
||||
byId("optionsSeed").value = seed;
|
||||
Math.random = aleaPRNG(seed);
|
||||
}
|
||||
|
||||
// Place points to calculate Voronoi diagram
|
||||
function placePoints() {
|
||||
TIME && console.time("placePoints");
|
||||
Math.random = aleaPRNG(seed); // reset PRNG
|
||||
|
||||
const cellsDesired = +pointsInput.dataset.cells;
|
||||
const spacing = (grid.spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2)); // spacing between points before jirrering
|
||||
grid.boundary = getBoundaryPoints(graphWidth, graphHeight, spacing);
|
||||
grid.points = getJitteredGrid(graphWidth, graphHeight, spacing); // jittered square grid
|
||||
grid.cellsX = Math.floor((graphWidth + 0.5 * spacing - 1e-10) / spacing);
|
||||
grid.cellsY = Math.floor((graphHeight + 0.5 * spacing - 1e-10) / spacing);
|
||||
TIME && console.timeEnd("placePoints");
|
||||
}
|
||||
|
||||
// calculate Delaunay and then Voronoi diagram
|
||||
function calculateVoronoi(graph, points) {
|
||||
TIME && console.time("calculateDelaunay");
|
||||
const n = points.length;
|
||||
const allPoints = points.concat(grid.boundary);
|
||||
const delaunay = Delaunator.from(allPoints);
|
||||
TIME && console.timeEnd("calculateDelaunay");
|
||||
|
||||
TIME && console.time("calculateVoronoi");
|
||||
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;
|
||||
TIME && console.timeEnd("calculateVoronoi");
|
||||
}
|
||||
|
||||
// Mark features (ocean, lakes, islands) and calculate distance field
|
||||
function markFeatures() {
|
||||
TIME && console.time("markFeatures");
|
||||
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
|
||||
|
||||
const cells = grid.cells,
|
||||
heights = grid.cells.h;
|
||||
const cells = grid.cells;
|
||||
const heights = grid.cells.h;
|
||||
cells.f = new Uint16Array(cells.i.length); // cell feature number
|
||||
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land coast; -1 = water near coast
|
||||
grid.features = [0];
|
||||
|
|
@ -879,7 +859,7 @@ function addLakesInDeepDepressions() {
|
|||
|
||||
// near sea lakes usually get a lot of water inflow, most of them should brake threshold and flow out to sea (see Ancylus Lake)
|
||||
function openNearSeaLakes() {
|
||||
if (templateInput.value === "Atoll") return; // no need for Atolls
|
||||
if (byId("templateInput").value === "Atoll") return; // no need for Atolls
|
||||
|
||||
const cells = grid.cells;
|
||||
const features = grid.features;
|
||||
|
|
@ -924,7 +904,7 @@ function defineMapSize() {
|
|||
if (randomize || !locked("latitude")) latitudeOutput.value = latitudeInput.value = rn(latitude);
|
||||
|
||||
function getSizeAndLatitude() {
|
||||
const template = document.getElementById("templateInput").value; // heightmap template
|
||||
const template = byId("templateInput").value; // heightmap template
|
||||
|
||||
if (template === "africa-centric") return [45, 53];
|
||||
if (template === "arabia") return [20, 35];
|
||||
|
|
@ -1181,8 +1161,8 @@ function generatePrecipitation() {
|
|||
// recalculate Voronoi Graph to pack cells
|
||||
function reGraph() {
|
||||
TIME && console.time("reGraph");
|
||||
let {cells, points, features} = grid;
|
||||
const newCells = {p: [], g: [], h: []}; // to store new data
|
||||
const {cells, points, features} = grid;
|
||||
const newCells = {p: [], g: [], h: []}; // store new data
|
||||
const spacing2 = grid.spacing ** 2;
|
||||
|
||||
for (const i of cells.i) {
|
||||
|
|
@ -1216,14 +1196,17 @@ function reGraph() {
|
|||
newCells.h.push(height);
|
||||
}
|
||||
|
||||
calculateVoronoi(pack, newCells.p);
|
||||
cells = pack.cells;
|
||||
cells.p = newCells.p; // points coordinates [x, y]
|
||||
cells.g = grid.cells.i.length < 65535 ? Uint16Array.from(newCells.g) : Uint32Array.from(newCells.g); // reference to initial grid cell
|
||||
cells.q = d3.quadtree(cells.p.map((p, d) => [p[0], p[1], d])); // points quadtree for fast search
|
||||
cells.h = new Uint8Array(newCells.h); // heights
|
||||
cells.area = new Uint16Array(cells.i.length); // cell area
|
||||
cells.i.forEach(i => (cells.area[i] = Math.abs(d3.polygonArea(getPackPolygon(i)))));
|
||||
function getCellArea(i) {
|
||||
const area = Math.abs(d3.polygonArea(getPackPolygon(i)));
|
||||
return Math.min(area, 65535);
|
||||
}
|
||||
|
||||
pack = calculateVoronoi(newCells.p, grid.boundary);
|
||||
pack.cells.p = newCells.p;
|
||||
pack.cells.g = getTypedArray(grid.points.length).from(newCells.g);
|
||||
pack.cells.q = d3.quadtree(newCells.p.map(([x, y], i) => [x, y, i]));
|
||||
pack.cells.h = getTypedArray(100).from(newCells.h);
|
||||
pack.cells.area = getTypedArray(65535).from(pack.cells.i).map(getCellArea);
|
||||
|
||||
TIME && console.timeEnd("reGraph");
|
||||
}
|
||||
|
|
@ -1901,11 +1884,14 @@ function addZones(number = 1) {
|
|||
|
||||
// show map stats on generation complete
|
||||
function showStatistics() {
|
||||
const template = templateInput.options[templateInput.selectedIndex].text;
|
||||
const templateRandom = locked("template") ? "" : "(random)";
|
||||
const heightmap = byId("templateInput").value;
|
||||
const isTemplate = heightmap in heightmapTemplates;
|
||||
const heightmapType = isTemplate ? "template" : "precreated";
|
||||
const isRandomTemplate = isTemplate && !locked("template") ? "random " : "";
|
||||
|
||||
const stats = ` Seed: ${seed}
|
||||
Canvas size: ${graphWidth}x${graphHeight}
|
||||
Template: ${template} ${templateRandom}
|
||||
Canvas size: ${graphWidth}x${graphHeight} px
|
||||
Heightmap: ${heightmap} (${isRandomTemplate}${heightmapType})
|
||||
Points: ${grid.points.length}
|
||||
Cells: ${pack.cells.i.length}
|
||||
Map size: ${mapSizeOutput.value}%
|
||||
|
|
@ -1917,22 +1903,28 @@ function showStatistics() {
|
|||
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});
|
||||
mapHistory.push({seed, width: graphWidth, height: graphHeight, template: heightmap, created: mapId});
|
||||
INFO && console.log(stats);
|
||||
}
|
||||
|
||||
const regenerateMap = debounce(async function () {
|
||||
const regenerateMap = debounce(async function (options) {
|
||||
WARN && console.warn("Generate new random map");
|
||||
showLoading();
|
||||
|
||||
const cellsDesired = +byId("pointsInput").dataset.cells;
|
||||
const shouldShowLoading = cellsDesired > 10000;
|
||||
shouldShowLoading && showLoading();
|
||||
|
||||
closeDialogs("#worldConfigurator, #options3d");
|
||||
customization = 0;
|
||||
resetZoom(1000);
|
||||
undraw();
|
||||
await generate();
|
||||
await generate(options);
|
||||
restoreLayers();
|
||||
if (ThreeD.options.isOn) ThreeD.redraw();
|
||||
if ($("#worldConfigurator").is(":visible")) editWorld();
|
||||
hideLoading();
|
||||
|
||||
shouldShowLoading && hideLoading();
|
||||
clearMainTip();
|
||||
}, 1000);
|
||||
|
||||
// clear the map
|
||||
|
|
|
|||
|
|
@ -1077,7 +1077,7 @@ window.BurgsAndStates = (function () {
|
|||
|
||||
const generateProvinces = function (regenerate) {
|
||||
TIME && console.time("generateProvinces");
|
||||
const localSeed = regenerate ? Math.floor(Math.random() * 1e9).toString() : seed;
|
||||
const localSeed = regenerate ? generateSeed() : seed;
|
||||
Math.random = aleaPRNG(localSeed);
|
||||
|
||||
const {cells, states, burgs} = pack;
|
||||
|
|
|
|||
|
|
@ -520,4 +520,9 @@ export function resolveVersionConflicts(version) {
|
|||
if (!zone.dataset.type) zone.dataset.type = "Unknown";
|
||||
});
|
||||
}
|
||||
|
||||
if (version < 1.84) {
|
||||
// v1.84.0 added grid.cellsDesired to stored data
|
||||
if (!grid.cellsDesired) grid.cellsDesired = rn((graphWidth * graphHeight) / grid.spacing ** 2, -3);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ export function open() {
|
|||
$("#culturesEditor").dialog({
|
||||
title: "Cultures Editor",
|
||||
resizable: false,
|
||||
width: fitContent(),
|
||||
close: closeCulturesEditor,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
||||
});
|
||||
|
|
@ -24,16 +23,16 @@ export function open() {
|
|||
}
|
||||
|
||||
function insertEditorHtml() {
|
||||
const editorHtml = /* html */ `<div id="culturesEditor" class="dialog stable" style="display: none">
|
||||
<div id="culturesHeader" class="header">
|
||||
<div style="left: 1.8em" data-tip="Click to sort by culture name" class="sortable alphabetically" data-sortby="name">Culture </div>
|
||||
<div style="left: 9.9em" data-tip="Click to sort by type" class="sortable alphabetically" data-sortby="type">Type </div>
|
||||
<div style="left: 16.2em" data-tip="Click to sort by culture namesbase" class="sortable" data-sortby="base">Namesbase </div>
|
||||
<div style="left: 24.5em" data-tip="Click to sort by culture cells count" class="sortable hide" data-sortby="cells">Cells </div>
|
||||
<div style="left: 29.8em" data-tip="Click to sort by expansionism" class="sortable hide" data-sortby="expansionism">Expansion </div>
|
||||
<div style="left: 37.2em" data-tip="Click to sort by culture area" class="sortable hide" data-sortby="area">Area </div>
|
||||
<div style="left: 42.8em" data-tip="Click to sort by culture population" class="sortable hide icon-sort-number-down" data-sortby="population">Population </div>
|
||||
<div style="left: 50.8em" data-tip="Click to sort by culture emblems shape" class="sortable alphabetically hide" data-sortby="emblems">Emblems </div>
|
||||
const editorHtml = /* html */ `<div id="culturesEditor" class="dialog stable">
|
||||
<div id="culturesHeader" class="header" style="grid-template-columns: 10em 7em 8em 4em 8em 5em 8em 8em">
|
||||
<div data-tip="Click to sort by culture name" class="sortable alphabetically" data-sortby="name">Culture </div>
|
||||
<div data-tip="Click to sort by type" class="sortable alphabetically" data-sortby="type">Type </div>
|
||||
<div data-tip="Click to sort by culture namesbase" class="sortable" data-sortby="base">Namesbase </div>
|
||||
<div data-tip="Click to sort by culture cells count" class="sortable hide" data-sortby="cells">Cells </div>
|
||||
<div data-tip="Click to sort by expansionism" class="sortable hide" data-sortby="expansionism">Expansion </div>
|
||||
<div data-tip="Click to sort by culture area" class="sortable hide" data-sortby="area">Area </div>
|
||||
<div data-tip="Click to sort by culture population" class="sortable hide icon-sort-number-down" data-sortby="population">Population </div>
|
||||
<div data-tip="Click to sort by culture emblems shape" class="sortable alphabetically hide" data-sortby="emblems">Emblems </div>
|
||||
</div>
|
||||
<div id="culturesBody" class="table" data-type="absolute"></div>
|
||||
|
||||
|
|
@ -52,7 +51,7 @@ function insertEditorHtml() {
|
|||
<button id="culturesHeirarchy" data-tip="Show cultures hierarchy tree" class="icon-sitemap"></button>
|
||||
<button id="culturesManually" data-tip="Manually re-assign cultures" class="icon-brush"></button>
|
||||
<div id="culturesManuallyButtons" style="display: none">
|
||||
<label data-tip="Change brush size. Shortcut: + (increase), – (decrease)" class="italic">Brush size:
|
||||
<label data-tip="Change brush size" data-shortcut="+ (increase), – (decrease)" class="italic">Brush size:
|
||||
<input
|
||||
id="culturesManuallyBrush"
|
||||
oninput="tip('Brush size: '+this.value); culturesManuallyBrushNumber.value = this.value"
|
||||
|
|
|
|||
|
|
@ -14,25 +14,24 @@ export function open() {
|
|||
$("#statesEditor").dialog({
|
||||
title: "States Editor",
|
||||
resizable: false,
|
||||
width: fitContent(),
|
||||
close: closeStatesEditor,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||
});
|
||||
}
|
||||
|
||||
function insertEditorHtml() {
|
||||
const editorHtml = /* html */ `<div id="statesEditor" class="dialog stable" style="display: none">
|
||||
<div id="statesHeader" class="header">
|
||||
<div style="left: 1.8em" data-tip="Click to sort by state name" class="sortable alphabetically" data-sortby="name">State </div>
|
||||
<div style="left: 10.8em" data-tip="Click to sort by state form name" class="sortable alphabetically" data-sortby="form">Form </div>
|
||||
<div style="left: 19.1em" data-tip="Click to sort by capital name" class="sortable alphabetically hide" data-sortby="capital">Capital </div>
|
||||
<div style="left: 26.1em" data-tip="Click to sort by state dominant culture" class="sortable alphabetically hide" data-sortby="culture">Culture </div>
|
||||
<div style="left: 33.4em" data-tip="Click to sort by state burgs count" class="sortable hide" data-sortby="burgs">Burgs </div>
|
||||
<div style="left: 39.6em" data-tip="Click to sort by state area" class="sortable hide icon-sort-number-down" data-sortby="area">Area </div>
|
||||
<div style="left: 45.9em" data-tip="Click to sort by state population" class="sortable hide" data-sortby="population">Population </div>
|
||||
<div style="left: 52.2em" data-tip="Click to sort by state type" class="sortable alphabetically hidden show hide" data-sortby="type">Type </div>
|
||||
<div style="left: 59em" data-tip="Click to sort by state expansion value" class="sortable hidden show hide" data-sortby="expansionism">Expansion </div>
|
||||
<div style="left: 65.5em" data-tip="Click to sort by state cells count" class="sortable hidden show hide" data-sortby="cells">Cells </div>
|
||||
const editorHtml = /* html */ `<div id="statesEditor" class="dialog stable">
|
||||
<div id="statesHeader" class="header" style="grid-template-columns: 11em 8em 7em 7em 6em 6em 8em 6em 7em 6em">
|
||||
<div data-tip="Click to sort by state name" class="sortable alphabetically" data-sortby="name">State </div>
|
||||
<div data-tip="Click to sort by state form name" class="sortable alphabetically" data-sortby="form">Form </div>
|
||||
<div data-tip="Click to sort by capital name" class="sortable alphabetically hide" data-sortby="capital">Capital </div>
|
||||
<div data-tip="Click to sort by state dominant culture" class="sortable alphabetically hide" data-sortby="culture">Culture </div>
|
||||
<div data-tip="Click to sort by state burgs count" class="sortable hide" data-sortby="burgs">Burgs </div>
|
||||
<div data-tip="Click to sort by state area" class="sortable hide icon-sort-number-down" data-sortby="area">Area </div>
|
||||
<div data-tip="Click to sort by state population" class="sortable hide" data-sortby="population">Population </div>
|
||||
<div data-tip="Click to sort by state type" class="sortable alphabetically hidden show hide" data-sortby="type">Type </div>
|
||||
<div data-tip="Click to sort by state expansion value" class="sortable hidden show hide" data-sortby="expansionism">Expansion </div>
|
||||
<div data-tip="Click to sort by state cells count" class="sortable hidden show hide" data-sortby="cells">Cells </div>
|
||||
</div>
|
||||
|
||||
<div id="statesBodySection" class="table" data-type="absolute"></div>
|
||||
|
|
@ -90,7 +89,7 @@ function insertEditorHtml() {
|
|||
|
||||
<button id="statesManually" data-tip="Manually re-assign states" class="icon-brush"></button>
|
||||
<div id="statesManuallyButtons" style="display: none">
|
||||
<label data-tip="Change brush size. Shortcut: + (increase), – (decrease)" class="italic"
|
||||
<label data-tip="Change brush size" data-shortcut="+ (increase), – (decrease)" class="italic"
|
||||
>Brush size:
|
||||
<input
|
||||
id="statesManuallyBrush"
|
||||
|
|
|
|||
342
modules/dynamic/heightmap-selection.js
Normal file
342
modules/dynamic/heightmap-selection.js
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
const initialSeed = generateSeed();
|
||||
let graph = getGraph(grid);
|
||||
|
||||
appendStyleSheet();
|
||||
insertEditorHtml();
|
||||
addListeners();
|
||||
|
||||
export function open() {
|
||||
closeDialogs(".stable");
|
||||
|
||||
const $templateInput = byId("templateInput");
|
||||
setSelected($templateInput.value);
|
||||
graph = getGraph(graph);
|
||||
|
||||
$("#heightmapSelection").dialog({
|
||||
title: "Select Heightmap",
|
||||
resizable: false,
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
},
|
||||
Select: function () {
|
||||
const id = getSelected();
|
||||
applyOption($templateInput, id, getName(id));
|
||||
lock("template");
|
||||
|
||||
$(this).dialog("close");
|
||||
},
|
||||
"New Map": function () {
|
||||
const id = getSelected();
|
||||
applyOption($templateInput, id, getName(id));
|
||||
lock("template");
|
||||
|
||||
const seed = getSeed();
|
||||
regeneratePrompt({seed, graph});
|
||||
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function appendStyleSheet() {
|
||||
const styles = /* css */ `
|
||||
div.dialog > div.heightmap-selection {
|
||||
width: 70vw;
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.heightmap-selection_container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
grid-gap: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.heightmap-selection_container {
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
grid-gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2000px) {
|
||||
.heightmap-selection_container {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
grid-gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.heightmap-selection_options {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
|
||||
.heightmap-selection_options > div:first-child {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
align-items: center;
|
||||
justify-self: start;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.heightmap-selection_options {
|
||||
grid-template-columns: 3fr 1fr;
|
||||
}
|
||||
|
||||
.heightmap-selection_options > div:first-child {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.heightmap-selection_options > div:last-child {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.heightmap-selection article {
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.1s ease-in-out;
|
||||
filter: drop-shadow(1px 1px 4px #999);
|
||||
}
|
||||
|
||||
.heightmap-selection article:hover {
|
||||
background-color: #ddd;
|
||||
filter: drop-shadow(1px 1px 8px #999);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.heightmap-selection article.selected {
|
||||
background-color: #ccc;
|
||||
outline: 1px solid var(--dark-solid);
|
||||
filter: drop-shadow(1px 1px 8px #999);
|
||||
}
|
||||
|
||||
.heightmap-selection article > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 2px 1px;
|
||||
}
|
||||
|
||||
.heightmap-selection article > img {
|
||||
width: 100%;
|
||||
aspect-ratio: ${graphWidth}/${graphHeight};
|
||||
border-radius: 8px;
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
.heightmap-selection article .regeneratePreview {
|
||||
outline: 1px solid #bbb;
|
||||
padding: 1px 3px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.heightmap-selection article .regeneratePreview:hover {
|
||||
outline: 1px solid #666;
|
||||
}
|
||||
|
||||
.heightmap-selection article .regeneratePreview:active {
|
||||
outline: 1px solid #333;
|
||||
color: #000;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.appendChild(document.createTextNode(styles));
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function insertEditorHtml() {
|
||||
const heightmapSelectionHtml = /* html */ `<div id="heightmapSelection" class="dialog stable">
|
||||
<div class="heightmap-selection">
|
||||
<section data-tip="Select heightmap template – template provides unique, but similar-looking maps on generation">
|
||||
<header><h1>Heightmap templates</h1></header>
|
||||
<div class="heightmap-selection_container"></div>
|
||||
</section>
|
||||
<section data-tip="Select precreated heightmap – it will be the same for each map">
|
||||
<header><h1>Precreated heightmaps</h1></header>
|
||||
<div class="heightmap-selection_container"></div>
|
||||
</section>
|
||||
<section>
|
||||
<header><h1>Options</h1></header>
|
||||
<div class="heightmap-selection_options">
|
||||
<div>
|
||||
<label data-tip="Rerender all preview images" class="checkbox-label" id="heightmapSelectionRedrawPreview">
|
||||
<i class="icon-cw"></i>
|
||||
Redraw preview
|
||||
</label>
|
||||
<div>
|
||||
<input id="heightmapSelectionRenderOcean" class="checkbox" type="checkbox" />
|
||||
<label data-tip="Draw heights of water cells" for="heightmapSelectionRenderOcean" class="checkbox-label">Render ocean heights</label>
|
||||
</div>
|
||||
<div data-tip="Color scheme used for heightmap preview">
|
||||
Color scheme
|
||||
<select id="heightmapSelectionColorScheme">
|
||||
<option value="bright" selected>Bright</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="green">Green</option>
|
||||
<option value="monochrome">Monochrome</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button data-tip="Open Template Editor" data-tool="templateEditor" id="heightmapSelectionEditTemplates">Edit Templates</button>
|
||||
<button data-tip="Open Image Converter" data-tool="imageConverter" id="heightmapSelectionImportHeightmap">Import Heightmap</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
byId("dialogs").insertAdjacentHTML("beforeend", heightmapSelectionHtml);
|
||||
|
||||
const sections = document.getElementsByClassName("heightmap-selection_container");
|
||||
|
||||
sections[0].innerHTML = Object.keys(heightmapTemplates)
|
||||
.map(key => {
|
||||
const name = heightmapTemplates[key].name;
|
||||
Math.random = aleaPRNG(initialSeed);
|
||||
const heights = HeightmapGenerator.fromTemplate(graph, key);
|
||||
const dataUrl = drawHeights(heights);
|
||||
|
||||
return /* html */ `<article data-id="${key}" data-seed="${initialSeed}">
|
||||
<img src="${dataUrl}" alt="${name}" />
|
||||
<div>
|
||||
${name}
|
||||
<span data-tip="Regenerate preview" class="icon-cw regeneratePreview"></span>
|
||||
</div>
|
||||
</article>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
sections[1].innerHTML = Object.keys(precreatedHeightmaps)
|
||||
.map(key => {
|
||||
const name = precreatedHeightmaps[key].name;
|
||||
drawPrecreatedHeightmap(key);
|
||||
|
||||
return /* html */ `<article data-id="${key}" data-seed="${initialSeed}">
|
||||
<img alt="${name}" />
|
||||
<div>${name}</div>
|
||||
</article>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function addListeners() {
|
||||
byId("heightmapSelection").on("click", event => {
|
||||
const article = event.target.closest("#heightmapSelection article");
|
||||
if (!article) return;
|
||||
|
||||
const id = article.dataset.id;
|
||||
if (event.target.matches("span.icon-cw")) regeneratePreview(article, id);
|
||||
setSelected(id);
|
||||
});
|
||||
|
||||
byId("heightmapSelectionRenderOcean").on("change", redrawAll);
|
||||
byId("heightmapSelectionColorScheme").on("change", redrawAll);
|
||||
byId("heightmapSelectionRedrawPreview").on("click", redrawAll);
|
||||
byId("heightmapSelectionEditTemplates").on("click", confirmHeightmapEdit);
|
||||
byId("heightmapSelectionImportHeightmap").on("click", confirmHeightmapEdit);
|
||||
}
|
||||
|
||||
function getSelected() {
|
||||
return byId("heightmapSelection").querySelector(".selected")?.dataset?.id;
|
||||
}
|
||||
|
||||
function setSelected(id) {
|
||||
const $heightmapSelection = byId("heightmapSelection");
|
||||
$heightmapSelection.querySelector(".selected")?.classList?.remove("selected");
|
||||
$heightmapSelection.querySelector(`[data-id="${id}"]`)?.classList?.add("selected");
|
||||
}
|
||||
|
||||
function getSeed() {
|
||||
return byId("heightmapSelection").querySelector(".selected")?.dataset?.seed;
|
||||
}
|
||||
|
||||
function getName(id) {
|
||||
const isTemplate = id in heightmapTemplates;
|
||||
return isTemplate ? heightmapTemplates[id].name : precreatedHeightmaps[id].name;
|
||||
}
|
||||
|
||||
function getGraph(currentGraph) {
|
||||
const newGraph = shouldRegenerateGrid(currentGraph) ? generateGrid() : deepCopy(currentGraph);
|
||||
delete newGraph.cells.h;
|
||||
return newGraph;
|
||||
}
|
||||
|
||||
function drawHeights(heights) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = graph.cellsX;
|
||||
canvas.height = graph.cellsY;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const imageData = ctx.createImageData(graph.cellsX, graph.cellsY);
|
||||
|
||||
const schemeId = byId("heightmapSelectionColorScheme").value;
|
||||
const scheme = getColorScheme(schemeId);
|
||||
const renderOcean = byId("heightmapSelectionRenderOcean").checked;
|
||||
const getHeight = height => (height < 20 ? (renderOcean ? height : 0) : height);
|
||||
|
||||
for (let i = 0; i < heights.length; i++) {
|
||||
const color = scheme(1 - getHeight(heights[i]) / 100);
|
||||
const {r, g, b} = d3.color(color);
|
||||
|
||||
const n = i * 4;
|
||||
imageData.data[n] = r;
|
||||
imageData.data[n + 1] = g;
|
||||
imageData.data[n + 2] = b;
|
||||
imageData.data[n + 3] = 255;
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
return canvas.toDataURL("image/png");
|
||||
}
|
||||
|
||||
function drawTemplatePreview(id) {
|
||||
const heights = HeightmapGenerator.fromTemplate(graph, id);
|
||||
const dataUrl = drawHeights(heights);
|
||||
const article = byId("heightmapSelection").querySelector(`[data-id="${id}"]`);
|
||||
article.querySelector("img").src = dataUrl;
|
||||
}
|
||||
|
||||
async function drawPrecreatedHeightmap(id) {
|
||||
const heights = await HeightmapGenerator.fromPrecreated(graph, id);
|
||||
const dataUrl = drawHeights(heights);
|
||||
const article = byId("heightmapSelection").querySelector(`[data-id="${id}"]`);
|
||||
article.querySelector("img").src = dataUrl;
|
||||
}
|
||||
|
||||
function regeneratePreview(article, id) {
|
||||
graph = getGraph(graph);
|
||||
const seed = generateSeed();
|
||||
article.dataset.seed = seed;
|
||||
Math.random = aleaPRNG(seed);
|
||||
drawTemplatePreview(id);
|
||||
}
|
||||
|
||||
function redrawAll() {
|
||||
graph = getGraph(graph);
|
||||
const articles = byId("heightmapSelection").querySelectorAll(`article`);
|
||||
for (const article of articles) {
|
||||
const {id, seed} = article.dataset;
|
||||
Math.random = aleaPRNG(seed);
|
||||
|
||||
const isTemplate = id in heightmapTemplates;
|
||||
if (isTemplate) drawTemplatePreview(id);
|
||||
else drawPrecreatedHeightmap(id);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmHeightmapEdit() {
|
||||
const tool = this.dataset.tool;
|
||||
|
||||
confirmationDialog({
|
||||
title: this.dataset.tip,
|
||||
message: "Opening the tool will erase the current map. Are you sure you want to proceed?",
|
||||
confirm: "Continue",
|
||||
onConfirm: () => editHeightmap({mode: "erase", tool})
|
||||
});
|
||||
}
|
||||
|
|
@ -1,116 +1,140 @@
|
|||
"use strict";
|
||||
|
||||
window.HeightmapGenerator = (function () {
|
||||
let cells, p;
|
||||
let grid = null;
|
||||
let heights = null;
|
||||
let blobPower;
|
||||
let linePower;
|
||||
|
||||
const generate = async function () {
|
||||
cells = grid.cells;
|
||||
p = grid.points;
|
||||
cells.h = new Uint8Array(grid.points.length);
|
||||
const setGraph = graph => {
|
||||
const {cellsDesired, cells, points} = graph;
|
||||
heights = cells.h || createTypedArray({maxValue: 100, length: points.length});
|
||||
blobPower = getBlobPower(cellsDesired);
|
||||
linePower = getLinePower(cellsDesired);
|
||||
grid = graph;
|
||||
};
|
||||
|
||||
const input = document.getElementById("templateInput");
|
||||
const selectedId = input.selectedIndex >= 0 ? input.selectedIndex : 0;
|
||||
const type = input.options[selectedId]?.parentElement?.label;
|
||||
const getHeights = () => heights;
|
||||
|
||||
if (type === "Specific") {
|
||||
// pre-defined heightmap
|
||||
TIME && console.time("defineHeightmap");
|
||||
return new Promise(resolve => {
|
||||
// create canvas where 1px correcponds to a cell
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const {cellsX, cellsY} = grid;
|
||||
canvas.width = cellsX;
|
||||
canvas.height = cellsY;
|
||||
const clearData = () => {
|
||||
heights = null;
|
||||
grid = null;
|
||||
};
|
||||
|
||||
// load heightmap into image and render to canvas
|
||||
const img = new Image();
|
||||
img.src = `./heightmaps/${input.value}.png`;
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0, cellsX, cellsY);
|
||||
const imageData = ctx.getImageData(0, 0, cellsX, cellsY);
|
||||
assignColorsToHeight(imageData.data);
|
||||
canvas.remove();
|
||||
img.remove();
|
||||
TIME && console.timeEnd("defineHeightmap");
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// heightmap template
|
||||
TIME && console.time("generateHeightmap");
|
||||
const template = input.value;
|
||||
const templateString = HeightmapTemplates[template];
|
||||
const fromTemplate = (graph, id) => {
|
||||
const templateString = heightmapTemplates[id]?.template || "";
|
||||
const steps = templateString.split("\n");
|
||||
|
||||
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${template}. Steps: ${steps}`);
|
||||
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`);
|
||||
setGraph(graph);
|
||||
|
||||
for (const step of steps) {
|
||||
const elements = step.trim().split(" ");
|
||||
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${template}. Step: ${elements}`);
|
||||
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${id}. Step: ${elements}`);
|
||||
addStep(...elements);
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("generateHeightmap");
|
||||
return heights;
|
||||
};
|
||||
|
||||
function addStep(a1, a2, a3, a4, a5) {
|
||||
if (a1 === "Hill") return addHill(a2, a3, a4, a5);
|
||||
if (a1 === "Pit") return addPit(a2, a3, a4, a5);
|
||||
if (a1 === "Range") return addRange(a2, a3, a4, a5);
|
||||
if (a1 === "Trough") return addTrough(a2, a3, a4, a5);
|
||||
if (a1 === "Strait") return addStrait(a2, a3);
|
||||
if (a1 === "Mask") return mask(a2);
|
||||
if (a1 === "Add") return modify(a3, +a2, 1);
|
||||
if (a1 === "Multiply") return modify(a3, 0, +a2);
|
||||
if (a1 === "Smooth") return smooth(a2);
|
||||
const fromPrecreated = (graph, id) => {
|
||||
return new Promise(resolve => {
|
||||
// create canvas where 1px corresponts to a cell
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const {cellsX, cellsY} = graph;
|
||||
canvas.width = cellsX;
|
||||
canvas.height = cellsY;
|
||||
|
||||
// load heightmap into image and render to canvas
|
||||
const img = new Image();
|
||||
img.src = `./heightmaps/${id}.png`;
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0, cellsX, cellsY);
|
||||
const imageData = ctx.getImageData(0, 0, cellsX, cellsY);
|
||||
setGraph(graph);
|
||||
getHeightsFromImageData(imageData.data);
|
||||
canvas.remove();
|
||||
img.remove();
|
||||
resolve(heights);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const generate = async function (graph) {
|
||||
TIME && console.time("defineHeightmap");
|
||||
const id = byId("templateInput").value;
|
||||
|
||||
Math.random = aleaPRNG(seed);
|
||||
const isTemplate = id in heightmapTemplates;
|
||||
const heights = isTemplate ? fromTemplate(graph, id) : await fromPrecreated(graph, id);
|
||||
TIME && console.timeEnd("defineHeightmap");
|
||||
|
||||
clearData();
|
||||
return heights;
|
||||
};
|
||||
|
||||
function addStep(tool, a2, a3, a4, a5) {
|
||||
if (tool === "Hill") return addHill(a2, a3, a4, a5);
|
||||
if (tool === "Pit") return addPit(a2, a3, a4, a5);
|
||||
if (tool === "Range") return addRange(a2, a3, a4, a5);
|
||||
if (tool === "Trough") return addTrough(a2, a3, a4, a5);
|
||||
if (tool === "Strait") return addStrait(a2, a3);
|
||||
if (tool === "Mask") return mask(a2);
|
||||
if (tool === "Invert") return invert(a2, a3);
|
||||
if (tool === "Add") return modify(a3, +a2, 1);
|
||||
if (tool === "Multiply") return modify(a3, 0, +a2);
|
||||
if (tool === "Smooth") return smooth(a2);
|
||||
}
|
||||
|
||||
function getBlobPower() {
|
||||
const cells = +pointsInput.dataset.cells;
|
||||
if (cells === 1000) return 0.93;
|
||||
if (cells === 2000) return 0.95;
|
||||
if (cells === 5000) return 0.96;
|
||||
if (cells === 10000) return 0.98;
|
||||
if (cells === 20000) return 0.985;
|
||||
if (cells === 30000) return 0.987;
|
||||
if (cells === 40000) return 0.9892;
|
||||
if (cells === 50000) return 0.9911;
|
||||
if (cells === 60000) return 0.9921;
|
||||
if (cells === 70000) return 0.9934;
|
||||
if (cells === 80000) return 0.9942;
|
||||
if (cells === 90000) return 0.9946;
|
||||
if (cells === 100000) return 0.995;
|
||||
function getBlobPower(cells) {
|
||||
const blobPowerMap = {
|
||||
1000: 0.93,
|
||||
2000: 0.95,
|
||||
5000: 0.97,
|
||||
10000: 0.98,
|
||||
20000: 0.99,
|
||||
30000: 0.991,
|
||||
40000: 0.993,
|
||||
50000: 0.994,
|
||||
60000: 0.995,
|
||||
70000: 0.9955,
|
||||
80000: 0.996,
|
||||
90000: 0.9964,
|
||||
100000: 0.9973
|
||||
};
|
||||
return blobPowerMap[cells] || 0.98;
|
||||
}
|
||||
|
||||
function getLinePower() {
|
||||
const cells = +pointsInput.dataset.cells;
|
||||
if (cells === 1000) return 0.74;
|
||||
if (cells === 2000) return 0.75;
|
||||
if (cells === 5000) return 0.78;
|
||||
if (cells === 10000) return 0.81;
|
||||
if (cells === 20000) return 0.82;
|
||||
if (cells === 30000) return 0.83;
|
||||
if (cells === 40000) return 0.84;
|
||||
if (cells === 50000) return 0.855;
|
||||
if (cells === 60000) return 0.87;
|
||||
if (cells === 70000) return 0.885;
|
||||
if (cells === 80000) return 0.91;
|
||||
if (cells === 90000) return 0.92;
|
||||
if (cells === 100000) return 0.93;
|
||||
const linePowerMap = {
|
||||
1000: 0.75,
|
||||
2000: 0.77,
|
||||
5000: 0.79,
|
||||
10000: 0.81,
|
||||
20000: 0.82,
|
||||
30000: 0.83,
|
||||
40000: 0.84,
|
||||
50000: 0.86,
|
||||
60000: 0.87,
|
||||
70000: 0.88,
|
||||
80000: 0.91,
|
||||
90000: 0.92,
|
||||
100000: 0.93
|
||||
};
|
||||
|
||||
return linePowerMap[cells] || 0.81;
|
||||
}
|
||||
|
||||
const addHill = (count, height, rangeX, rangeY) => {
|
||||
count = getNumberInRange(count);
|
||||
const power = getBlobPower();
|
||||
while (count > 0) {
|
||||
addOneHill();
|
||||
count--;
|
||||
}
|
||||
|
||||
function addOneHill() {
|
||||
const change = new Uint8Array(cells.h.length);
|
||||
const change = new Uint8Array(heights.length);
|
||||
let limit = 0;
|
||||
let start;
|
||||
let h = lim(getNumberInRange(height));
|
||||
|
|
@ -118,23 +142,23 @@ window.HeightmapGenerator = (function () {
|
|||
do {
|
||||
const x = getPointInRange(rangeX, graphWidth);
|
||||
const y = getPointInRange(rangeY, graphHeight);
|
||||
start = findGridCell(x, y);
|
||||
start = findGridCell(x, y, grid);
|
||||
limit++;
|
||||
} while (cells.h[start] + h > 90 && limit < 50);
|
||||
} while (heights[start] + h > 90 && limit < 50);
|
||||
|
||||
change[start] = h;
|
||||
const queue = [start];
|
||||
while (queue.length) {
|
||||
const q = queue.shift();
|
||||
|
||||
for (const c of cells.c[q]) {
|
||||
for (const c of grid.cells.c[q]) {
|
||||
if (change[c]) continue;
|
||||
change[c] = change[q] ** power * (Math.random() * 0.2 + 0.9);
|
||||
change[c] = change[q] ** blobPower * (Math.random() * 0.2 + 0.9);
|
||||
if (change[c] > 1) queue.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
cells.h = cells.h.map((h, i) => lim(h + change[i]));
|
||||
heights = heights.map((h, i) => lim(h + change[i]));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -146,7 +170,7 @@ window.HeightmapGenerator = (function () {
|
|||
}
|
||||
|
||||
function addOnePit() {
|
||||
const used = new Uint8Array(cells.h.length);
|
||||
const used = new Uint8Array(heights.length);
|
||||
let limit = 0,
|
||||
start;
|
||||
let h = lim(getNumberInRange(height));
|
||||
|
|
@ -154,19 +178,19 @@ window.HeightmapGenerator = (function () {
|
|||
do {
|
||||
const x = getPointInRange(rangeX, graphWidth);
|
||||
const y = getPointInRange(rangeY, graphHeight);
|
||||
start = findGridCell(x, y);
|
||||
start = findGridCell(x, y, grid);
|
||||
limit++;
|
||||
} while (cells.h[start] < 20 && limit < 50);
|
||||
} while (heights[start] < 20 && limit < 50);
|
||||
|
||||
const queue = [start];
|
||||
while (queue.length) {
|
||||
const q = queue.shift();
|
||||
h = h ** getBlobPower() * (Math.random() * 0.2 + 0.9);
|
||||
h = h ** blobPower * (Math.random() * 0.2 + 0.9);
|
||||
if (h < 1) return;
|
||||
|
||||
cells.c[q].forEach(function (c, i) {
|
||||
grid.cells.c[q].forEach(function (c, i) {
|
||||
if (used[c]) return;
|
||||
cells.h[c] = lim(cells.h[c] - h * (Math.random() * 0.2 + 0.9));
|
||||
heights[c] = lim(heights[c] - h * (Math.random() * 0.2 + 0.9));
|
||||
used[c] = 1;
|
||||
queue.push(c);
|
||||
});
|
||||
|
|
@ -176,14 +200,13 @@ window.HeightmapGenerator = (function () {
|
|||
|
||||
const addRange = (count, height, rangeX, rangeY) => {
|
||||
count = getNumberInRange(count);
|
||||
const power = getLinePower();
|
||||
while (count > 0) {
|
||||
addOneRange();
|
||||
count--;
|
||||
}
|
||||
|
||||
function addOneRange() {
|
||||
const used = new Uint8Array(cells.h.length);
|
||||
const used = new Uint8Array(heights.length);
|
||||
let h = lim(getNumberInRange(height));
|
||||
|
||||
// find start and end points
|
||||
|
|
@ -201,16 +224,19 @@ window.HeightmapGenerator = (function () {
|
|||
limit++;
|
||||
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50);
|
||||
|
||||
let range = getRange(findGridCell(startX, startY), findGridCell(endX, endY));
|
||||
const startCell = findGridCell(startX, startY, grid);
|
||||
const endCell = findGridCell(endX, endY, grid);
|
||||
let range = getRange(startCell, endCell);
|
||||
|
||||
// get main ridge
|
||||
function getRange(cur, end) {
|
||||
const range = [cur];
|
||||
const p = grid.points;
|
||||
used[cur] = 1;
|
||||
|
||||
while (cur !== end) {
|
||||
let min = Infinity;
|
||||
cells.c[cur].forEach(function (e) {
|
||||
grid.cells.c[cur].forEach(function (e) {
|
||||
if (used[e]) return;
|
||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||
if (Math.random() > 0.85) diff = diff / 2;
|
||||
|
|
@ -234,12 +260,12 @@ window.HeightmapGenerator = (function () {
|
|||
const frontier = queue.slice();
|
||||
(queue = []), i++;
|
||||
frontier.forEach(i => {
|
||||
cells.h[i] = lim(cells.h[i] + h * (Math.random() * 0.3 + 0.85));
|
||||
heights[i] = lim(heights[i] + h * (Math.random() * 0.3 + 0.85));
|
||||
});
|
||||
h = h ** power - 1;
|
||||
h = h ** linePower - 1;
|
||||
if (h < 2) break;
|
||||
frontier.forEach(f => {
|
||||
cells.c[f].forEach(i => {
|
||||
grid.cells.c[f].forEach(i => {
|
||||
if (!used[i]) {
|
||||
queue.push(i);
|
||||
used[i] = 1;
|
||||
|
|
@ -252,8 +278,8 @@ window.HeightmapGenerator = (function () {
|
|||
range.forEach((cur, d) => {
|
||||
if (d % 6 !== 0) return;
|
||||
for (const l of d3.range(i)) {
|
||||
const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
|
||||
cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3;
|
||||
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
|
||||
heights[min] = (heights[cur] * 2 + heights[min]) / 3;
|
||||
cur = min;
|
||||
}
|
||||
});
|
||||
|
|
@ -262,14 +288,13 @@ window.HeightmapGenerator = (function () {
|
|||
|
||||
const addTrough = (count, height, rangeX, rangeY) => {
|
||||
count = getNumberInRange(count);
|
||||
const power = getLinePower();
|
||||
while (count > 0) {
|
||||
addOneTrough();
|
||||
count--;
|
||||
}
|
||||
|
||||
function addOneTrough() {
|
||||
const used = new Uint8Array(cells.h.length);
|
||||
const used = new Uint8Array(heights.length);
|
||||
let h = lim(getNumberInRange(height));
|
||||
|
||||
// find start and end points
|
||||
|
|
@ -283,9 +308,9 @@ window.HeightmapGenerator = (function () {
|
|||
do {
|
||||
startX = getPointInRange(rangeX, graphWidth);
|
||||
startY = getPointInRange(rangeY, graphHeight);
|
||||
start = findGridCell(startX, startY);
|
||||
start = findGridCell(startX, startY, grid);
|
||||
limit++;
|
||||
} while (cells.h[start] < 20 && limit < 50);
|
||||
} while (heights[start] < 20 && limit < 50);
|
||||
|
||||
limit = 0;
|
||||
do {
|
||||
|
|
@ -295,16 +320,17 @@ window.HeightmapGenerator = (function () {
|
|||
limit++;
|
||||
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50);
|
||||
|
||||
let range = getRange(start, findGridCell(endX, endY));
|
||||
let range = getRange(start, findGridCell(endX, endY, grid));
|
||||
|
||||
// get main ridge
|
||||
function getRange(cur, end) {
|
||||
const range = [cur];
|
||||
const p = grid.points;
|
||||
used[cur] = 1;
|
||||
|
||||
while (cur !== end) {
|
||||
let min = Infinity;
|
||||
cells.c[cur].forEach(function (e) {
|
||||
grid.cells.c[cur].forEach(function (e) {
|
||||
if (used[e]) return;
|
||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||
if (Math.random() > 0.8) diff = diff / 2;
|
||||
|
|
@ -328,12 +354,12 @@ window.HeightmapGenerator = (function () {
|
|||
const frontier = queue.slice();
|
||||
(queue = []), i++;
|
||||
frontier.forEach(i => {
|
||||
cells.h[i] = lim(cells.h[i] - h * (Math.random() * 0.3 + 0.85));
|
||||
heights[i] = lim(heights[i] - h * (Math.random() * 0.3 + 0.85));
|
||||
});
|
||||
h = h ** power - 1;
|
||||
h = h ** linePower - 1;
|
||||
if (h < 2) break;
|
||||
frontier.forEach(f => {
|
||||
cells.c[f].forEach(i => {
|
||||
grid.cells.c[f].forEach(i => {
|
||||
if (!used[i]) {
|
||||
queue.push(i);
|
||||
used[i] = 1;
|
||||
|
|
@ -346,9 +372,9 @@ window.HeightmapGenerator = (function () {
|
|||
range.forEach((cur, d) => {
|
||||
if (d % 6 !== 0) return;
|
||||
for (const l of d3.range(i)) {
|
||||
const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
|
||||
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
|
||||
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
|
||||
cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3;
|
||||
heights[min] = (heights[cur] * 2 + heights[min]) / 3;
|
||||
cur = min;
|
||||
}
|
||||
});
|
||||
|
|
@ -358,24 +384,25 @@ window.HeightmapGenerator = (function () {
|
|||
const addStrait = (width, direction = "vertical") => {
|
||||
width = Math.min(getNumberInRange(width), grid.cellsX / 3);
|
||||
if (width < 1 && P(width)) return;
|
||||
const used = new Uint8Array(cells.h.length);
|
||||
const used = new Uint8Array(heights.length);
|
||||
const vert = direction === "vertical";
|
||||
const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5;
|
||||
const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * 0.4 + graphHeight * 0.3);
|
||||
const endX = vert ? Math.floor(graphWidth - startX - graphWidth * 0.1 + Math.random() * graphWidth * 0.2) : graphWidth - 5;
|
||||
const endY = vert ? graphHeight - 5 : Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2);
|
||||
|
||||
const start = findGridCell(startX, startY);
|
||||
const end = findGridCell(endX, endY);
|
||||
const start = findGridCell(startX, startY, grid);
|
||||
const end = findGridCell(endX, endY, grid);
|
||||
let range = getRange(start, end);
|
||||
const query = [];
|
||||
|
||||
function getRange(cur, end) {
|
||||
const range = [];
|
||||
const p = grid.points;
|
||||
|
||||
while (cur !== end) {
|
||||
let min = Infinity;
|
||||
cells.c[cur].forEach(function (e) {
|
||||
grid.cells.c[cur].forEach(function (e) {
|
||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||
if (Math.random() > 0.8) diff = diff / 2;
|
||||
if (diff < min) {
|
||||
|
|
@ -394,12 +421,12 @@ window.HeightmapGenerator = (function () {
|
|||
while (width > 0) {
|
||||
const exp = 0.9 - step * width;
|
||||
range.forEach(function (r) {
|
||||
cells.c[r].forEach(function (e) {
|
||||
grid.cells.c[r].forEach(function (e) {
|
||||
if (used[e]) return;
|
||||
used[e] = 1;
|
||||
query.push(e);
|
||||
cells.h[e] **= exp;
|
||||
if (cells.h[e] > 100) cells.h[e] = 5;
|
||||
heights[e] **= exp;
|
||||
if (heights[e] > 100) heights[e] = 5;
|
||||
});
|
||||
});
|
||||
range = query.slice();
|
||||
|
|
@ -413,7 +440,7 @@ window.HeightmapGenerator = (function () {
|
|||
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
|
||||
const isLand = min === 20;
|
||||
|
||||
grid.cells.h = grid.cells.h.map(h => {
|
||||
heights = heights.map(h => {
|
||||
if (h < min || h > max) return h;
|
||||
|
||||
if (add) h = isLand ? Math.max(h + add, 20) : h + add;
|
||||
|
|
@ -424,9 +451,9 @@ window.HeightmapGenerator = (function () {
|
|||
};
|
||||
|
||||
const smooth = (fr = 2, add = 0) => {
|
||||
cells.h = cells.h.map((h, i) => {
|
||||
heights = heights.map((h, i) => {
|
||||
const a = [h];
|
||||
cells.c[i].forEach(c => a.push(cells.h[c]));
|
||||
grid.cells.c[i].forEach(c => a.push(heights[c]));
|
||||
if (fr === 1) return d3.mean(a) + add;
|
||||
return lim((h * (fr - 1) + d3.mean(a) + add) / fr);
|
||||
});
|
||||
|
|
@ -435,8 +462,8 @@ window.HeightmapGenerator = (function () {
|
|||
const mask = (power = 1) => {
|
||||
const fr = power ? Math.abs(power) : 1;
|
||||
|
||||
cells.h = cells.h.map((h, i) => {
|
||||
const [x, y] = p[i];
|
||||
heights = heights.map((h, i) => {
|
||||
const [x, y] = grid.points[i];
|
||||
const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center
|
||||
const ny = (2 * y) / graphHeight - 1; // [-1, 1], 0 is center
|
||||
let distance = (1 - nx ** 2) * (1 - ny ** 2); // 1 is center, 0 is edge
|
||||
|
|
@ -453,17 +480,17 @@ window.HeightmapGenerator = (function () {
|
|||
const invertY = axes !== "x";
|
||||
const {cellsX, cellsY} = grid;
|
||||
|
||||
const inverted = cells.h.map((h, i) => {
|
||||
const inverted = heights.map((h, i) => {
|
||||
const x = i % cellsX;
|
||||
const y = Math.floor(i / cellsX);
|
||||
|
||||
const nx = invertX ? cellsX - x - 1 : x;
|
||||
const ny = invertY ? cellsY - y - 1 : y;
|
||||
const invertedI = nx + ny * cellsX;
|
||||
return cells.h[invertedI];
|
||||
return heights[invertedI];
|
||||
});
|
||||
|
||||
cells.h = inverted;
|
||||
heights = inverted;
|
||||
};
|
||||
|
||||
function getPointInRange(range, length) {
|
||||
|
|
@ -477,13 +504,28 @@ window.HeightmapGenerator = (function () {
|
|||
return rand(min * length, max * length);
|
||||
}
|
||||
|
||||
function assignColorsToHeight(imageData) {
|
||||
for (let i = 0; i < cells.i.length; i++) {
|
||||
function getHeightsFromImageData(imageData) {
|
||||
for (let i = 0; i < heights.length; i++) {
|
||||
const lightness = imageData[i * 4] / 255;
|
||||
const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8;
|
||||
cells.h[i] = minmax(Math.floor(powered * 100), 0, 100);
|
||||
heights[i] = minmax(Math.floor(powered * 100), 0, 100);
|
||||
}
|
||||
}
|
||||
|
||||
return {generate, addHill, addRange, addTrough, addStrait, addPit, smooth, modify, mask, invert};
|
||||
return {
|
||||
setGraph,
|
||||
getHeights,
|
||||
generate,
|
||||
fromTemplate,
|
||||
fromPrecreated,
|
||||
addHill,
|
||||
addRange,
|
||||
addTrough,
|
||||
addStrait,
|
||||
addPit,
|
||||
smooth,
|
||||
modify,
|
||||
mask,
|
||||
invert
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@ function removeUnusedElements(clone) {
|
|||
|
||||
function updateMeshCells(clone) {
|
||||
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20);
|
||||
const scheme = getColorScheme();
|
||||
const scheme = getColorScheme(terrs.attr("scheme"));
|
||||
clone.select("#heights").attr("filter", "url(#blur1)");
|
||||
clone
|
||||
.select("#heights")
|
||||
|
|
|
|||
|
|
@ -324,7 +324,11 @@ async function parseLoadedData(data) {
|
|||
|
||||
void (function parseGridData() {
|
||||
grid = JSON.parse(data[6]);
|
||||
calculateVoronoi(grid, grid.points);
|
||||
|
||||
const {cells, vertices} = calculateVoronoi(grid.points, grid.boundary);
|
||||
grid.cells = cells;
|
||||
grid.vertices = vertices;
|
||||
|
||||
grid.cells.h = Uint8Array.from(data[7].split(","));
|
||||
grid.cells.prec = Uint8Array.from(data[8].split(","));
|
||||
grid.cells.f = Uint16Array.from(data[9].split(","));
|
||||
|
|
@ -333,7 +337,6 @@ async function parseLoadedData(data) {
|
|||
})();
|
||||
|
||||
void (function parsePackData() {
|
||||
pack = {};
|
||||
reGraph();
|
||||
reMarkFeatures();
|
||||
pack.features = JSON.parse(data[12]);
|
||||
|
|
@ -424,7 +427,7 @@ async function parseLoadedData(data) {
|
|||
{
|
||||
// dynamically import and run auto-udpdate script
|
||||
const versionNumber = parseFloat(params[0]);
|
||||
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js");
|
||||
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=29052022");
|
||||
resolveVersionConflicts(versionNumber);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@ function getMapData() {
|
|||
|
||||
const serializedSVG = new XMLSerializer().serializeToString(cloneEl);
|
||||
|
||||
const {spacing, cellsX, cellsY, boundary, points, features} = grid;
|
||||
const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features});
|
||||
const {spacing, cellsX, cellsY, boundary, points, features, cellsDesired} = grid;
|
||||
const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features, cellsDesired});
|
||||
const packFeatures = JSON.stringify(pack.features);
|
||||
const cultures = JSON.stringify(pack.cultures);
|
||||
const states = JSON.stringify(pack.states);
|
||||
|
|
|
|||
|
|
@ -41,8 +41,7 @@ window.Submap = (function () {
|
|||
|
||||
// create new grid
|
||||
applyMapSize();
|
||||
placePoints();
|
||||
calculateVoronoi(grid, grid.points);
|
||||
grid = generateGrid();
|
||||
drawScaleBar(scale);
|
||||
|
||||
const resampler = (points, qtree, f) => {
|
||||
|
|
|
|||
|
|
@ -1119,12 +1119,12 @@ function refreshAllEditors() {
|
|||
// dynamically loaded editors
|
||||
async function editStates() {
|
||||
if (customization) return;
|
||||
const StateEditor = await import("../dynamic/editors/states-editor.js?v=20052022");
|
||||
const StateEditor = await import("../dynamic/editors/states-editor.js?v=29052022");
|
||||
StateEditor.open();
|
||||
}
|
||||
|
||||
async function editCultures() {
|
||||
if (customization) return;
|
||||
const CulturesEditor = await import("../dynamic/editors/cultures-editor.js?v=20052022");
|
||||
const CulturesEditor = await import("../dynamic/editors/cultures-editor.js?v=29052022");
|
||||
CulturesEditor.open();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,8 @@ function showElevationProfile(data, routeLen, isRiver) {
|
|||
draw();
|
||||
|
||||
function downloadCSV() {
|
||||
let data = "Point,X,Y,Cell,Height,Height value,Population,Burg,Burg population,Biome,Biome color,Culture,Culture color,Religion,Religion color,Province,Province color,State,State color\n"; // headers
|
||||
let data =
|
||||
"Point,X,Y,Cell,Height,Height value,Population,Burg,Burg population,Biome,Biome color,Culture,Culture color,Religion,Religion color,Province,Province color,State,State color\n"; // headers
|
||||
|
||||
for (let k = 0; k < chartData.points.length; k++) {
|
||||
let cell = chartData.cell[k];
|
||||
|
|
@ -179,9 +180,20 @@ function showElevationProfile(data, routeLen, isRiver) {
|
|||
.attr("id", "elevationSVG")
|
||||
.attr("class", "epbackground");
|
||||
// arrow-head definition
|
||||
chart.append("defs").append("marker").attr("id", "arrowhead").attr("orient", "auto").attr("markerWidth", "2").attr("markerHeight", "4").attr("refX", "0.1").attr("refY", "2").append("path").attr("d", "M0,0 V4 L2,2 Z").attr("fill", "darkgray");
|
||||
chart
|
||||
.append("defs")
|
||||
.append("marker")
|
||||
.attr("id", "arrowhead")
|
||||
.attr("orient", "auto")
|
||||
.attr("markerWidth", "2")
|
||||
.attr("markerHeight", "4")
|
||||
.attr("refX", "0.1")
|
||||
.attr("refY", "2")
|
||||
.append("path")
|
||||
.attr("d", "M0,0 V4 L2,2 Z")
|
||||
.attr("fill", "darkgray");
|
||||
|
||||
let colors = getColorScheme();
|
||||
let colors = getColorScheme(terrs.attr("scheme"));
|
||||
const landdef = chart.select("defs").append("linearGradient").attr("id", "landdef").attr("x1", "0%").attr("y1", "0%").attr("x2", "0%").attr("y2", "100%");
|
||||
|
||||
if (chartData.mah == chartData.mih) {
|
||||
|
|
@ -258,7 +270,24 @@ function showElevationProfile(data, routeLen, isRiver) {
|
|||
const populationDesc = rn(pop * populationRate);
|
||||
|
||||
const provinceDesc = province ? ", " + pack.provinces[province].name : "";
|
||||
const dataTip = biomesData.name[chartData.biome[k]] + provinceDesc + ", " + pack.states[state].name + ", " + pack.religions[religion].name + ", " + pack.cultures[culture].name + " (height: " + chartData.height[k] + " " + hu + ", population " + populationDesc + ", cell " + chartData.cell[k] + ")";
|
||||
const dataTip =
|
||||
biomesData.name[chartData.biome[k]] +
|
||||
provinceDesc +
|
||||
", " +
|
||||
pack.states[state].name +
|
||||
", " +
|
||||
pack.religions[religion].name +
|
||||
", " +
|
||||
pack.cultures[culture].name +
|
||||
" (height: " +
|
||||
chartData.height[k] +
|
||||
" " +
|
||||
hu +
|
||||
", population " +
|
||||
populationDesc +
|
||||
", cell " +
|
||||
chartData.cell[k] +
|
||||
")";
|
||||
|
||||
g.append("rect").attr("stroke", c).attr("fill", c).attr("x", x).attr("y", y).attr("width", xscale(1)).attr("height", 15).attr("data-tip", dataTip);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
// fit full-screen map if window is resized
|
||||
window.addEventListener("resize", function (e) {
|
||||
if (localStorage.getItem("mapWidth") && localStorage.getItem("mapHeight")) return;
|
||||
if (stored("mapWidth") && stored("mapHeight")) return;
|
||||
mapWidthInput.value = window.innerWidth;
|
||||
mapHeightInput.value = window.innerHeight;
|
||||
changeMapSize();
|
||||
|
|
@ -21,18 +21,22 @@ document.getElementById("dialogs").addEventListener("mousemove", showDataTip);
|
|||
document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip);
|
||||
document.getElementById("exitCustomization").addEventListener("mousemove", showDataTip);
|
||||
|
||||
function tip(tip = "Tip is undefined", main, type, time) {
|
||||
const tipBackgroundMap = {
|
||||
info: "linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)",
|
||||
success: "linear-gradient(0.1turn, #ffffff00, #127912cc, #ffffff00)",
|
||||
warn: "linear-gradient(0.1turn, #ffffff00, #be5d08cc, #ffffff00)",
|
||||
error: "linear-gradient(0.1turn, #ffffff00, #e11d1dcc, #ffffff00)"
|
||||
};
|
||||
|
||||
function tip(tip = "Tip is undefined", main = false, type = "info", time = 0) {
|
||||
tooltip.innerHTML = tip;
|
||||
tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)";
|
||||
if (type === "error") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #e11d1dcc, #ffffff00)";
|
||||
else if (type === "warn") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #be5d08cc, #ffffff00)";
|
||||
else if (type === "success") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #127912cc, #ffffff00)";
|
||||
tooltip.style.background = tipBackgroundMap[type];
|
||||
|
||||
if (main) {
|
||||
tooltip.dataset.main = tip;
|
||||
tooltip.dataset.color = tooltip.style.background;
|
||||
}
|
||||
if (time) setTimeout(() => clearMainTip(), time);
|
||||
if (time) setTimeout(clearMainTip, time);
|
||||
}
|
||||
|
||||
function showMainTip() {
|
||||
|
|
@ -47,11 +51,16 @@ function clearMainTip() {
|
|||
}
|
||||
|
||||
// show tip at the bottom of the screen, consider possible translation
|
||||
function showDataTip(e) {
|
||||
if (!e.target) return;
|
||||
let dataTip = e.target.dataset.tip;
|
||||
if (!dataTip && e.target.parentNode.dataset.tip) dataTip = e.target.parentNode.dataset.tip;
|
||||
function showDataTip(event) {
|
||||
if (!event.target) return;
|
||||
|
||||
let dataTip = event.target.dataset.tip;
|
||||
if (!dataTip && event.target.parentNode.dataset.tip) dataTip = event.target.parentNode.dataset.tip;
|
||||
if (!dataTip) return;
|
||||
|
||||
const shortcut = event.target.dataset.shortcut;
|
||||
if (shortcut && !MOBILE) dataTip += `. Shortcut: ${shortcut}`;
|
||||
|
||||
//const tooltip = lang === "en" ? dataTip : translate(e.target.dataset.t || e.target.parentNode.dataset.t, dataTip);
|
||||
tip(dataTip);
|
||||
}
|
||||
|
|
@ -72,10 +81,10 @@ function handleMouseMove() {
|
|||
if (i === undefined) return;
|
||||
|
||||
showNotes(d3.event);
|
||||
const g = findGridCell(point[0], point[1]); // grid cell id
|
||||
const gridCell = findGridCell(point[0], point[1], grid);
|
||||
if (tooltip.dataset.main) showMainTip();
|
||||
else showMapTooltip(point, d3.event, i, g);
|
||||
if (cellInfo?.offsetParent) updateCellInfo(point, i, g);
|
||||
else showMapTooltip(point, d3.event, i, gridCell);
|
||||
if (cellInfo?.offsetParent) updateCellInfo(point, i, gridCell);
|
||||
}
|
||||
|
||||
// show note box on hover (if any)
|
||||
|
|
@ -235,7 +244,7 @@ function updateCellInfo(point, i, g) {
|
|||
infoCell.innerHTML = i;
|
||||
infoArea.innerHTML = cells.area[i] ? si(getArea(cells.area[i])) + " " + getAreaUnit() : "n/a";
|
||||
infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
|
||||
infoDepth.innerHTML = getDepth(pack.features[f], pack.cells.h[i], point);
|
||||
infoDepth.innerHTML = getDepth(pack.features[f], point);
|
||||
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
|
||||
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
|
||||
infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : "no";
|
||||
|
|
@ -267,11 +276,11 @@ function getElevation(f, h) {
|
|||
}
|
||||
|
||||
// get water depth
|
||||
function getDepth(f, h, p) {
|
||||
function getDepth(f, p) {
|
||||
if (f.land) return "0 " + heightUnit.value; // land: 0
|
||||
|
||||
// lake: difference between surface and bottom
|
||||
const gridH = grid.cells.h[findGridCell(p[0], p[1])];
|
||||
const gridH = grid.cells.h[findGridCell(p[0], p[1], grid)];
|
||||
if (f.type === "lake") {
|
||||
const depth = gridH === 19 ? f.height / 2 : gridH;
|
||||
return getHeight(depth, "abs");
|
||||
|
|
@ -281,9 +290,9 @@ function getDepth(f, h, p) {
|
|||
}
|
||||
|
||||
// get user-friendly (real-world) height value from map data
|
||||
function getFriendlyHeight(p) {
|
||||
const packH = pack.cells.h[findCell(p[0], p[1])];
|
||||
const gridH = grid.cells.h[findGridCell(p[0], p[1])];
|
||||
function getFriendlyHeight([x, y]) {
|
||||
const packH = pack.cells.h[findCell(x, y, grid)];
|
||||
const gridH = grid.cells.h[findGridCell(x, y, grid)];
|
||||
const h = packH < 20 ? gridH : packH;
|
||||
return getHeight(h);
|
||||
}
|
||||
|
|
@ -405,7 +414,7 @@ document.querySelectorAll("[data-locked]").forEach(function (e) {
|
|||
// lock option
|
||||
function lock(id) {
|
||||
const input = document.querySelector('[data-stored="' + id + '"]');
|
||||
if (input) localStorage.setItem(id, input.value);
|
||||
if (input) store(id, input.value);
|
||||
const el = document.getElementById("lock_" + id);
|
||||
if (!el) return;
|
||||
el.dataset.locked = 1;
|
||||
|
|
@ -427,9 +436,14 @@ function locked(id) {
|
|||
return lockEl.dataset.locked == 1;
|
||||
}
|
||||
|
||||
// check if option is stored in localStorage
|
||||
function stored(option) {
|
||||
return localStorage.getItem(option);
|
||||
// return key value stored in localStorage or null
|
||||
function stored(key) {
|
||||
return localStorage.getItem(key) || null;
|
||||
}
|
||||
|
||||
// store key value in localStorage
|
||||
function store(key, value) {
|
||||
return localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
// assign skeaker behaviour
|
||||
|
|
@ -449,10 +463,10 @@ function speak(text) {
|
|||
}
|
||||
|
||||
// apply drop-down menu option. If the value is not in options, add it
|
||||
function applyOption(select, id, name = id) {
|
||||
const custom = !Array.from(select.options).some(o => o.value == id);
|
||||
if (custom) select.options.add(new Option(name, id));
|
||||
select.value = id;
|
||||
function applyOption($select, value, name = value) {
|
||||
const isExisting = Array.from($select.options).some(o => o.value === value);
|
||||
if (!isExisting) $select.options.add(new Option(name, value));
|
||||
$select.value = value;
|
||||
}
|
||||
|
||||
// show info about the generator in a popup
|
||||
|
|
|
|||
|
|
@ -1,35 +1,13 @@
|
|||
"use strict";
|
||||
|
||||
function editHeightmap() {
|
||||
void (function selectEditMode() {
|
||||
alertMessage.innerHTML = /* html */ `Heightmap is a core element on which all other data (rivers, burgs, states etc) is based. So the best edit approach is to
|
||||
<i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.
|
||||
<p><i>Erase</i> mode also allows you Convert an Image into a heightmap or use Template Editor.</p>
|
||||
<p>You can <i>keep</i> the data, but you won't be able to change the coastline.</p>
|
||||
<p>
|
||||
Try <i>risk</i> mode to change the coastline and keep the data. The data will be restored as much as possible, but it can cause unpredictable errors.
|
||||
</p>
|
||||
<p>Please <span class="pseudoLink" onclick="dowloadMap();" editHeightmap();>save the map</span> before editing the heightmap!</p>
|
||||
<p style="margin-bottom: 0">Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.</p>`;
|
||||
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Edit Heightmap",
|
||||
width: "28em",
|
||||
buttons: {
|
||||
Erase: () => enterHeightmapEditMode("erase"),
|
||||
Keep: () => enterHeightmapEditMode("keep"),
|
||||
Risk: () => enterHeightmapEditMode("risk"),
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
function editHeightmap(options) {
|
||||
const {mode, tool} = options || {};
|
||||
restartHistory();
|
||||
viewbox.insert("g", "#terrs").attr("id", "heights");
|
||||
|
||||
if (!mode) showModeDialog();
|
||||
else enterHeightmapEditMode(mode);
|
||||
|
||||
if (modules.editHeightmap) return;
|
||||
modules.editHeightmap = true;
|
||||
|
||||
|
|
@ -44,35 +22,66 @@ function editHeightmap() {
|
|||
byId("templateUndo").on("click", () => restoreHistory(edits.n - 1));
|
||||
byId("templateRedo").on("click", () => restoreHistory(edits.n + 1));
|
||||
|
||||
function enterHeightmapEditMode(type) {
|
||||
function showModeDialog() {
|
||||
alertMessage.innerHTML = /* html */ `Heightmap is a core element on which all other data (rivers, burgs, states etc) is based. So the best edit approach is to
|
||||
<i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.
|
||||
<p><i>Erase</i> mode also allows you Convert an Image into a heightmap or use Template Editor.</p>
|
||||
<p>You can <i>keep</i> the data, but you won't be able to change the coastline.</p>
|
||||
<p>Try <i>risk</i> mode to change the coastline and keep the data. The data will be restored as much as possible, but it can cause unpredictable errors.</p>
|
||||
<p>Please <span class="pseudoLink" onclick="dowloadMap();">save the map</span> before editing the heightmap!</p>
|
||||
<p style="margin-bottom: 0">Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.</p>`;
|
||||
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Edit Heightmap",
|
||||
width: "28em",
|
||||
buttons: {
|
||||
Erase: () => enterHeightmapEditMode("erase"),
|
||||
Keep: () => enterHeightmapEditMode("keep"),
|
||||
Risk: () => enterHeightmapEditMode("risk"),
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function enterHeightmapEditMode(mode) {
|
||||
editHeightmap.layers = Array.from(mapLayers.querySelectorAll("li:not(.buttonoff)")).map(node => node.id); // store layers preset
|
||||
editHeightmap.layers.forEach(l => byId(l).click()); // turn off all layers
|
||||
|
||||
customization = 1;
|
||||
closeDialogs();
|
||||
tip('Heightmap edit mode is active. Click on "Exit Customization" to finalize the heightmap', true);
|
||||
customizationMenu.style.display = "block";
|
||||
toolsContent.style.display = "none";
|
||||
heightmapEditMode.innerHTML = type;
|
||||
|
||||
if (type === "erase") {
|
||||
byId("options")
|
||||
.querySelectorAll(".tabcontent")
|
||||
.forEach(tabcontent => {
|
||||
tabcontent.style.display = "none";
|
||||
});
|
||||
byId("options").querySelector(".tab > .active").classList.remove("active");
|
||||
byId("customizationMenu").style.display = "block";
|
||||
byId("toolsTab").classList.add("active");
|
||||
heightmapEditMode.innerHTML = mode;
|
||||
|
||||
if (mode === "erase") {
|
||||
undraw();
|
||||
changeOnlyLand.checked = false;
|
||||
} else if (type === "keep") {
|
||||
} else if (mode === "keep") {
|
||||
viewbox.selectAll("#landmass, #lakes").style("display", "none");
|
||||
changeOnlyLand.checked = true;
|
||||
} else if (type === "risk") {
|
||||
} else if (mode === "risk") {
|
||||
defs.selectAll("#land, #water").selectAll("path").remove();
|
||||
viewbox.selectAll("#coastline path, #lakes path, #oceanLayers path").remove();
|
||||
changeOnlyLand.checked = false;
|
||||
}
|
||||
|
||||
// show convert and template buttons for Erase mode only
|
||||
applyTemplate.style.display = type === "erase" ? "inline-block" : "none";
|
||||
convertImage.style.display = type === "erase" ? "inline-block" : "none";
|
||||
applyTemplate.style.display = mode === "erase" ? "inline-block" : "none";
|
||||
convertImage.style.display = mode === "erase" ? "inline-block" : "none";
|
||||
|
||||
// hide erosion checkbox if mode is Keep
|
||||
allowErosionBox.style.display = type === "keep" ? "none" : "inline-block";
|
||||
allowErosionBox.style.display = mode === "keep" ? "none" : "inline-block";
|
||||
|
||||
// show finalize button
|
||||
if (!sessionStorage.getItem("noExitButtonAnimation")) {
|
||||
|
|
@ -95,27 +104,30 @@ function editHeightmap() {
|
|||
.style("transform", "scale(1)");
|
||||
} else exitCustomization.style.display = "block";
|
||||
|
||||
openBrushesPanel();
|
||||
turnButtonOn("toggleHeight");
|
||||
layersPreset.value = "heightmap";
|
||||
layersPreset.disabled = true;
|
||||
mockHeightmap();
|
||||
viewbox.on("touchmove mousemove", moveCursor);
|
||||
|
||||
if (tool === "templateEditor") openTemplateEditor();
|
||||
else if (tool === "imageConverter") openImageConverter();
|
||||
else openBrushesPanel();
|
||||
}
|
||||
|
||||
function moveCursor() {
|
||||
const p = d3.mouse(this),
|
||||
cell = findGridCell(p[0], p[1]);
|
||||
heightmapInfoX.innerHTML = rn(p[0]);
|
||||
heightmapInfoY.innerHTML = rn(p[1]);
|
||||
const [x, y] = d3.mouse(this);
|
||||
const cell = findGridCell(x, y, grid);
|
||||
heightmapInfoX.innerHTML = rn(x);
|
||||
heightmapInfoY.innerHTML = rn(y);
|
||||
heightmapInfoCell.innerHTML = cell;
|
||||
heightmapInfoHeight.innerHTML = /* html */ `${grid.cells.h[cell]} (${getHeight(grid.cells.h[cell])})`;
|
||||
heightmapInfoHeight.innerHTML = `${grid.cells.h[cell]} (${getHeight(grid.cells.h[cell])})`;
|
||||
if (tooltip.dataset.main) showMainTip();
|
||||
|
||||
// move radius circle if drag mode is active
|
||||
const pressed = byId("brushesButtons").querySelector("button.pressed");
|
||||
if (!pressed) return;
|
||||
moveCircle(p[0], p[1], brushRadius.valueAsNumber, "#333");
|
||||
moveCircle(x, y, brushRadius.valueAsNumber, "#333");
|
||||
}
|
||||
|
||||
// get user-friendly (real-world) height value from map data
|
||||
|
|
@ -593,8 +605,8 @@ function editHeightmap() {
|
|||
|
||||
function dragBrush() {
|
||||
const r = brushRadius.valueAsNumber;
|
||||
const point = d3.mouse(this);
|
||||
const start = findGridCell(point[0], point[1]);
|
||||
const [x, y] = d3.mouse(this);
|
||||
const start = findGridCell(x, y, grid);
|
||||
|
||||
d3.event.on("drag", () => {
|
||||
const p = d3.mouse(this);
|
||||
|
|
@ -652,17 +664,22 @@ function editHeightmap() {
|
|||
if (Number.isNaN(operand)) return tip("Operand should be a number", false, "error");
|
||||
if ((operator === "add" || operator === "subtract") && !Number.isInteger(operand)) return tip("Operand should be an integer", false, "error");
|
||||
|
||||
HeightmapGenerator.setGraph(grid);
|
||||
|
||||
if (operator === "multiply") HeightmapGenerator.modify(range, 0, operand, 0);
|
||||
else if (operator === "divide") HeightmapGenerator.modify(range, 0, 1 / operand, 0);
|
||||
else if (operator === "add") HeightmapGenerator.modify(range, operand, 1, 0);
|
||||
else if (operator === "subtract") HeightmapGenerator.modify(range, -1 * operand, 1, 0);
|
||||
else if (operator === "exponent") HeightmapGenerator.modify(range, 0, 1, operand);
|
||||
|
||||
grid.cells.h = HeightmapGenerator.getHeights();
|
||||
updateHeightmap();
|
||||
}
|
||||
|
||||
function smoothAllHeights() {
|
||||
HeightmapGenerator.setGraph(grid);
|
||||
HeightmapGenerator.smooth(4, 1.5);
|
||||
grid.cells.h = HeightmapGenerator.getHeights();
|
||||
updateHeightmap();
|
||||
}
|
||||
|
||||
|
|
@ -879,10 +896,7 @@ function editHeightmap() {
|
|||
const steps = body.querySelectorAll("div").length;
|
||||
const changed = +body.getAttribute("data-changed");
|
||||
const template = e.target.value;
|
||||
if (!steps || !changed) {
|
||||
changeTemplate(template);
|
||||
return;
|
||||
}
|
||||
if (!steps || !changed) return changeTemplate(template);
|
||||
|
||||
alertMessage.innerHTML = "Are you sure you want to select a different template? All changes will be lost.";
|
||||
$("#alert").dialog({
|
||||
|
|
@ -905,7 +919,7 @@ function editHeightmap() {
|
|||
body.setAttribute("data-changed", 0);
|
||||
body.innerHTML = "";
|
||||
|
||||
const templateString = HeightmapTemplates[template];
|
||||
const templateString = heightmapTemplates[template]?.template;
|
||||
if (!templateString) return;
|
||||
|
||||
const steps = templateString.split("\n");
|
||||
|
|
@ -921,10 +935,11 @@ function editHeightmap() {
|
|||
const steps = byId("templateBody").querySelectorAll("#templateBody > div");
|
||||
if (!steps.length) return;
|
||||
|
||||
grid.cells.h = new Uint8Array(grid.cells.i.length); // clean all heights
|
||||
|
||||
const seed = byId("templateSeed").value;
|
||||
if (seed) Math.random = aleaPRNG(seed);
|
||||
|
||||
grid.cells.h = createTypedArray({maxValue: 100, length: grid.points.length});
|
||||
HeightmapGenerator.setGraph(grid);
|
||||
restartHistory();
|
||||
|
||||
for (const step of steps) {
|
||||
|
|
@ -948,9 +963,11 @@ function editHeightmap() {
|
|||
else if (type === "Multiply") HeightmapGenerator.modify(dist, 0, +count);
|
||||
else if (type === "Smooth") HeightmapGenerator.smooth(+count);
|
||||
|
||||
grid.cells.h = HeightmapGenerator.getHeights();
|
||||
updateHistory("noStat"); // update history on every step
|
||||
}
|
||||
|
||||
grid.cells.h = HeightmapGenerator.getHeights();
|
||||
updateStatistics();
|
||||
mockHeightmap();
|
||||
if (byId("preview")) drawHeightmapPreview(); // update heightmap preview if opened
|
||||
|
|
@ -1360,12 +1377,14 @@ function editHeightmap() {
|
|||
const imageData = ctx.createImageData(grid.cellsX, grid.cellsY);
|
||||
|
||||
grid.cells.h.forEach((height, i) => {
|
||||
let h = height < 20 ? Math.max(height / 1.5, 0) : height;
|
||||
const h = height < 20 ? Math.max(height / 1.5, 0) : height;
|
||||
const v = (h / 100) * 255;
|
||||
imageData.data[i * 4] = v;
|
||||
imageData.data[i * 4 + 1] = v;
|
||||
imageData.data[i * 4 + 2] = v;
|
||||
imageData.data[i * 4 + 3] = 255;
|
||||
|
||||
const n = i * 4;
|
||||
imageData.data[n] = v;
|
||||
imageData.data[n + 1] = v;
|
||||
imageData.data[n + 2] = v;
|
||||
imageData.data[n + 3] = 255;
|
||||
});
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function handleKeyup(event) {
|
|||
const alt = altKey || key === "Alt";
|
||||
|
||||
if (code === "F1") showInfo();
|
||||
else if (code === "F2") regeneratePrompt("hotkey");
|
||||
else if (code === "F2") regeneratePrompt();
|
||||
else if (code === "F6") quickSave();
|
||||
else if (code === "F9") quickLoad();
|
||||
else if (code === "Tab") toggleOptions(event);
|
||||
|
|
|
|||
|
|
@ -65,8 +65,8 @@ function editIce() {
|
|||
}
|
||||
|
||||
function addIcebergOnClick() {
|
||||
const point = d3.mouse(this);
|
||||
const i = findGridCell(point[0], point[1]);
|
||||
const [x, y] = d3.mouse(this);
|
||||
const i = findGridCell(x, y, grid);
|
||||
const c = grid.points[i];
|
||||
const s = +document.getElementById("iceSize").value;
|
||||
|
||||
|
|
|
|||
|
|
@ -151,16 +151,17 @@ function toggleHeight(event) {
|
|||
function drawHeightmap() {
|
||||
TIME && console.time("drawHeightmap");
|
||||
terrs.selectAll("*").remove();
|
||||
const cells = pack.cells,
|
||||
vertices = pack.vertices,
|
||||
n = cells.i.length;
|
||||
|
||||
const {cells, vertices} = pack;
|
||||
const n = cells.i.length;
|
||||
const used = new Uint8Array(cells.i.length);
|
||||
const paths = new Array(101).fill("");
|
||||
|
||||
const scheme = getColorScheme();
|
||||
const scheme = getColorScheme(terrs.attr("scheme"));
|
||||
const terracing = terrs.attr("terracing") / 10; // add additional shifted darker layer for pseudo-3d effect
|
||||
const skip = +terrs.attr("skip") + 1;
|
||||
const simplification = +terrs.attr("relax");
|
||||
|
||||
switch (+terrs.attr("curve")) {
|
||||
case 0:
|
||||
lineGen.curve(d3.curveBasisClosed);
|
||||
|
|
@ -233,8 +234,7 @@ function drawHeightmap() {
|
|||
TIME && console.timeEnd("drawHeightmap");
|
||||
}
|
||||
|
||||
function getColorScheme() {
|
||||
const scheme = terrs.attr("scheme");
|
||||
function getColorScheme(scheme) {
|
||||
if (scheme === "bright") return d3.scaleSequential(d3.interpolateSpectral);
|
||||
if (scheme === "light") return d3.scaleSequential(d3.interpolateRdYlGn);
|
||||
if (scheme === "green") return d3.scaleSequential(d3.interpolateGreens);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ function overviewMilitary() {
|
|||
// update military types in header and tooltips
|
||||
function updateHeaders() {
|
||||
const header = document.getElementById("militaryHeader");
|
||||
const units = options.military.length;
|
||||
header.style.gridTemplateColumns = `8em repeat(${units}, 5.2em) 4em 7em 5em 6em`;
|
||||
|
||||
header.querySelectorAll(".removable").forEach(el => el.remove());
|
||||
const insert = html => document.getElementById("militaryTotal").insertAdjacentHTML("beforebegin", html);
|
||||
for (const u of options.military) {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ function editNotes(id, name) {
|
|||
|
||||
$("#notesEditor").dialog({
|
||||
title: "Notes Editor",
|
||||
width: "70vw",
|
||||
width: "minmax(80vw, 540px)",
|
||||
height: window.innerHeight * 0.75,
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
close: removeEditor
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ $("#exitCustomization").draggable({handle: "div"});
|
|||
$("#mapLayers").disableSelection();
|
||||
|
||||
// remove glow if tip is aknowledged
|
||||
if (localStorage.getItem("disable_click_arrow_tooltip")) {
|
||||
if (stored("disable_click_arrow_tooltip")) {
|
||||
clearMainTip();
|
||||
optionsTrigger.classList.remove("glow");
|
||||
}
|
||||
|
||||
// Show options pane on trigger click
|
||||
function showOptions(event) {
|
||||
if (!localStorage.getItem("disable_click_arrow_tooltip")) {
|
||||
if (!stored("disable_click_arrow_tooltip")) {
|
||||
clearMainTip();
|
||||
localStorage.setItem("disable_click_arrow_tooltip", true);
|
||||
optionsTrigger.classList.remove("glow");
|
||||
|
|
@ -81,12 +81,12 @@ async function showSupporters() {
|
|||
}
|
||||
|
||||
// on any option or dialog change
|
||||
document.getElementById("options").addEventListener("change", checkIfStored);
|
||||
document.getElementById("dialogs").addEventListener("change", checkIfStored);
|
||||
document.getElementById("options").addEventListener("change", storeValueIfRequired);
|
||||
document.getElementById("dialogs").addEventListener("change", storeValueIfRequired);
|
||||
document.getElementById("options").addEventListener("input", updateOutputToFollowInput);
|
||||
document.getElementById("dialogs").addEventListener("input", updateOutputToFollowInput);
|
||||
|
||||
function checkIfStored(ev) {
|
||||
function storeValueIfRequired(ev) {
|
||||
if (ev.target.dataset.stored) lock(ev.target.dataset.stored);
|
||||
}
|
||||
|
||||
|
|
@ -142,6 +142,7 @@ optionsContent.addEventListener("click", function (event) {
|
|||
else if (id === "optionsMapHistory") showSeedHistoryDialog();
|
||||
else if (id === "optionsCopySeed") copyMapURL();
|
||||
else if (id === "optionsEraRegenerate") regenerateEra();
|
||||
else if (id === "templateInputContainer") openTemplateSelectionDialog();
|
||||
else if (id === "zoomExtentDefault") restoreDefaultZoomExtent();
|
||||
else if (id === "translateExtent") toggleTranslateExtent(event.target);
|
||||
else if (id === "speakerTest") testSpeaker();
|
||||
|
|
@ -232,7 +233,7 @@ const voiceInterval = setInterval(function () {
|
|||
voices.forEach((voice, i) => {
|
||||
select.options.add(new Option(voice.name, i, false));
|
||||
});
|
||||
if (stored("speakerVoice")) select.value = localStorage.getItem("speakerVoice");
|
||||
if (stored("speakerVoice")) select.value = stored("speakerVoice");
|
||||
// se voice to store
|
||||
else select.value = voices.findIndex(voice => voice.lang === "en-US"); // or to first found English-US
|
||||
}, 1000);
|
||||
|
|
@ -248,9 +249,9 @@ function testSpeaker() {
|
|||
speechSynthesis.speak(speaker);
|
||||
}
|
||||
|
||||
function generateMapWithSeed(source) {
|
||||
function generateMapWithSeed() {
|
||||
if (optionsSeed.value == seed) return tip("The current map already has this seed", false, "error");
|
||||
regeneratePrompt(source);
|
||||
regeneratePrompt();
|
||||
}
|
||||
|
||||
function showSeedHistoryDialog() {
|
||||
|
|
@ -272,14 +273,15 @@ function showSeedHistoryDialog() {
|
|||
|
||||
// generate map with historical seed
|
||||
function restoreSeed(id) {
|
||||
if (mapHistory[id].seed == seed) return tip("The current map is already generated with this seed", null, "error");
|
||||
const {seed, width, height, template} = mapHistory[id];
|
||||
byId("optionsSeed").value = seed;
|
||||
byId("mapWidthInput").value = width;
|
||||
byId("mapHeightInput").value = height;
|
||||
byId("templateInput").value = template;
|
||||
|
||||
optionsSeed.value = mapHistory[id].seed;
|
||||
mapWidthInput.value = mapHistory[id].width;
|
||||
mapHeightInput.value = mapHistory[id].height;
|
||||
templateInput.value = mapHistory[id].template;
|
||||
if (locked("template")) unlock("template");
|
||||
regeneratePrompt("seed history");
|
||||
|
||||
regeneratePrompt();
|
||||
}
|
||||
|
||||
function restoreDefaultZoomExtent() {
|
||||
|
|
@ -456,43 +458,50 @@ function changeZoomExtent(value) {
|
|||
zoom.scaleTo(svg, scale);
|
||||
}
|
||||
|
||||
// control stored options logic
|
||||
// restore options stored in localStorage
|
||||
function applyStoredOptions() {
|
||||
if (!localStorage.getItem("mapWidth") || !localStorage.getItem("mapHeight")) {
|
||||
if (!stored("mapWidth") || !stored("mapHeight")) {
|
||||
mapWidthInput.value = window.innerWidth;
|
||||
mapHeightInput.value = window.innerHeight;
|
||||
}
|
||||
|
||||
if (localStorage.getItem("distanceUnit")) applyOption(distanceUnitInput, localStorage.getItem("distanceUnit"));
|
||||
if (localStorage.getItem("heightUnit")) applyOption(heightUnit, localStorage.getItem("heightUnit"));
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const stored = localStorage.key(i);
|
||||
const value = localStorage.getItem(stored);
|
||||
|
||||
if (stored === "speakerVoice") continue;
|
||||
const input = document.getElementById(stored + "Input") || document.getElementById(stored);
|
||||
const output = document.getElementById(stored + "Output");
|
||||
if (input) input.value = value;
|
||||
if (output) output.value = value;
|
||||
lock(stored);
|
||||
|
||||
// add saved style presets to options
|
||||
if (stored.slice(0, 5) === "style") applyOption(stylePreset, stored, stored.slice(5));
|
||||
const heightmapId = stored("template");
|
||||
if (heightmapId) {
|
||||
const name = heightmapTemplates[heightmapId]?.name || precreatedHeightmaps[heightmapId]?.name || heightmapId;
|
||||
applyOption(byId("templateInput"), heightmapId, name);
|
||||
}
|
||||
|
||||
if (localStorage.getItem("winds"))
|
||||
if (stored("distanceUnit")) applyOption(distanceUnitInput, stored("distanceUnit"));
|
||||
if (stored("heightUnit")) applyOption(heightUnit, stored("heightUnit"));
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
|
||||
if (key === "speakerVoice") continue;
|
||||
const input = byId(key + "Input") || byId(key);
|
||||
const output = byId(key + "Output");
|
||||
|
||||
const value = stored(key);
|
||||
if (input) input.value = value;
|
||||
if (output) output.value = value;
|
||||
lock(key);
|
||||
|
||||
// add saved style presets to options
|
||||
if (key.slice(0, 5) === "style") applyOption(stylePreset, key, key.slice(5));
|
||||
}
|
||||
|
||||
if (stored("winds"))
|
||||
options.winds = localStorage
|
||||
.getItem("winds")
|
||||
.split(",")
|
||||
.map(w => +w);
|
||||
if (localStorage.getItem("military")) options.military = JSON.parse(localStorage.getItem("military"));
|
||||
if (stored("military")) options.military = JSON.parse(stored("military"));
|
||||
|
||||
if (localStorage.getItem("tooltipSize")) changeTooltipSize(localStorage.getItem("tooltipSize"));
|
||||
if (localStorage.getItem("regions")) changeStatesNumber(localStorage.getItem("regions"));
|
||||
if (stored("tooltipSize")) changeTooltipSize(stored("tooltipSize"));
|
||||
if (stored("regions")) changeStatesNumber(stored("regions"));
|
||||
|
||||
uiSizeInput.max = uiSizeOutput.max = getUImaxSize();
|
||||
if (localStorage.getItem("uiSize")) changeUIsize(localStorage.getItem("uiSize"));
|
||||
if (stored("uiSize")) changeUIsize(stored("uiSize"));
|
||||
else changeUIsize(minmax(rn(mapWidthInput.value / 1280, 1), 1, 2.5));
|
||||
|
||||
// search params overwrite stored and default options
|
||||
|
|
@ -502,8 +511,8 @@ function applyStoredOptions() {
|
|||
if (width) mapWidthInput.value = width;
|
||||
if (height) mapHeightInput.value = height;
|
||||
|
||||
const transparency = localStorage.getItem("transparency") || 5;
|
||||
const themeColor = localStorage.getItem("themeColor");
|
||||
const transparency = stored("transparency") || 5;
|
||||
const themeColor = stored("themeColor");
|
||||
changeDialogsTheme(themeColor, transparency);
|
||||
|
||||
setRendering(shapeRendering.value);
|
||||
|
|
@ -512,7 +521,6 @@ function applyStoredOptions() {
|
|||
|
||||
// randomize options if randomization is allowed (not locked or options='default')
|
||||
function randomizeOptions() {
|
||||
Math.random = aleaPRNG(seed); // reset seed to initial one
|
||||
const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options
|
||||
|
||||
// 'Options' settings
|
||||
|
|
@ -549,22 +557,13 @@ function randomizeOptions() {
|
|||
|
||||
// select heightmap template pseudo-randomly
|
||||
function randomizeHeightmapTemplate() {
|
||||
const templates = {
|
||||
volcano: 3,
|
||||
highIsland: 19,
|
||||
lowIsland: 9,
|
||||
continents: 16,
|
||||
archipelago: 18,
|
||||
mediterranean: 5,
|
||||
peninsula: 3,
|
||||
pangea: 5,
|
||||
isthmus: 2,
|
||||
atoll: 1,
|
||||
shattered: 7,
|
||||
taklamakan: 1,
|
||||
oldWorld: 11
|
||||
};
|
||||
document.getElementById("templateInput").value = rw(templates);
|
||||
const templates = {};
|
||||
for (const key in heightmapTemplates) {
|
||||
templates[key] = heightmapTemplates[key].probability || 0;
|
||||
}
|
||||
const template = rw(templates);
|
||||
const name = heightmapTemplates[template].name;
|
||||
applyOption(byId("templateInput"), template, name);
|
||||
}
|
||||
|
||||
// select culture set pseudo-randomly
|
||||
|
|
@ -623,6 +622,11 @@ function changeEra() {
|
|||
options.era = eraInput.value;
|
||||
}
|
||||
|
||||
async function openTemplateSelectionDialog() {
|
||||
const HeightmapSelectionDialog = await import("../dynamic/heightmap-selection.js?v=290520222");
|
||||
HeightmapSelectionDialog.open();
|
||||
}
|
||||
|
||||
// remove all saved data from LocalStorage and reload the page
|
||||
function restoreDefaultOptions() {
|
||||
localStorage.clear();
|
||||
|
|
@ -632,17 +636,17 @@ function restoreDefaultOptions() {
|
|||
// Sticked menu Options listeners
|
||||
document.getElementById("sticked").addEventListener("click", function (event) {
|
||||
const id = event.target.id;
|
||||
if (id === "newMapButton") regeneratePrompt("sticky button");
|
||||
if (id === "newMapButton") regeneratePrompt();
|
||||
else if (id === "saveButton") showSavePane();
|
||||
else if (id === "exportButton") showExportPane();
|
||||
else if (id === "loadButton") showLoadPane();
|
||||
else if (id === "zoomReset") resetZoom(1000);
|
||||
});
|
||||
|
||||
function regeneratePrompt(source) {
|
||||
function regeneratePrompt(options) {
|
||||
if (customization) return tip("New map cannot be generated when edit mode is active, please exit the mode and retry", false, "error");
|
||||
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
|
||||
if (workingTime < 5) return regenerateMap(source);
|
||||
if (workingTime < 5) return regenerateMap(options);
|
||||
|
||||
alertMessage.innerHTML = /* html */ `Are you sure you want to generate a new map?<br />
|
||||
All unsaved changes made to the current map will be lost`;
|
||||
|
|
@ -655,7 +659,7 @@ function regeneratePrompt(source) {
|
|||
},
|
||||
Generate: function () {
|
||||
closeDialogs();
|
||||
regenerateMap(source);
|
||||
regenerateMap(options);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ function overviewRegiments(state) {
|
|||
// update military types in header and tooltips
|
||||
function updateHeaders() {
|
||||
const header = document.getElementById("regimentsHeader");
|
||||
const units = options.military.length;
|
||||
header.style.gridTemplateColumns = `9em 13em repeat(${units}, 5.2em) 7em`;
|
||||
|
||||
header.querySelectorAll(".removable").forEach(el => el.remove());
|
||||
const insert = html => document.getElementById("regimentsTotal").insertAdjacentHTML("beforebegin", html);
|
||||
for (const u of options.military) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// UI elements for submap generation
|
||||
|
||||
window.UISubmap = (function () {
|
||||
const byId = document.getElementById.bind(document);
|
||||
byId("submapPointsInput").addEventListener("input", function () {
|
||||
const output = byId("submapPointsOutputFormatted");
|
||||
const cells = cellsDensityMap[+this.value] || 1000;
|
||||
|
|
@ -13,7 +12,7 @@ window.UISubmap = (function () {
|
|||
|
||||
byId("submapScaleInput").addEventListener("input", function (event) {
|
||||
const exp = Math.pow(1.1, +event.target.value);
|
||||
byId("submapScaleOutput").value = rn(exp,2);
|
||||
byId("submapScaleOutput").value = rn(exp, 2);
|
||||
});
|
||||
|
||||
byId("submapAngleInput").addEventListener("input", function (event) {
|
||||
|
|
@ -28,7 +27,6 @@ window.UISubmap = (function () {
|
|||
function openSubmapMenu() {
|
||||
$("#submapOptionsDialog").dialog({
|
||||
title: "Create a submap",
|
||||
width: "30em",
|
||||
resizable: false,
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
|
|
@ -49,8 +47,8 @@ window.UISubmap = (function () {
|
|||
shiftY: +byId("submapShiftY").value,
|
||||
ratio: +byId("submapScaleInput").value,
|
||||
mirrorH: byId("submapMirrorH").checked,
|
||||
mirrorV: byId("submapMirrorV").checked,
|
||||
})
|
||||
mirrorV: byId("submapMirrorV").checked
|
||||
});
|
||||
|
||||
async function openResampleMenu() {
|
||||
resetZoom(0);
|
||||
|
|
@ -64,14 +62,14 @@ window.UISubmap = (function () {
|
|||
$shiftX.value = 0;
|
||||
$shiftY.value = 0;
|
||||
|
||||
const previewScale = 400 / graphWidth;
|
||||
const [w, h] = [400, graphHeight * previewScale];
|
||||
$previewBox.style.width = w + 'px';
|
||||
$previewBox.style.height = h + 'px';
|
||||
$previewBox.style.position = 'relative';
|
||||
const w = Math.min(400, window.innerWidth * 0.5);
|
||||
const previewScale = w / graphWidth;
|
||||
const h = graphHeight * previewScale;
|
||||
$previewBox.style.width = w + "px";
|
||||
$previewBox.style.height = h + "px";
|
||||
|
||||
// handle mouse input
|
||||
const dispatchInput = e => e.dispatchEvent(new Event('input', {bubbles:true}));
|
||||
const dispatchInput = e => e.dispatchEvent(new Event("input", {bubbles: true}));
|
||||
|
||||
// mouse wheel
|
||||
$previewBox.onwheel = e => {
|
||||
|
|
@ -80,14 +78,16 @@ window.UISubmap = (function () {
|
|||
};
|
||||
|
||||
// mouse drag
|
||||
let mouseIsDown = false, mouseX = 0, mouseY = 0;
|
||||
let mouseIsDown = false,
|
||||
mouseX = 0,
|
||||
mouseY = 0;
|
||||
$previewBox.onmousedown = e => {
|
||||
mouseIsDown = true;
|
||||
mouseX = $shiftX.value - e.clientX / previewScale;
|
||||
mouseY = $shiftY.value - e.clientY / previewScale;
|
||||
}
|
||||
$previewBox.onmouseup = _ => mouseIsDown = false;
|
||||
$previewBox.onmouseleave = _ => mouseIsDown = false;
|
||||
};
|
||||
$previewBox.onmouseup = _ => (mouseIsDown = false);
|
||||
$previewBox.onmouseleave = _ => (mouseIsDown = false);
|
||||
$previewBox.onmousemove = e => {
|
||||
if (!mouseIsDown) return;
|
||||
e.preventDefault();
|
||||
|
|
@ -99,7 +99,6 @@ window.UISubmap = (function () {
|
|||
|
||||
$("#resampleDialog").dialog({
|
||||
title: "Transform map",
|
||||
width: "430px",
|
||||
resizable: false,
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
|
|
@ -114,7 +113,7 @@ window.UISubmap = (function () {
|
|||
});
|
||||
|
||||
// use double resolution for PNG to get sharper image
|
||||
const $preview = await loadPreview($previewBox, w*2, h*2);
|
||||
const $preview = await loadPreview($previewBox, w * 2, h * 2);
|
||||
// could be done with SVG. Faster to load, slower to use.
|
||||
// const $preview = await loadPreviewSVG($previewBox, w, h);
|
||||
$preview.style.position = "absolute";
|
||||
|
|
@ -122,22 +121,22 @@ window.UISubmap = (function () {
|
|||
$preview.style.height = h + "px";
|
||||
|
||||
byId("resampleDialog").oninput = event => {
|
||||
const { angle, shiftX, shiftY, ratio, mirrorH, mirrorV } = getTransformInput();
|
||||
const scale = Math.pow(1.1,ratio);
|
||||
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
|
||||
const scale = Math.pow(1.1, ratio);
|
||||
const transformStyle = `
|
||||
translate(${shiftX*previewScale}px, ${shiftY*previewScale}px)
|
||||
scale(${mirrorH?-scale:scale}, ${mirrorV?-scale:scale})
|
||||
translate(${shiftX * previewScale}px, ${shiftY * previewScale}px)
|
||||
scale(${mirrorH ? -scale : scale}, ${mirrorV ? -scale : scale})
|
||||
rotate(${angle}rad)
|
||||
`;
|
||||
|
||||
$preview.style.transform = transformStyle;
|
||||
$preview.style['transform-origin'] = 'center';
|
||||
$preview.style["transform-origin"] = "center";
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function loadPreview($container, w, h) {
|
||||
const url = await getMapURL("png", { globe: false, noWater: true, fullMap: true, noLabels: true, noScaleBar: true, noIce: true });
|
||||
const url = await getMapURL("png", {globe: false, noWater: true, fullMap: true, noLabels: true, noScaleBar: true, noIce: true});
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
|
@ -148,16 +147,16 @@ window.UISubmap = (function () {
|
|||
img.onload = function () {
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
};
|
||||
$container.textContent = '';
|
||||
$container.textContent = "";
|
||||
$container.appendChild(canvas);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// currently unused alternative to PNG version
|
||||
async function loadPreviewSVG($container, w, h) {
|
||||
$container.innerHTML = /*html*/`
|
||||
$container.innerHTML = /*html*/ `
|
||||
<svg id="submapPreviewSVG" viewBox="0 0 ${graphWidth} ${graphHeight}">
|
||||
<rect width="100%" height="100%" fill="${byId('styleOceanFill').value}" />
|
||||
<rect width="100%" height="100%" fill="${byId("styleOceanFill").value}" />
|
||||
<rect fill="url(#oceanic)" width="100%" height="100%" />
|
||||
<use href="#map"></use>
|
||||
</svg>
|
||||
|
|
@ -171,12 +170,12 @@ window.UISubmap = (function () {
|
|||
const cellNumId = +byId("submapPointsInput").value;
|
||||
if (!cellsDensityMap[cellNumId]) return console.error("Unknown cell number!");
|
||||
|
||||
const { angle, shiftX, shiftY, ratio, mirrorH, mirrorV } = getTransformInput()
|
||||
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
|
||||
|
||||
const [cx, cy] = [graphWidth / 2, graphHeight / 2];
|
||||
const rot = alfa => (x, y) => [(x - cx) * Math.cos(alfa) - (y - cy) * Math.sin(alfa) + cx, (y - cy) * Math.cos(alfa) + (x - cx) * Math.sin(alfa) + cy];
|
||||
const shift = (dx, dy) => (x, y) => [x + dx, y + dy];
|
||||
const scale = r => (x, y) => [(x-cx) * r + cx, (y-cy) * r + cy];
|
||||
const scale = r => (x, y) => [(x - cx) * r + cx, (y - cy) * r + cy];
|
||||
const flipH = (x, y) => [-x + 2 * cx, y];
|
||||
const flipV = (x, y) => [x, -y + 2 * cy];
|
||||
const app = (f, g) => (x, y) => f(...g(x, y));
|
||||
|
|
@ -186,7 +185,7 @@ window.UISubmap = (function () {
|
|||
let inverse = id;
|
||||
|
||||
if (angle) [projection, inverse] = [rot(angle), rot(-angle)];
|
||||
if (ratio) [projection, inverse] = [app(scale(Math.pow(1.1,ratio)), projection), app(inverse, scale(Math.pow(1.1,-ratio)))];
|
||||
if (ratio) [projection, inverse] = [app(scale(Math.pow(1.1, ratio)), projection), app(inverse, scale(Math.pow(1.1, -ratio)))];
|
||||
if (mirrorH) [projection, inverse] = [app(flipH, projection), app(inverse, flipH)];
|
||||
if (mirrorV) [projection, inverse] = [app(flipV, projection), app(inverse, flipV)];
|
||||
if (shiftX || shiftY) {
|
||||
|
|
@ -230,7 +229,7 @@ window.UISubmap = (function () {
|
|||
smoothHeightMap: scale > 2,
|
||||
inverse: (x, y) => [x / origScale + x0, y / origScale + y0],
|
||||
projection: (x, y) => [(x - x0) * origScale, (y - y0) * origScale],
|
||||
scale: origScale,
|
||||
scale: origScale
|
||||
};
|
||||
|
||||
// converting map position on the planet
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ function recalculatePopulation() {
|
|||
}
|
||||
|
||||
function regenerateStates() {
|
||||
const localSeed = Math.floor(Math.random() * 1e9); // new random seed
|
||||
const localSeed = generateSeed();
|
||||
Math.random = aleaPRNG(localSeed);
|
||||
|
||||
const statesCount = +regionsOutput.value;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ function editWorld() {
|
|||
$("#worldConfigurator").dialog({
|
||||
title: "Configure World",
|
||||
resizable: false,
|
||||
width: "42em",
|
||||
width: "minmax(40em, 85vw)",
|
||||
buttons: {
|
||||
"Whole World": () => applyWorldPreset(100, 50),
|
||||
Northern: () => applyWorldPreset(33, 25),
|
||||
|
|
@ -73,16 +73,16 @@ function editWorld() {
|
|||
const eqD = ((graphHeight / 2) * 100) / size;
|
||||
|
||||
calculateMapCoordinates();
|
||||
const mc = mapCoordinates; // shortcut
|
||||
const scale = +distanceScaleInput.value,
|
||||
unit = distanceUnitInput.value;
|
||||
const mc = mapCoordinates;
|
||||
const scale = +distanceScaleInput.value;
|
||||
const unit = distanceUnitInput.value;
|
||||
const meridian = toKilometer(eqD * 2 * scale);
|
||||
document.getElementById("mapSize").innerHTML = /* html */ `${graphWidth}x${graphHeight}`;
|
||||
document.getElementById("mapSizeFriendly").innerHTML = /* html */ `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
|
||||
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
|
||||
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
|
||||
document.getElementById("meridianLength").innerHTML = rn(eqD * 2);
|
||||
document.getElementById("meridianLengthFriendly").innerHTML = /* html */ `${rn(eqD * 2 * scale)} ${unit}`;
|
||||
document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
|
||||
document.getElementById("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : "";
|
||||
document.getElementById("mapCoordinates").innerHTML = /* html */ `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
|
||||
document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
|
||||
|
||||
function toKilometer(v) {
|
||||
if (unit === "km") return v;
|
||||
|
|
@ -92,9 +92,11 @@ function editWorld() {
|
|||
return 0; // 0 if distanceUnitInput is a custom unit
|
||||
}
|
||||
|
||||
// parse latitude value
|
||||
function lat(lat) {
|
||||
return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";
|
||||
} // parse latitude value
|
||||
}
|
||||
|
||||
const area = d3.geoGraticule().extent([
|
||||
[mc.lonW, mc.latN],
|
||||
[mc.lonE, mc.latS]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"use strict";
|
||||
// FMG utils related to arrays
|
||||
|
||||
// return the last element of array
|
||||
function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
|
|
@ -37,9 +35,24 @@ function deepCopy(obj) {
|
|||
[Set, s => [...s.values()].map(dcAny)],
|
||||
[Date, d => new Date(d.getTime())],
|
||||
[Object, dcObject]
|
||||
// other types will be referenced
|
||||
// ... extend here to implement their custom deep copy
|
||||
]);
|
||||
|
||||
return dcAny(obj);
|
||||
}
|
||||
|
||||
function getTypedArray(maxValue) {
|
||||
console.assert(
|
||||
Number.isInteger(maxValue) && maxValue >= 0 && maxValue <= 4294967295,
|
||||
`Array maxValue must be an integer between 0 and 4294967295, got ${maxValue}`
|
||||
);
|
||||
|
||||
if (maxValue <= 255) return Uint8Array;
|
||||
if (maxValue <= 65535) return Uint16Array;
|
||||
if (maxValue <= 4294967295) return Uint32Array;
|
||||
return Uint32Array;
|
||||
}
|
||||
|
||||
function createTypedArray({maxValue, length}) {
|
||||
return new (getTypedArray(maxValue))(length);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,60 @@
|
|||
"use strict";
|
||||
// FMG utils related to graph
|
||||
|
||||
// add boundary points to pseudo-clip voronoi cells
|
||||
// check if new grid graph should be generated or we can use the existing one
|
||||
function shouldRegenerateGrid(grid) {
|
||||
const cellsDesired = +byId("pointsInput").dataset.cells;
|
||||
if (cellsDesired !== grid.cellsDesired) return true;
|
||||
|
||||
const newSpacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2);
|
||||
const newCellsX = Math.floor((graphWidth + 0.5 * newSpacing - 1e-10) / newSpacing);
|
||||
const newCellsY = Math.floor((graphHeight + 0.5 * newSpacing - 1e-10) / newSpacing);
|
||||
|
||||
return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY;
|
||||
}
|
||||
|
||||
function generateGrid() {
|
||||
Math.random = aleaPRNG(seed); // reset PRNG
|
||||
const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints();
|
||||
const {cells, vertices} = calculateVoronoi(points, boundary);
|
||||
return {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices};
|
||||
}
|
||||
|
||||
// place random points to calculate Voronoi diagram
|
||||
function placePoints() {
|
||||
TIME && console.time("placePoints");
|
||||
const cellsDesired = +byId("pointsInput").dataset.cells;
|
||||
const spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); // spacing between points before jirrering
|
||||
|
||||
const boundary = getBoundaryPoints(graphWidth, graphHeight, spacing);
|
||||
const points = getJitteredGrid(graphWidth, graphHeight, spacing); // points of jittered square grid
|
||||
const cellsX = Math.floor((graphWidth + 0.5 * spacing - 1e-10) / spacing);
|
||||
const cellsY = Math.floor((graphHeight + 0.5 * spacing - 1e-10) / spacing);
|
||||
TIME && console.timeEnd("placePoints");
|
||||
|
||||
return {spacing, cellsDesired, boundary, points, cellsX, cellsY};
|
||||
}
|
||||
|
||||
// calculate Delaunay and then Voronoi diagram
|
||||
function calculateVoronoi(points, boundary) {
|
||||
TIME && console.time("calculateDelaunay");
|
||||
const allPoints = points.concat(boundary);
|
||||
const delaunay = Delaunator.from(allPoints);
|
||||
TIME && console.timeEnd("calculateDelaunay");
|
||||
|
||||
TIME && console.time("calculateVoronoi");
|
||||
const n = points.length;
|
||||
const voronoi = new Voronoi(delaunay, allPoints, n);
|
||||
|
||||
const cells = voronoi.cells;
|
||||
cells.i = getTypedArray(n).from(d3.range(n)); // array of indexes
|
||||
const vertices = voronoi.vertices;
|
||||
TIME && console.timeEnd("calculateVoronoi");
|
||||
|
||||
return {cells, vertices};
|
||||
}
|
||||
|
||||
// add points along map edge to pseudo-clip voronoi cells
|
||||
function getBoundaryPoints(width, height, spacing) {
|
||||
const offset = rn(-1 * spacing);
|
||||
const bSpacing = spacing * 2;
|
||||
|
|
@ -9,15 +62,18 @@ function getBoundaryPoints(width, height, spacing) {
|
|||
const h = height - offset * 2;
|
||||
const numberX = Math.ceil(w / bSpacing) - 1;
|
||||
const numberY = Math.ceil(h / bSpacing) - 1;
|
||||
let points = [];
|
||||
const points = [];
|
||||
|
||||
for (let i = 0.5; i < numberX; i++) {
|
||||
let x = Math.ceil((w * i) / numberX + offset);
|
||||
points.push([x, offset], [x, h + offset]);
|
||||
}
|
||||
|
||||
for (let i = 0.5; i < numberY; i++) {
|
||||
let y = Math.ceil((h * i) / numberY + offset);
|
||||
points.push([offset, y], [w + offset, y]);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +96,7 @@ function getJitteredGrid(width, height, spacing) {
|
|||
}
|
||||
|
||||
// return cell index on a regular square grid
|
||||
function findGridCell(x, y) {
|
||||
function findGridCell(x, y, grid) {
|
||||
return Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX + Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1));
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +104,7 @@ function findGridCell(x, y) {
|
|||
function findGridAll(x, y, radius) {
|
||||
const c = grid.cells.c;
|
||||
let r = Math.floor(radius / grid.spacing);
|
||||
let found = [findGridCell(x, y)];
|
||||
let found = [findGridCell(x, y, grid)];
|
||||
if (!r || radius === 1) return found;
|
||||
if (r > 0) found = found.concat(c[found[0]]);
|
||||
if (r > 1) {
|
||||
|
|
@ -261,7 +317,7 @@ function drawCellsValue(data) {
|
|||
function drawPolygons(data) {
|
||||
const max = d3.max(data),
|
||||
min = d3.min(data),
|
||||
scheme = getColorScheme();
|
||||
scheme = getColorScheme(terrs.attr("scheme"));
|
||||
data = data.map(d => 1 - normalize(d, min, max));
|
||||
|
||||
debug.selectAll("polygon").remove();
|
||||
|
|
|
|||
|
|
@ -74,3 +74,7 @@ function getNumberInRange(r) {
|
|||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function generateSeed() {
|
||||
return String(Math.floor(Math.random() * 1e9));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
// version and caching control
|
||||
|
||||
const version = "1.83.0"; // generator version, update each time
|
||||
const version = "1.84.0"; // generator version, update each time
|
||||
|
||||
{
|
||||
document.title += " v" + version;
|
||||
|
|
@ -28,6 +28,8 @@ const version = "1.83.0"; // generator version, update each time
|
|||
|
||||
<ul>
|
||||
<strong>Latest changes:</strong>
|
||||
<li>Heightmap selection screen</li>
|
||||
<li>Dialogs optimization for mobile</li>
|
||||
<li>New heightmap template: Fractious</li>
|
||||
<li>Template Editor: mask and invert tools</li>
|
||||
<li>Ability to install the App</li>
|
||||
|
|
@ -36,7 +38,6 @@ const version = "1.83.0"; // generator version, update each time
|
|||
<li>Submap tool by Goteguru</li>
|
||||
<li>Resample tool by Goteguru</li>
|
||||
<li>Pre-defined heightmaps</li>
|
||||
<li>Advanced notes editor</li>
|
||||
</ul>
|
||||
|
||||
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue