Merge pull request #821 from Azgaar/heightmap-select-dialog

Heightmap select dialog
This commit is contained in:
Azgaar 2022-06-01 01:05:19 +03:00 committed by GitHub
commit b76a0b83dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1455 additions and 926 deletions

View file

@ -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}
};
})();

View 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"}
};

View file

@ -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%;
}

File diff suppressed because it is too large Load diff

2
libs/jquery-ui.css vendored
View file

@ -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
View file

@ -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

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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&nbsp;</div>
<div style="left: 9.9em" data-tip="Click to sort by type" class="sortable alphabetically" data-sortby="type">Type&nbsp;</div>
<div style="left: 16.2em" data-tip="Click to sort by culture namesbase" class="sortable" data-sortby="base">Namesbase&nbsp;</div>
<div style="left: 24.5em" data-tip="Click to sort by culture cells count" class="sortable hide" data-sortby="cells">Cells&nbsp;</div>
<div style="left: 29.8em" data-tip="Click to sort by expansionism" class="sortable hide" data-sortby="expansionism">Expansion&nbsp;</div>
<div style="left: 37.2em" data-tip="Click to sort by culture area" class="sortable hide" data-sortby="area">Area&nbsp;</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&nbsp;</div>
<div style="left: 50.8em" data-tip="Click to sort by culture emblems shape" class="sortable alphabetically hide" data-sortby="emblems">Emblems&nbsp;</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&nbsp;</div>
<div data-tip="Click to sort by type" class="sortable alphabetically" data-sortby="type">Type&nbsp;</div>
<div data-tip="Click to sort by culture namesbase" class="sortable" data-sortby="base">Namesbase&nbsp;</div>
<div data-tip="Click to sort by culture cells count" class="sortable hide" data-sortby="cells">Cells&nbsp;</div>
<div data-tip="Click to sort by expansionism" class="sortable hide" data-sortby="expansionism">Expansion&nbsp;</div>
<div data-tip="Click to sort by culture area" class="sortable hide" data-sortby="area">Area&nbsp;</div>
<div data-tip="Click to sort by culture population" class="sortable hide icon-sort-number-down" data-sortby="population">Population&nbsp;</div>
<div data-tip="Click to sort by culture emblems shape" class="sortable alphabetically hide" data-sortby="emblems">Emblems&nbsp;</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"

View file

@ -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&nbsp;</div>
<div style="left: 10.8em" data-tip="Click to sort by state form name" class="sortable alphabetically" data-sortby="form">Form&nbsp;</div>
<div style="left: 19.1em" data-tip="Click to sort by capital name" class="sortable alphabetically hide" data-sortby="capital">Capital&nbsp;</div>
<div style="left: 26.1em" data-tip="Click to sort by state dominant culture" class="sortable alphabetically hide" data-sortby="culture">Culture&nbsp;</div>
<div style="left: 33.4em" data-tip="Click to sort by state burgs count" class="sortable hide" data-sortby="burgs">Burgs&nbsp;</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&nbsp;</div>
<div style="left: 45.9em" data-tip="Click to sort by state population" class="sortable hide" data-sortby="population">Population&nbsp;</div>
<div style="left: 52.2em" data-tip="Click to sort by state type" class="sortable alphabetically hidden show hide" data-sortby="type">Type&nbsp;</div>
<div style="left: 59em" data-tip="Click to sort by state expansion value" class="sortable hidden show hide" data-sortby="expansionism">Expansion&nbsp;</div>
<div style="left: 65.5em" data-tip="Click to sort by state cells count" class="sortable hidden show hide" data-sortby="cells">Cells&nbsp;</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&nbsp;</div>
<div data-tip="Click to sort by state form name" class="sortable alphabetically" data-sortby="form">Form&nbsp;</div>
<div data-tip="Click to sort by capital name" class="sortable alphabetically hide" data-sortby="capital">Capital&nbsp;</div>
<div data-tip="Click to sort by state dominant culture" class="sortable alphabetically hide" data-sortby="culture">Culture&nbsp;</div>
<div data-tip="Click to sort by state burgs count" class="sortable hide" data-sortby="burgs">Burgs&nbsp;</div>
<div data-tip="Click to sort by state area" class="sortable hide icon-sort-number-down" data-sortby="area">Area&nbsp;</div>
<div data-tip="Click to sort by state population" class="sortable hide" data-sortby="population">Population&nbsp;</div>
<div data-tip="Click to sort by state type" class="sortable alphabetically hidden show hide" data-sortby="type">Type&nbsp;</div>
<div data-tip="Click to sort by state expansion value" class="sortable hidden show hide" data-sortby="expansionism">Expansion&nbsp;</div>
<div data-tip="Click to sort by state cells count" class="sortable hidden show hide" data-sortby="cells">Cells&nbsp;</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"

View 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})
});
}

View file

@ -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
};
})();

View file

@ -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")

View file

@ -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);
}

View file

@ -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);

View file

@ -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) => {

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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) {

View file

@ -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

View file

@ -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);
}
}
});

View file

@ -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) {

View file

@ -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

View file

@ -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;

View file

@ -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]

View file

@ -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);
}

View file

@ -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();

View file

@ -74,3 +74,7 @@ function getNumberInRange(r) {
}
return count;
}
function generateSeed() {
return String(Math.floor(Math.random() * 1e9));
}

View file

@ -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>