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"; "use strict";
window.HeightmapTemplates = (function () { const heightmapTemplates = (function () {
const volcano = `Hill 1 90-100 44-56 40-60 const volcano = `Hill 1 90-100 44-56 40-60
Multiply 0.8 50-100 0 0 Multiply 0.8 50-100 0 0
Range 1.5 30-55 45-55 40-60 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`; Range 6-8 40-50 5-95 10-90`;
return { return {
volcano, volcano: {id: 0, name: "Volcano", template: volcano, probability: 3},
highIsland, highIsland: {id: 1, name: "High Island", template: highIsland, probability: 19},
lowIsland, lowIsland: {id: 2, name: "Low Island", template: lowIsland, probability: 9},
continents, continents: {id: 3, name: "Continents", template: continents, probability: 16},
archipelago, archipelago: {id: 4, name: "Archipelago", template: archipelago, probability: 18},
atoll, atoll: {id: 5, name: "Atoll", template: atoll, probability: 1},
mediterranean, mediterranean: {id: 6, name: "Mediterranean", template: mediterranean, probability: 5},
peninsula, peninsula: {id: 7, name: "Peninsula", template: peninsula, probability: 3},
peninsula, pangea: {id: 8, name: "Pangea", template: pangea, probability: 5},
pangea, isthmus: {id: 9, name: "Isthmus", template: isthmus, probability: 2},
isthmus, shattered: {id: 10, name: "Shattered", template: shattered, probability: 7},
shattered, taklamakan: {id: 11, name: "Taklamakan", template: taklamakan, probability: 1},
taklamakan, oldWorld: {id: 12, name: "Old World", template: oldWorld, probability: 8},
oldWorld, fractious: {id: 13, name: "Fractious", template: fractious, probability: 3}
fractious
}; };
})(); })();

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; 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 { #options {
position: absolute; position: absolute;
font-family: Consolas, monospace; font-family: Consolas, monospace;
@ -929,7 +943,7 @@ fieldset {
padding: 0.1em 0.5em; padding: 0.1em 0.5em;
float: left; float: left;
font-size: 1.2em; font-size: 1.2em;
font-family: Copperplate, monospace; font-family: monospace;
} }
#brushesButtons > button { #brushesButtons > button {
@ -1318,6 +1332,13 @@ div.slider .ui-slider-handle {
scrollbar-width: thin; scrollbar-width: thin;
} }
@media screen and (max-width: 600px) {
.table {
max-width: unset;
}
}
.dialog::-webkit-scrollbar,
#alertMessage::-webkit-scrollbar, #alertMessage::-webkit-scrollbar,
.table::-webkit-scrollbar, .table::-webkit-scrollbar,
.matrix-table::-webkit-scrollbar { .matrix-table::-webkit-scrollbar {
@ -1326,6 +1347,7 @@ div.slider .ui-slider-handle {
background-color: transparent; background-color: transparent;
} }
.dialog::-webkit-scrollbar-thumb,
#alertMessage::-webkit-scrollbar-thumb, #alertMessage::-webkit-scrollbar-thumb,
.table::-webkit-scrollbar-thumb, .table::-webkit-scrollbar-thumb,
.matrix-table::-webkit-scrollbar-thumb { .matrix-table::-webkit-scrollbar-thumb {
@ -1333,30 +1355,30 @@ div.slider .ui-slider-handle {
border-radius: 6px; border-radius: 6px;
} }
.dialog::-webkit-scrollbar-thumb:hover,
#alertMessage::-webkit-scrollbar-thumb:hover, #alertMessage::-webkit-scrollbar-thumb:hover,
.table::-webkit-scrollbar-thumb:hover, .table::-webkit-scrollbar-thumb:hover,
.matrix-table::-webkit-scrollbar-thumb:hover { .matrix-table::-webkit-scrollbar-thumb:hover {
background: #666; background: #666;
} }
.overflow { .dialog {
max-width: 93vw; max-width: 93vw;
overflow: auto;
max-height: 75vh;
} }
.overflow > div { .dialog > div {
width: max-content; width: max-content;
} }
div.header > div { div.header {
display: grid;
width: 0;
font-weight: bold; font-weight: bold;
font-size: 0.9em; font-size: 0.9em;
display: inline-block; }
position: sticky;
white-space: nowrap; div.header > div:first-child {
overflow-x: hidden; margin-left: 1.8em;
vertical-align: bottom;
} }
.sortable { .sortable {
@ -1655,11 +1677,6 @@ div.states > div.biomeArea {
width: 5em; width: 5em;
} }
#militaryHeader > div,
#regimentsHeader > div {
width: 5.2em;
}
#militaryBody div.states > input { #militaryBody div.states > input {
-moz-appearance: textfield; -moz-appearance: textfield;
} }
@ -1978,12 +1995,9 @@ input[type="checkbox"] {
} }
div.textual select, div.textual select,
div.textual textarea { div.textual textarea,
font-family: Copperplate, monospace;
}
div.textual input { div.textual input {
font-family: Copperplate, monospace; font-family: monospace;
} }
div.textual fieldset { div.textual fieldset {
@ -1998,7 +2012,7 @@ div.textual span,
} }
#namesbaseExamples { #namesbaseExamples {
font-family: Copperplate, monospace; font-family: monospace;
cursor: pointer; cursor: pointer;
} }
@ -2063,7 +2077,7 @@ div.textual span,
outline: 0; outline: 0;
overflow-y: auto; overflow-y: auto;
padding: 0.6em; padding: 0.6em;
font-family: Copperplate, monospace; font-family: monospace;
background-color: #fff; background-color: #fff;
border: 1px solid #dedede; border: 1px solid #dedede;
color: #000; color: #000;
@ -2092,6 +2106,7 @@ svg.button {
#reliefIconsDiv { #reliefIconsDiv {
margin-top: 2px; margin-top: 2px;
padding: 2px; padding: 2px;
width: 100%;
} }
#reliefIconsDiv svg { #reliefIconsDiv svg {
@ -2301,11 +2316,8 @@ svg.button {
#mapOverlay { #mapOverlay {
position: absolute; position: absolute;
inset: 0;
display: flex; display: flex;
top: 0;
left: 0;
right: 0;
bottom: 0;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 10; z-index: 10;
@ -2340,17 +2352,6 @@ svg.button {
} }
@media only screen and (max-width: 420px) { @media only screen and (max-width: 420px) {
#collapsible,
#options {
margin: 0;
border: 0;
}
#options {
height: 100vh;
width: 100vw;
}
table { table {
width: 100%; 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; padding: 0.5em 1em;
background: none; background: none;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: auto;
} }
.ui-dialog .ui-dialog-buttonpane { .ui-dialog .ui-dialog-buttonpane {
text-align: left; text-align: left;

148
main.js
View file

@ -10,6 +10,9 @@ const TIME = DEBUG || !PRODUCTION;
const WARN = true; const WARN = true;
const ERROR = true; const ERROR = true;
// detect device
const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile;
if (PRODUCTION && "serviceWorker" in navigator) { if (PRODUCTION && "serviceWorker" in navigator) {
window.addEventListener("load", () => { window.addEventListener("load", () => {
navigator.serviceWorker.register("./sw.js").catch(err => { 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 { } else {
hideLoading(); hideLoading();
await checkLoadParameters(); await checkLoadParameters();
@ -212,15 +212,13 @@ document.addEventListener("DOMContentLoaded", async () => {
}); });
function hideLoading() { function hideLoading() {
d3.select("#loading").transition().duration(4000).style("opacity", 0); d3.select("#loading").transition().duration(3000).style("opacity", 0);
d3.select("#initial").transition().duration(4000).attr("opacity", 0); d3.select("#optionsContainer").transition().duration(2000).style("opacity", 1);
d3.select("#optionsContainer").transition().duration(3000).style("opacity", 1); d3.select("#tooltip").transition().duration(3000).style("opacity", 1);
d3.select("#tooltip").transition().duration(4000).style("opacity", 1);
} }
function showLoading() { function showLoading() {
d3.select("#loading").transition().duration(200).style("opacity", 1); 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("#optionsContainer").transition().duration(100).style("opacity", 0);
d3.select("#tooltip").transition().duration(200).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 { try {
const timeStart = performance.now(); const timeStart = performance.now();
const {seed: precreatedSeed, graph: precreatedGraph} = options || {};
invokeActiveZooming(); invokeActiveZooming();
generateSeed(); setSeed(precreatedSeed);
INFO && console.group("Generated Map " + seed); INFO && console.group("Generated Map " + seed);
applyMapSize(); applyMapSize();
randomizeOptions(); randomizeOptions();
placePoints();
calculateVoronoi(grid, grid.points); if (shouldRegenerateGrid(grid)) grid = precreatedGraph || generateGrid();
drawScaleBar(scale); else delete grid.cells.h;
await HeightmapGenerator.generate(); grid.cells.h = await HeightmapGenerator.generate(grid);
markFeatures(); markFeatures();
markupGridOcean(); markupGridOcean();
addLakesInDeepDepressions(); addLakesInDeepDepressions();
@ -650,6 +652,7 @@ async function generate() {
calculateMapCoordinates(); calculateMapCoordinates();
calculateTemperatures(); calculateTemperatures();
generatePrecipitation(); generatePrecipitation();
reGraph(); reGraph();
drawCoastline(); drawCoastline();
@ -677,6 +680,8 @@ async function generate() {
Military.generate(); Military.generate();
Markers.generate(); Markers.generate();
addZones(); addZones();
drawScaleBar(scale);
Names.getMapName(); Names.getMapName();
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`); 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 // set map seed (string!)
function generateSeed() { function setSeed(precreatedSeed) {
const first = !mapHistory[0]; if (!precreatedSeed) {
const url = new URL(window.location.href); const first = !mapHistory[0];
const params = url.searchParams; const url = new URL(window.location.href);
const urlSeed = url.searchParams.get("seed"); const params = url.searchParams;
if (first && params.get("from") === "MFCG" && urlSeed.length === 13) seed = urlSeed.slice(0, -4); const urlSeed = url.searchParams.get("seed");
else if (first && urlSeed) seed = urlSeed; if (first && params.get("from") === "MFCG" && urlSeed.length === 13) seed = urlSeed.slice(0, -4);
else if (optionsSeed.value && optionsSeed.value != seed) seed = optionsSeed.value; else if (first && urlSeed) seed = urlSeed;
else seed = Math.floor(Math.random() * 1e9).toString(); else if (optionsSeed.value && optionsSeed.value != seed) seed = optionsSeed.value;
optionsSeed.value = seed; else seed = generateSeed();
} else {
seed = precreatedSeed;
}
byId("optionsSeed").value = seed;
Math.random = aleaPRNG(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 // Mark features (ocean, lakes, islands) and calculate distance field
function markFeatures() { function markFeatures() {
TIME && console.time("markFeatures"); TIME && console.time("markFeatures");
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
const cells = grid.cells, const cells = grid.cells;
heights = grid.cells.h; const heights = grid.cells.h;
cells.f = new Uint16Array(cells.i.length); // cell feature number 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 cells.t = new Int8Array(cells.i.length); // cell type: 1 = land coast; -1 = water near coast
grid.features = [0]; 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) // 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() { 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 cells = grid.cells;
const features = grid.features; const features = grid.features;
@ -924,7 +904,7 @@ function defineMapSize() {
if (randomize || !locked("latitude")) latitudeOutput.value = latitudeInput.value = rn(latitude); if (randomize || !locked("latitude")) latitudeOutput.value = latitudeInput.value = rn(latitude);
function getSizeAndLatitude() { 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 === "africa-centric") return [45, 53];
if (template === "arabia") return [20, 35]; if (template === "arabia") return [20, 35];
@ -1181,8 +1161,8 @@ function generatePrecipitation() {
// recalculate Voronoi Graph to pack cells // recalculate Voronoi Graph to pack cells
function reGraph() { function reGraph() {
TIME && console.time("reGraph"); TIME && console.time("reGraph");
let {cells, points, features} = grid; const {cells, points, features} = grid;
const newCells = {p: [], g: [], h: []}; // to store new data const newCells = {p: [], g: [], h: []}; // store new data
const spacing2 = grid.spacing ** 2; const spacing2 = grid.spacing ** 2;
for (const i of cells.i) { for (const i of cells.i) {
@ -1216,14 +1196,17 @@ function reGraph() {
newCells.h.push(height); newCells.h.push(height);
} }
calculateVoronoi(pack, newCells.p); function getCellArea(i) {
cells = pack.cells; const area = Math.abs(d3.polygonArea(getPackPolygon(i)));
cells.p = newCells.p; // points coordinates [x, y] return Math.min(area, 65535);
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 pack = calculateVoronoi(newCells.p, grid.boundary);
cells.area = new Uint16Array(cells.i.length); // cell area pack.cells.p = newCells.p;
cells.i.forEach(i => (cells.area[i] = Math.abs(d3.polygonArea(getPackPolygon(i))))); 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"); TIME && console.timeEnd("reGraph");
} }
@ -1901,11 +1884,14 @@ function addZones(number = 1) {
// show map stats on generation complete // show map stats on generation complete
function showStatistics() { function showStatistics() {
const template = templateInput.options[templateInput.selectedIndex].text; const heightmap = byId("templateInput").value;
const templateRandom = locked("template") ? "" : "(random)"; const isTemplate = heightmap in heightmapTemplates;
const heightmapType = isTemplate ? "template" : "precreated";
const isRandomTemplate = isTemplate && !locked("template") ? "random " : "";
const stats = ` Seed: ${seed} const stats = ` Seed: ${seed}
Canvas size: ${graphWidth}x${graphHeight} Canvas size: ${graphWidth}x${graphHeight} px
Template: ${template} ${templateRandom} Heightmap: ${heightmap} (${isRandomTemplate}${heightmapType})
Points: ${grid.points.length} Points: ${grid.points.length}
Cells: ${pack.cells.i.length} Cells: ${pack.cells.i.length}
Map size: ${mapSizeOutput.value}% Map size: ${mapSizeOutput.value}%
@ -1917,22 +1903,28 @@ function showStatistics() {
Cultures: ${pack.cultures.length - 1}`; Cultures: ${pack.cultures.length - 1}`;
mapId = Date.now(); // unique map id is it's creation date number 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); INFO && console.log(stats);
} }
const regenerateMap = debounce(async function () { const regenerateMap = debounce(async function (options) {
WARN && console.warn("Generate new random map"); WARN && console.warn("Generate new random map");
showLoading();
const cellsDesired = +byId("pointsInput").dataset.cells;
const shouldShowLoading = cellsDesired > 10000;
shouldShowLoading && showLoading();
closeDialogs("#worldConfigurator, #options3d"); closeDialogs("#worldConfigurator, #options3d");
customization = 0; customization = 0;
resetZoom(1000); resetZoom(1000);
undraw(); undraw();
await generate(); await generate(options);
restoreLayers(); restoreLayers();
if (ThreeD.options.isOn) ThreeD.redraw(); if (ThreeD.options.isOn) ThreeD.redraw();
if ($("#worldConfigurator").is(":visible")) editWorld(); if ($("#worldConfigurator").is(":visible")) editWorld();
hideLoading();
shouldShowLoading && hideLoading();
clearMainTip();
}, 1000); }, 1000);
// clear the map // clear the map

View file

@ -1077,7 +1077,7 @@ window.BurgsAndStates = (function () {
const generateProvinces = function (regenerate) { const generateProvinces = function (regenerate) {
TIME && console.time("generateProvinces"); TIME && console.time("generateProvinces");
const localSeed = regenerate ? Math.floor(Math.random() * 1e9).toString() : seed; const localSeed = regenerate ? generateSeed() : seed;
Math.random = aleaPRNG(localSeed); Math.random = aleaPRNG(localSeed);
const {cells, states, burgs} = pack; const {cells, states, burgs} = pack;

View file

@ -520,4 +520,9 @@ export function resolveVersionConflicts(version) {
if (!zone.dataset.type) zone.dataset.type = "Unknown"; 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({ $("#culturesEditor").dialog({
title: "Cultures Editor", title: "Cultures Editor",
resizable: false, resizable: false,
width: fitContent(),
close: closeCulturesEditor, close: closeCulturesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"} position: {my: "right top", at: "right-10 top+10", of: "svg"}
}); });
@ -24,16 +23,16 @@ export function open() {
} }
function insertEditorHtml() { function insertEditorHtml() {
const editorHtml = /* html */ `<div id="culturesEditor" class="dialog stable" style="display: none"> const editorHtml = /* html */ `<div id="culturesEditor" class="dialog stable">
<div id="culturesHeader" class="header"> <div id="culturesHeader" class="header" style="grid-template-columns: 10em 7em 8em 4em 8em 5em 8em 8em">
<div style="left: 1.8em" data-tip="Click to sort by culture name" class="sortable alphabetically" data-sortby="name">Culture&nbsp;</div> <div 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 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 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 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 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 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 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> <div data-tip="Click to sort by culture emblems shape" class="sortable alphabetically hide" data-sortby="emblems">Emblems&nbsp;</div>
</div> </div>
<div id="culturesBody" class="table" data-type="absolute"></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="culturesHeirarchy" data-tip="Show cultures hierarchy tree" class="icon-sitemap"></button>
<button id="culturesManually" data-tip="Manually re-assign cultures" class="icon-brush"></button> <button id="culturesManually" data-tip="Manually re-assign cultures" class="icon-brush"></button>
<div id="culturesManuallyButtons" style="display: none"> <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 <input
id="culturesManuallyBrush" id="culturesManuallyBrush"
oninput="tip('Brush size: '+this.value); culturesManuallyBrushNumber.value = this.value" oninput="tip('Brush size: '+this.value); culturesManuallyBrushNumber.value = this.value"

View file

@ -14,25 +14,24 @@ export function open() {
$("#statesEditor").dialog({ $("#statesEditor").dialog({
title: "States Editor", title: "States Editor",
resizable: false, resizable: false,
width: fitContent(),
close: closeStatesEditor, close: closeStatesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"} position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
}); });
} }
function insertEditorHtml() { function insertEditorHtml() {
const editorHtml = /* html */ `<div id="statesEditor" class="dialog stable" style="display: none"> const editorHtml = /* html */ `<div id="statesEditor" class="dialog stable">
<div id="statesHeader" class="header"> <div id="statesHeader" class="header" style="grid-template-columns: 11em 8em 7em 7em 6em 6em 8em 6em 7em 6em">
<div style="left: 1.8em" 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 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 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 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 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 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 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 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 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 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> <div data-tip="Click to sort by state cells count" class="sortable hidden show hide" data-sortby="cells">Cells&nbsp;</div>
</div> </div>
<div id="statesBodySection" class="table" data-type="absolute"></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> <button id="statesManually" data-tip="Manually re-assign states" class="icon-brush"></button>
<div id="statesManuallyButtons" style="display: none"> <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: >Brush size:
<input <input
id="statesManuallyBrush" 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"; "use strict";
window.HeightmapGenerator = (function () { window.HeightmapGenerator = (function () {
let cells, p; let grid = null;
let heights = null;
let blobPower;
let linePower;
const generate = async function () { const setGraph = graph => {
cells = grid.cells; const {cellsDesired, cells, points} = graph;
p = grid.points; heights = cells.h || createTypedArray({maxValue: 100, length: points.length});
cells.h = new Uint8Array(grid.points.length); blobPower = getBlobPower(cellsDesired);
linePower = getLinePower(cellsDesired);
grid = graph;
};
const input = document.getElementById("templateInput"); const getHeights = () => heights;
const selectedId = input.selectedIndex >= 0 ? input.selectedIndex : 0;
const type = input.options[selectedId]?.parentElement?.label;
if (type === "Specific") { const clearData = () => {
// pre-defined heightmap heights = null;
TIME && console.time("defineHeightmap"); grid = null;
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;
// load heightmap into image and render to canvas const fromTemplate = (graph, id) => {
const img = new Image(); const templateString = heightmapTemplates[id]?.template || "";
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 steps = templateString.split("\n"); 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) { for (const step of steps) {
const elements = step.trim().split(" "); 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); addStep(...elements);
} }
TIME && console.timeEnd("generateHeightmap"); return heights;
}; };
function addStep(a1, a2, a3, a4, a5) { const fromPrecreated = (graph, id) => {
if (a1 === "Hill") return addHill(a2, a3, a4, a5); return new Promise(resolve => {
if (a1 === "Pit") return addPit(a2, a3, a4, a5); // create canvas where 1px corresponts to a cell
if (a1 === "Range") return addRange(a2, a3, a4, a5); const canvas = document.createElement("canvas");
if (a1 === "Trough") return addTrough(a2, a3, a4, a5); const ctx = canvas.getContext("2d");
if (a1 === "Strait") return addStrait(a2, a3); const {cellsX, cellsY} = graph;
if (a1 === "Mask") return mask(a2); canvas.width = cellsX;
if (a1 === "Add") return modify(a3, +a2, 1); canvas.height = cellsY;
if (a1 === "Multiply") return modify(a3, 0, +a2);
if (a1 === "Smooth") return smooth(a2); // 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() { function getBlobPower(cells) {
const cells = +pointsInput.dataset.cells; const blobPowerMap = {
if (cells === 1000) return 0.93; 1000: 0.93,
if (cells === 2000) return 0.95; 2000: 0.95,
if (cells === 5000) return 0.96; 5000: 0.97,
if (cells === 10000) return 0.98; 10000: 0.98,
if (cells === 20000) return 0.985; 20000: 0.99,
if (cells === 30000) return 0.987; 30000: 0.991,
if (cells === 40000) return 0.9892; 40000: 0.993,
if (cells === 50000) return 0.9911; 50000: 0.994,
if (cells === 60000) return 0.9921; 60000: 0.995,
if (cells === 70000) return 0.9934; 70000: 0.9955,
if (cells === 80000) return 0.9942; 80000: 0.996,
if (cells === 90000) return 0.9946; 90000: 0.9964,
if (cells === 100000) return 0.995; 100000: 0.9973
};
return blobPowerMap[cells] || 0.98;
} }
function getLinePower() { function getLinePower() {
const cells = +pointsInput.dataset.cells; const linePowerMap = {
if (cells === 1000) return 0.74; 1000: 0.75,
if (cells === 2000) return 0.75; 2000: 0.77,
if (cells === 5000) return 0.78; 5000: 0.79,
if (cells === 10000) return 0.81; 10000: 0.81,
if (cells === 20000) return 0.82; 20000: 0.82,
if (cells === 30000) return 0.83; 30000: 0.83,
if (cells === 40000) return 0.84; 40000: 0.84,
if (cells === 50000) return 0.855; 50000: 0.86,
if (cells === 60000) return 0.87; 60000: 0.87,
if (cells === 70000) return 0.885; 70000: 0.88,
if (cells === 80000) return 0.91; 80000: 0.91,
if (cells === 90000) return 0.92; 90000: 0.92,
if (cells === 100000) return 0.93; 100000: 0.93
};
return linePowerMap[cells] || 0.81;
} }
const addHill = (count, height, rangeX, rangeY) => { const addHill = (count, height, rangeX, rangeY) => {
count = getNumberInRange(count); count = getNumberInRange(count);
const power = getBlobPower();
while (count > 0) { while (count > 0) {
addOneHill(); addOneHill();
count--; count--;
} }
function addOneHill() { function addOneHill() {
const change = new Uint8Array(cells.h.length); const change = new Uint8Array(heights.length);
let limit = 0; let limit = 0;
let start; let start;
let h = lim(getNumberInRange(height)); let h = lim(getNumberInRange(height));
@ -118,23 +142,23 @@ window.HeightmapGenerator = (function () {
do { do {
const x = getPointInRange(rangeX, graphWidth); const x = getPointInRange(rangeX, graphWidth);
const y = getPointInRange(rangeY, graphHeight); const y = getPointInRange(rangeY, graphHeight);
start = findGridCell(x, y); start = findGridCell(x, y, grid);
limit++; limit++;
} while (cells.h[start] + h > 90 && limit < 50); } while (heights[start] + h > 90 && limit < 50);
change[start] = h; change[start] = h;
const queue = [start]; const queue = [start];
while (queue.length) { while (queue.length) {
const q = queue.shift(); const q = queue.shift();
for (const c of cells.c[q]) { for (const c of grid.cells.c[q]) {
if (change[c]) continue; 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); 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() { function addOnePit() {
const used = new Uint8Array(cells.h.length); const used = new Uint8Array(heights.length);
let limit = 0, let limit = 0,
start; start;
let h = lim(getNumberInRange(height)); let h = lim(getNumberInRange(height));
@ -154,19 +178,19 @@ window.HeightmapGenerator = (function () {
do { do {
const x = getPointInRange(rangeX, graphWidth); const x = getPointInRange(rangeX, graphWidth);
const y = getPointInRange(rangeY, graphHeight); const y = getPointInRange(rangeY, graphHeight);
start = findGridCell(x, y); start = findGridCell(x, y, grid);
limit++; limit++;
} while (cells.h[start] < 20 && limit < 50); } while (heights[start] < 20 && limit < 50);
const queue = [start]; const queue = [start];
while (queue.length) { while (queue.length) {
const q = queue.shift(); 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; if (h < 1) return;
cells.c[q].forEach(function (c, i) { grid.cells.c[q].forEach(function (c, i) {
if (used[c]) return; 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; used[c] = 1;
queue.push(c); queue.push(c);
}); });
@ -176,14 +200,13 @@ window.HeightmapGenerator = (function () {
const addRange = (count, height, rangeX, rangeY) => { const addRange = (count, height, rangeX, rangeY) => {
count = getNumberInRange(count); count = getNumberInRange(count);
const power = getLinePower();
while (count > 0) { while (count > 0) {
addOneRange(); addOneRange();
count--; count--;
} }
function addOneRange() { function addOneRange() {
const used = new Uint8Array(cells.h.length); const used = new Uint8Array(heights.length);
let h = lim(getNumberInRange(height)); let h = lim(getNumberInRange(height));
// find start and end points // find start and end points
@ -201,16 +224,19 @@ window.HeightmapGenerator = (function () {
limit++; limit++;
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50); } 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 // get main ridge
function getRange(cur, end) { function getRange(cur, end) {
const range = [cur]; const range = [cur];
const p = grid.points;
used[cur] = 1; used[cur] = 1;
while (cur !== end) { while (cur !== end) {
let min = Infinity; let min = Infinity;
cells.c[cur].forEach(function (e) { grid.cells.c[cur].forEach(function (e) {
if (used[e]) return; if (used[e]) return;
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2; let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
if (Math.random() > 0.85) diff = diff / 2; if (Math.random() > 0.85) diff = diff / 2;
@ -234,12 +260,12 @@ window.HeightmapGenerator = (function () {
const frontier = queue.slice(); const frontier = queue.slice();
(queue = []), i++; (queue = []), i++;
frontier.forEach(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; if (h < 2) break;
frontier.forEach(f => { frontier.forEach(f => {
cells.c[f].forEach(i => { grid.cells.c[f].forEach(i => {
if (!used[i]) { if (!used[i]) {
queue.push(i); queue.push(i);
used[i] = 1; used[i] = 1;
@ -252,8 +278,8 @@ window.HeightmapGenerator = (function () {
range.forEach((cur, d) => { range.forEach((cur, d) => {
if (d % 6 !== 0) return; if (d % 6 !== 0) return;
for (const l of d3.range(i)) { for (const l of d3.range(i)) {
const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3; heights[min] = (heights[cur] * 2 + heights[min]) / 3;
cur = min; cur = min;
} }
}); });
@ -262,14 +288,13 @@ window.HeightmapGenerator = (function () {
const addTrough = (count, height, rangeX, rangeY) => { const addTrough = (count, height, rangeX, rangeY) => {
count = getNumberInRange(count); count = getNumberInRange(count);
const power = getLinePower();
while (count > 0) { while (count > 0) {
addOneTrough(); addOneTrough();
count--; count--;
} }
function addOneTrough() { function addOneTrough() {
const used = new Uint8Array(cells.h.length); const used = new Uint8Array(heights.length);
let h = lim(getNumberInRange(height)); let h = lim(getNumberInRange(height));
// find start and end points // find start and end points
@ -283,9 +308,9 @@ window.HeightmapGenerator = (function () {
do { do {
startX = getPointInRange(rangeX, graphWidth); startX = getPointInRange(rangeX, graphWidth);
startY = getPointInRange(rangeY, graphHeight); startY = getPointInRange(rangeY, graphHeight);
start = findGridCell(startX, startY); start = findGridCell(startX, startY, grid);
limit++; limit++;
} while (cells.h[start] < 20 && limit < 50); } while (heights[start] < 20 && limit < 50);
limit = 0; limit = 0;
do { do {
@ -295,16 +320,17 @@ window.HeightmapGenerator = (function () {
limit++; limit++;
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50); } 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 // get main ridge
function getRange(cur, end) { function getRange(cur, end) {
const range = [cur]; const range = [cur];
const p = grid.points;
used[cur] = 1; used[cur] = 1;
while (cur !== end) { while (cur !== end) {
let min = Infinity; let min = Infinity;
cells.c[cur].forEach(function (e) { grid.cells.c[cur].forEach(function (e) {
if (used[e]) return; if (used[e]) return;
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2; 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 (Math.random() > 0.8) diff = diff / 2;
@ -328,12 +354,12 @@ window.HeightmapGenerator = (function () {
const frontier = queue.slice(); const frontier = queue.slice();
(queue = []), i++; (queue = []), i++;
frontier.forEach(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; if (h < 2) break;
frontier.forEach(f => { frontier.forEach(f => {
cells.c[f].forEach(i => { grid.cells.c[f].forEach(i => {
if (!used[i]) { if (!used[i]) {
queue.push(i); queue.push(i);
used[i] = 1; used[i] = 1;
@ -346,9 +372,9 @@ window.HeightmapGenerator = (function () {
range.forEach((cur, d) => { range.forEach((cur, d) => {
if (d % 6 !== 0) return; if (d % 6 !== 0) return;
for (const l of d3.range(i)) { for (const l of d3.range(i)) {
const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell const min = 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); //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; cur = min;
} }
}); });
@ -358,24 +384,25 @@ window.HeightmapGenerator = (function () {
const addStrait = (width, direction = "vertical") => { const addStrait = (width, direction = "vertical") => {
width = Math.min(getNumberInRange(width), grid.cellsX / 3); width = Math.min(getNumberInRange(width), grid.cellsX / 3);
if (width < 1 && P(width)) return; if (width < 1 && P(width)) return;
const used = new Uint8Array(cells.h.length); const used = new Uint8Array(heights.length);
const vert = direction === "vertical"; const vert = direction === "vertical";
const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5; 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 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 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 endY = vert ? graphHeight - 5 : Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2);
const start = findGridCell(startX, startY); const start = findGridCell(startX, startY, grid);
const end = findGridCell(endX, endY); const end = findGridCell(endX, endY, grid);
let range = getRange(start, end); let range = getRange(start, end);
const query = []; const query = [];
function getRange(cur, end) { function getRange(cur, end) {
const range = []; const range = [];
const p = grid.points;
while (cur !== end) { while (cur !== end) {
let min = Infinity; 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; 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 (Math.random() > 0.8) diff = diff / 2;
if (diff < min) { if (diff < min) {
@ -394,12 +421,12 @@ window.HeightmapGenerator = (function () {
while (width > 0) { while (width > 0) {
const exp = 0.9 - step * width; const exp = 0.9 - step * width;
range.forEach(function (r) { range.forEach(function (r) {
cells.c[r].forEach(function (e) { grid.cells.c[r].forEach(function (e) {
if (used[e]) return; if (used[e]) return;
used[e] = 1; used[e] = 1;
query.push(e); query.push(e);
cells.h[e] **= exp; heights[e] **= exp;
if (cells.h[e] > 100) cells.h[e] = 5; if (heights[e] > 100) heights[e] = 5;
}); });
}); });
range = query.slice(); range = query.slice();
@ -413,7 +440,7 @@ window.HeightmapGenerator = (function () {
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1]; const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
const isLand = min === 20; const isLand = min === 20;
grid.cells.h = grid.cells.h.map(h => { heights = heights.map(h => {
if (h < min || h > max) return h; if (h < min || h > max) return h;
if (add) h = isLand ? Math.max(h + add, 20) : h + add; if (add) h = isLand ? Math.max(h + add, 20) : h + add;
@ -424,9 +451,9 @@ window.HeightmapGenerator = (function () {
}; };
const smooth = (fr = 2, add = 0) => { const smooth = (fr = 2, add = 0) => {
cells.h = cells.h.map((h, i) => { heights = heights.map((h, i) => {
const a = [h]; 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; if (fr === 1) return d3.mean(a) + add;
return lim((h * (fr - 1) + d3.mean(a) + add) / fr); return lim((h * (fr - 1) + d3.mean(a) + add) / fr);
}); });
@ -435,8 +462,8 @@ window.HeightmapGenerator = (function () {
const mask = (power = 1) => { const mask = (power = 1) => {
const fr = power ? Math.abs(power) : 1; const fr = power ? Math.abs(power) : 1;
cells.h = cells.h.map((h, i) => { heights = heights.map((h, i) => {
const [x, y] = p[i]; const [x, y] = grid.points[i];
const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center
const ny = (2 * y) / graphHeight - 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 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 invertY = axes !== "x";
const {cellsX, cellsY} = grid; const {cellsX, cellsY} = grid;
const inverted = cells.h.map((h, i) => { const inverted = heights.map((h, i) => {
const x = i % cellsX; const x = i % cellsX;
const y = Math.floor(i / cellsX); const y = Math.floor(i / cellsX);
const nx = invertX ? cellsX - x - 1 : x; const nx = invertX ? cellsX - x - 1 : x;
const ny = invertY ? cellsY - y - 1 : y; const ny = invertY ? cellsY - y - 1 : y;
const invertedI = nx + ny * cellsX; const invertedI = nx + ny * cellsX;
return cells.h[invertedI]; return heights[invertedI];
}); });
cells.h = inverted; heights = inverted;
}; };
function getPointInRange(range, length) { function getPointInRange(range, length) {
@ -477,13 +504,28 @@ window.HeightmapGenerator = (function () {
return rand(min * length, max * length); return rand(min * length, max * length);
} }
function assignColorsToHeight(imageData) { function getHeightsFromImageData(imageData) {
for (let i = 0; i < cells.i.length; i++) { for (let i = 0; i < heights.length; i++) {
const lightness = imageData[i * 4] / 255; const lightness = imageData[i * 4] / 255;
const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8; 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) { function updateMeshCells(clone) {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20); 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").attr("filter", "url(#blur1)");
clone clone
.select("#heights") .select("#heights")

View file

@ -324,7 +324,11 @@ async function parseLoadedData(data) {
void (function parseGridData() { void (function parseGridData() {
grid = JSON.parse(data[6]); 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.h = Uint8Array.from(data[7].split(","));
grid.cells.prec = Uint8Array.from(data[8].split(",")); grid.cells.prec = Uint8Array.from(data[8].split(","));
grid.cells.f = Uint16Array.from(data[9].split(",")); grid.cells.f = Uint16Array.from(data[9].split(","));
@ -333,7 +337,6 @@ async function parseLoadedData(data) {
})(); })();
void (function parsePackData() { void (function parsePackData() {
pack = {};
reGraph(); reGraph();
reMarkFeatures(); reMarkFeatures();
pack.features = JSON.parse(data[12]); pack.features = JSON.parse(data[12]);
@ -424,7 +427,7 @@ async function parseLoadedData(data) {
{ {
// dynamically import and run auto-udpdate script // dynamically import and run auto-udpdate script
const versionNumber = parseFloat(params[0]); 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); resolveVersionConflicts(versionNumber);
} }

View file

@ -54,8 +54,8 @@ function getMapData() {
const serializedSVG = new XMLSerializer().serializeToString(cloneEl); const serializedSVG = new XMLSerializer().serializeToString(cloneEl);
const {spacing, cellsX, cellsY, boundary, points, features} = grid; const {spacing, cellsX, cellsY, boundary, points, features, cellsDesired} = grid;
const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features}); const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features, cellsDesired});
const packFeatures = JSON.stringify(pack.features); const packFeatures = JSON.stringify(pack.features);
const cultures = JSON.stringify(pack.cultures); const cultures = JSON.stringify(pack.cultures);
const states = JSON.stringify(pack.states); const states = JSON.stringify(pack.states);

View file

@ -41,8 +41,7 @@ window.Submap = (function () {
// create new grid // create new grid
applyMapSize(); applyMapSize();
placePoints(); grid = generateGrid();
calculateVoronoi(grid, grid.points);
drawScaleBar(scale); drawScaleBar(scale);
const resampler = (points, qtree, f) => { const resampler = (points, qtree, f) => {

View file

@ -1119,12 +1119,12 @@ function refreshAllEditors() {
// dynamically loaded editors // dynamically loaded editors
async function editStates() { async function editStates() {
if (customization) return; 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(); StateEditor.open();
} }
async function editCultures() { async function editCultures() {
if (customization) return; 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(); CulturesEditor.open();
} }

View file

@ -109,7 +109,8 @@ function showElevationProfile(data, routeLen, isRiver) {
draw(); draw();
function downloadCSV() { 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++) { for (let k = 0; k < chartData.points.length; k++) {
let cell = chartData.cell[k]; let cell = chartData.cell[k];
@ -179,9 +180,20 @@ function showElevationProfile(data, routeLen, isRiver) {
.attr("id", "elevationSVG") .attr("id", "elevationSVG")
.attr("class", "epbackground"); .attr("class", "epbackground");
// arrow-head definition // 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%"); 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) { if (chartData.mah == chartData.mih) {
@ -258,7 +270,24 @@ function showElevationProfile(data, routeLen, isRiver) {
const populationDesc = rn(pop * populationRate); const populationDesc = rn(pop * populationRate);
const provinceDesc = province ? ", " + pack.provinces[province].name : ""; 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); 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 // fit full-screen map if window is resized
window.addEventListener("resize", function (e) { window.addEventListener("resize", function (e) {
if (localStorage.getItem("mapWidth") && localStorage.getItem("mapHeight")) return; if (stored("mapWidth") && stored("mapHeight")) return;
mapWidthInput.value = window.innerWidth; mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight; mapHeightInput.value = window.innerHeight;
changeMapSize(); changeMapSize();
@ -21,18 +21,22 @@ document.getElementById("dialogs").addEventListener("mousemove", showDataTip);
document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip); document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip);
document.getElementById("exitCustomization").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.innerHTML = tip;
tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)"; tooltip.style.background = tipBackgroundMap[type];
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)";
if (main) { if (main) {
tooltip.dataset.main = tip; tooltip.dataset.main = tip;
tooltip.dataset.color = tooltip.style.background; tooltip.dataset.color = tooltip.style.background;
} }
if (time) setTimeout(() => clearMainTip(), time); if (time) setTimeout(clearMainTip, time);
} }
function showMainTip() { function showMainTip() {
@ -47,11 +51,16 @@ function clearMainTip() {
} }
// show tip at the bottom of the screen, consider possible translation // show tip at the bottom of the screen, consider possible translation
function showDataTip(e) { function showDataTip(event) {
if (!e.target) return; if (!event.target) return;
let dataTip = e.target.dataset.tip;
if (!dataTip && e.target.parentNode.dataset.tip) dataTip = e.target.parentNode.dataset.tip; let dataTip = event.target.dataset.tip;
if (!dataTip && event.target.parentNode.dataset.tip) dataTip = event.target.parentNode.dataset.tip;
if (!dataTip) return; 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); //const tooltip = lang === "en" ? dataTip : translate(e.target.dataset.t || e.target.parentNode.dataset.t, dataTip);
tip(dataTip); tip(dataTip);
} }
@ -72,10 +81,10 @@ function handleMouseMove() {
if (i === undefined) return; if (i === undefined) return;
showNotes(d3.event); 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(); if (tooltip.dataset.main) showMainTip();
else showMapTooltip(point, d3.event, i, g); else showMapTooltip(point, d3.event, i, gridCell);
if (cellInfo?.offsetParent) updateCellInfo(point, i, g); if (cellInfo?.offsetParent) updateCellInfo(point, i, gridCell);
} }
// show note box on hover (if any) // show note box on hover (if any)
@ -235,7 +244,7 @@ function updateCellInfo(point, i, g) {
infoCell.innerHTML = i; infoCell.innerHTML = i;
infoArea.innerHTML = cells.area[i] ? si(getArea(cells.area[i])) + " " + getAreaUnit() : "n/a"; infoArea.innerHTML = cells.area[i] ? si(getArea(cells.area[i])) + " " + getAreaUnit() : "n/a";
infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]); 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]); infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a"; infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : "no"; 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 // get water depth
function getDepth(f, h, p) { function getDepth(f, p) {
if (f.land) return "0 " + heightUnit.value; // land: 0 if (f.land) return "0 " + heightUnit.value; // land: 0
// lake: difference between surface and bottom // 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") { if (f.type === "lake") {
const depth = gridH === 19 ? f.height / 2 : gridH; const depth = gridH === 19 ? f.height / 2 : gridH;
return getHeight(depth, "abs"); return getHeight(depth, "abs");
@ -281,9 +290,9 @@ function getDepth(f, h, p) {
} }
// get user-friendly (real-world) height value from map data // get user-friendly (real-world) height value from map data
function getFriendlyHeight(p) { function getFriendlyHeight([x, y]) {
const packH = pack.cells.h[findCell(p[0], p[1])]; const packH = pack.cells.h[findCell(x, y, grid)];
const gridH = grid.cells.h[findGridCell(p[0], p[1])]; const gridH = grid.cells.h[findGridCell(x, y, grid)];
const h = packH < 20 ? gridH : packH; const h = packH < 20 ? gridH : packH;
return getHeight(h); return getHeight(h);
} }
@ -405,7 +414,7 @@ document.querySelectorAll("[data-locked]").forEach(function (e) {
// lock option // lock option
function lock(id) { function lock(id) {
const input = document.querySelector('[data-stored="' + 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); const el = document.getElementById("lock_" + id);
if (!el) return; if (!el) return;
el.dataset.locked = 1; el.dataset.locked = 1;
@ -427,9 +436,14 @@ function locked(id) {
return lockEl.dataset.locked == 1; return lockEl.dataset.locked == 1;
} }
// check if option is stored in localStorage // return key value stored in localStorage or null
function stored(option) { function stored(key) {
return localStorage.getItem(option); return localStorage.getItem(key) || null;
}
// store key value in localStorage
function store(key, value) {
return localStorage.setItem(key, value);
} }
// assign skeaker behaviour // assign skeaker behaviour
@ -449,10 +463,10 @@ function speak(text) {
} }
// apply drop-down menu option. If the value is not in options, add it // apply drop-down menu option. If the value is not in options, add it
function applyOption(select, id, name = id) { function applyOption($select, value, name = value) {
const custom = !Array.from(select.options).some(o => o.value == id); const isExisting = Array.from($select.options).some(o => o.value === value);
if (custom) select.options.add(new Option(name, id)); if (!isExisting) $select.options.add(new Option(name, value));
select.value = id; $select.value = value;
} }
// show info about the generator in a popup // show info about the generator in a popup

View file

@ -1,35 +1,13 @@
"use strict"; "use strict";
function editHeightmap() { function editHeightmap(options) {
void (function selectEditMode() { const {mode, tool} = options || {};
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");
}
}
});
})();
restartHistory(); restartHistory();
viewbox.insert("g", "#terrs").attr("id", "heights"); viewbox.insert("g", "#terrs").attr("id", "heights");
if (!mode) showModeDialog();
else enterHeightmapEditMode(mode);
if (modules.editHeightmap) return; if (modules.editHeightmap) return;
modules.editHeightmap = true; modules.editHeightmap = true;
@ -44,35 +22,66 @@ function editHeightmap() {
byId("templateUndo").on("click", () => restoreHistory(edits.n - 1)); byId("templateUndo").on("click", () => restoreHistory(edits.n - 1));
byId("templateRedo").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 = 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 editHeightmap.layers.forEach(l => byId(l).click()); // turn off all layers
customization = 1; customization = 1;
closeDialogs(); closeDialogs();
tip('Heightmap edit mode is active. Click on "Exit Customization" to finalize the heightmap', true); 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(); undraw();
changeOnlyLand.checked = false; changeOnlyLand.checked = false;
} else if (type === "keep") { } else if (mode === "keep") {
viewbox.selectAll("#landmass, #lakes").style("display", "none"); viewbox.selectAll("#landmass, #lakes").style("display", "none");
changeOnlyLand.checked = true; changeOnlyLand.checked = true;
} else if (type === "risk") { } else if (mode === "risk") {
defs.selectAll("#land, #water").selectAll("path").remove(); defs.selectAll("#land, #water").selectAll("path").remove();
viewbox.selectAll("#coastline path, #lakes path, #oceanLayers path").remove(); viewbox.selectAll("#coastline path, #lakes path, #oceanLayers path").remove();
changeOnlyLand.checked = false; changeOnlyLand.checked = false;
} }
// show convert and template buttons for Erase mode only // show convert and template buttons for Erase mode only
applyTemplate.style.display = type === "erase" ? "inline-block" : "none"; applyTemplate.style.display = mode === "erase" ? "inline-block" : "none";
convertImage.style.display = type === "erase" ? "inline-block" : "none"; convertImage.style.display = mode === "erase" ? "inline-block" : "none";
// hide erosion checkbox if mode is Keep // 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 // show finalize button
if (!sessionStorage.getItem("noExitButtonAnimation")) { if (!sessionStorage.getItem("noExitButtonAnimation")) {
@ -95,27 +104,30 @@ function editHeightmap() {
.style("transform", "scale(1)"); .style("transform", "scale(1)");
} else exitCustomization.style.display = "block"; } else exitCustomization.style.display = "block";
openBrushesPanel();
turnButtonOn("toggleHeight"); turnButtonOn("toggleHeight");
layersPreset.value = "heightmap"; layersPreset.value = "heightmap";
layersPreset.disabled = true; layersPreset.disabled = true;
mockHeightmap(); mockHeightmap();
viewbox.on("touchmove mousemove", moveCursor); viewbox.on("touchmove mousemove", moveCursor);
if (tool === "templateEditor") openTemplateEditor();
else if (tool === "imageConverter") openImageConverter();
else openBrushesPanel();
} }
function moveCursor() { function moveCursor() {
const p = d3.mouse(this), const [x, y] = d3.mouse(this);
cell = findGridCell(p[0], p[1]); const cell = findGridCell(x, y, grid);
heightmapInfoX.innerHTML = rn(p[0]); heightmapInfoX.innerHTML = rn(x);
heightmapInfoY.innerHTML = rn(p[1]); heightmapInfoY.innerHTML = rn(y);
heightmapInfoCell.innerHTML = cell; 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(); if (tooltip.dataset.main) showMainTip();
// move radius circle if drag mode is active // move radius circle if drag mode is active
const pressed = byId("brushesButtons").querySelector("button.pressed"); const pressed = byId("brushesButtons").querySelector("button.pressed");
if (!pressed) return; 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 // get user-friendly (real-world) height value from map data
@ -593,8 +605,8 @@ function editHeightmap() {
function dragBrush() { function dragBrush() {
const r = brushRadius.valueAsNumber; const r = brushRadius.valueAsNumber;
const point = d3.mouse(this); const [x, y] = d3.mouse(this);
const start = findGridCell(point[0], point[1]); const start = findGridCell(x, y, grid);
d3.event.on("drag", () => { d3.event.on("drag", () => {
const p = d3.mouse(this); 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 (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"); 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); if (operator === "multiply") HeightmapGenerator.modify(range, 0, operand, 0);
else if (operator === "divide") HeightmapGenerator.modify(range, 0, 1 / 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 === "add") HeightmapGenerator.modify(range, operand, 1, 0);
else if (operator === "subtract") HeightmapGenerator.modify(range, -1 * operand, 1, 0); else if (operator === "subtract") HeightmapGenerator.modify(range, -1 * operand, 1, 0);
else if (operator === "exponent") HeightmapGenerator.modify(range, 0, 1, operand); else if (operator === "exponent") HeightmapGenerator.modify(range, 0, 1, operand);
grid.cells.h = HeightmapGenerator.getHeights();
updateHeightmap(); updateHeightmap();
} }
function smoothAllHeights() { function smoothAllHeights() {
HeightmapGenerator.setGraph(grid);
HeightmapGenerator.smooth(4, 1.5); HeightmapGenerator.smooth(4, 1.5);
grid.cells.h = HeightmapGenerator.getHeights();
updateHeightmap(); updateHeightmap();
} }
@ -879,10 +896,7 @@ function editHeightmap() {
const steps = body.querySelectorAll("div").length; const steps = body.querySelectorAll("div").length;
const changed = +body.getAttribute("data-changed"); const changed = +body.getAttribute("data-changed");
const template = e.target.value; const template = e.target.value;
if (!steps || !changed) { if (!steps || !changed) return changeTemplate(template);
changeTemplate(template);
return;
}
alertMessage.innerHTML = "Are you sure you want to select a different template? All changes will be lost."; alertMessage.innerHTML = "Are you sure you want to select a different template? All changes will be lost.";
$("#alert").dialog({ $("#alert").dialog({
@ -905,7 +919,7 @@ function editHeightmap() {
body.setAttribute("data-changed", 0); body.setAttribute("data-changed", 0);
body.innerHTML = ""; body.innerHTML = "";
const templateString = HeightmapTemplates[template]; const templateString = heightmapTemplates[template]?.template;
if (!templateString) return; if (!templateString) return;
const steps = templateString.split("\n"); const steps = templateString.split("\n");
@ -921,10 +935,11 @@ function editHeightmap() {
const steps = byId("templateBody").querySelectorAll("#templateBody > div"); const steps = byId("templateBody").querySelectorAll("#templateBody > div");
if (!steps.length) return; if (!steps.length) return;
grid.cells.h = new Uint8Array(grid.cells.i.length); // clean all heights
const seed = byId("templateSeed").value; const seed = byId("templateSeed").value;
if (seed) Math.random = aleaPRNG(seed); if (seed) Math.random = aleaPRNG(seed);
grid.cells.h = createTypedArray({maxValue: 100, length: grid.points.length});
HeightmapGenerator.setGraph(grid);
restartHistory(); restartHistory();
for (const step of steps) { for (const step of steps) {
@ -948,9 +963,11 @@ function editHeightmap() {
else if (type === "Multiply") HeightmapGenerator.modify(dist, 0, +count); else if (type === "Multiply") HeightmapGenerator.modify(dist, 0, +count);
else if (type === "Smooth") HeightmapGenerator.smooth(+count); else if (type === "Smooth") HeightmapGenerator.smooth(+count);
grid.cells.h = HeightmapGenerator.getHeights();
updateHistory("noStat"); // update history on every step updateHistory("noStat"); // update history on every step
} }
grid.cells.h = HeightmapGenerator.getHeights();
updateStatistics(); updateStatistics();
mockHeightmap(); mockHeightmap();
if (byId("preview")) drawHeightmapPreview(); // update heightmap preview if opened if (byId("preview")) drawHeightmapPreview(); // update heightmap preview if opened
@ -1360,12 +1377,14 @@ function editHeightmap() {
const imageData = ctx.createImageData(grid.cellsX, grid.cellsY); const imageData = ctx.createImageData(grid.cellsX, grid.cellsY);
grid.cells.h.forEach((height, i) => { 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; const v = (h / 100) * 255;
imageData.data[i * 4] = v;
imageData.data[i * 4 + 1] = v; const n = i * 4;
imageData.data[i * 4 + 2] = v; imageData.data[n] = v;
imageData.data[i * 4 + 3] = 255; imageData.data[n + 1] = v;
imageData.data[n + 2] = v;
imageData.data[n + 3] = 255;
}); });
ctx.putImageData(imageData, 0, 0); ctx.putImageData(imageData, 0, 0);

View file

@ -25,7 +25,7 @@ function handleKeyup(event) {
const alt = altKey || key === "Alt"; const alt = altKey || key === "Alt";
if (code === "F1") showInfo(); if (code === "F1") showInfo();
else if (code === "F2") regeneratePrompt("hotkey"); else if (code === "F2") regeneratePrompt();
else if (code === "F6") quickSave(); else if (code === "F6") quickSave();
else if (code === "F9") quickLoad(); else if (code === "F9") quickLoad();
else if (code === "Tab") toggleOptions(event); else if (code === "Tab") toggleOptions(event);

View file

@ -65,8 +65,8 @@ function editIce() {
} }
function addIcebergOnClick() { function addIcebergOnClick() {
const point = d3.mouse(this); const [x, y] = d3.mouse(this);
const i = findGridCell(point[0], point[1]); const i = findGridCell(x, y, grid);
const c = grid.points[i]; const c = grid.points[i];
const s = +document.getElementById("iceSize").value; const s = +document.getElementById("iceSize").value;

View file

@ -151,16 +151,17 @@ function toggleHeight(event) {
function drawHeightmap() { function drawHeightmap() {
TIME && console.time("drawHeightmap"); TIME && console.time("drawHeightmap");
terrs.selectAll("*").remove(); terrs.selectAll("*").remove();
const cells = pack.cells,
vertices = pack.vertices, const {cells, vertices} = pack;
n = cells.i.length; const n = cells.i.length;
const used = new Uint8Array(cells.i.length); const used = new Uint8Array(cells.i.length);
const paths = new Array(101).fill(""); 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 terracing = terrs.attr("terracing") / 10; // add additional shifted darker layer for pseudo-3d effect
const skip = +terrs.attr("skip") + 1; const skip = +terrs.attr("skip") + 1;
const simplification = +terrs.attr("relax"); const simplification = +terrs.attr("relax");
switch (+terrs.attr("curve")) { switch (+terrs.attr("curve")) {
case 0: case 0:
lineGen.curve(d3.curveBasisClosed); lineGen.curve(d3.curveBasisClosed);
@ -233,8 +234,7 @@ function drawHeightmap() {
TIME && console.timeEnd("drawHeightmap"); TIME && console.timeEnd("drawHeightmap");
} }
function getColorScheme() { function getColorScheme(scheme) {
const scheme = terrs.attr("scheme");
if (scheme === "bright") return d3.scaleSequential(d3.interpolateSpectral); if (scheme === "bright") return d3.scaleSequential(d3.interpolateSpectral);
if (scheme === "light") return d3.scaleSequential(d3.interpolateRdYlGn); if (scheme === "light") return d3.scaleSequential(d3.interpolateRdYlGn);
if (scheme === "green") return d3.scaleSequential(d3.interpolateGreens); if (scheme === "green") return d3.scaleSequential(d3.interpolateGreens);

View file

@ -47,6 +47,9 @@ function overviewMilitary() {
// update military types in header and tooltips // update military types in header and tooltips
function updateHeaders() { function updateHeaders() {
const header = document.getElementById("militaryHeader"); 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()); header.querySelectorAll(".removable").forEach(el => el.remove());
const insert = html => document.getElementById("militaryTotal").insertAdjacentHTML("beforebegin", html); const insert = html => document.getElementById("militaryTotal").insertAdjacentHTML("beforebegin", html);
for (const u of options.military) { for (const u of options.military) {

View file

@ -42,7 +42,7 @@ function editNotes(id, name) {
$("#notesEditor").dialog({ $("#notesEditor").dialog({
title: "Notes Editor", title: "Notes Editor",
width: "70vw", width: "minmax(80vw, 540px)",
height: window.innerHeight * 0.75, height: window.innerHeight * 0.75,
position: {my: "center", at: "center", of: "svg"}, position: {my: "center", at: "center", of: "svg"},
close: removeEditor close: removeEditor

View file

@ -6,14 +6,14 @@ $("#exitCustomization").draggable({handle: "div"});
$("#mapLayers").disableSelection(); $("#mapLayers").disableSelection();
// remove glow if tip is aknowledged // remove glow if tip is aknowledged
if (localStorage.getItem("disable_click_arrow_tooltip")) { if (stored("disable_click_arrow_tooltip")) {
clearMainTip(); clearMainTip();
optionsTrigger.classList.remove("glow"); optionsTrigger.classList.remove("glow");
} }
// Show options pane on trigger click // Show options pane on trigger click
function showOptions(event) { function showOptions(event) {
if (!localStorage.getItem("disable_click_arrow_tooltip")) { if (!stored("disable_click_arrow_tooltip")) {
clearMainTip(); clearMainTip();
localStorage.setItem("disable_click_arrow_tooltip", true); localStorage.setItem("disable_click_arrow_tooltip", true);
optionsTrigger.classList.remove("glow"); optionsTrigger.classList.remove("glow");
@ -81,12 +81,12 @@ async function showSupporters() {
} }
// on any option or dialog change // on any option or dialog change
document.getElementById("options").addEventListener("change", checkIfStored); document.getElementById("options").addEventListener("change", storeValueIfRequired);
document.getElementById("dialogs").addEventListener("change", checkIfStored); document.getElementById("dialogs").addEventListener("change", storeValueIfRequired);
document.getElementById("options").addEventListener("input", updateOutputToFollowInput); document.getElementById("options").addEventListener("input", updateOutputToFollowInput);
document.getElementById("dialogs").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); 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 === "optionsMapHistory") showSeedHistoryDialog();
else if (id === "optionsCopySeed") copyMapURL(); else if (id === "optionsCopySeed") copyMapURL();
else if (id === "optionsEraRegenerate") regenerateEra(); else if (id === "optionsEraRegenerate") regenerateEra();
else if (id === "templateInputContainer") openTemplateSelectionDialog();
else if (id === "zoomExtentDefault") restoreDefaultZoomExtent(); else if (id === "zoomExtentDefault") restoreDefaultZoomExtent();
else if (id === "translateExtent") toggleTranslateExtent(event.target); else if (id === "translateExtent") toggleTranslateExtent(event.target);
else if (id === "speakerTest") testSpeaker(); else if (id === "speakerTest") testSpeaker();
@ -232,7 +233,7 @@ const voiceInterval = setInterval(function () {
voices.forEach((voice, i) => { voices.forEach((voice, i) => {
select.options.add(new Option(voice.name, i, false)); 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 // se voice to store
else select.value = voices.findIndex(voice => voice.lang === "en-US"); // or to first found English-US else select.value = voices.findIndex(voice => voice.lang === "en-US"); // or to first found English-US
}, 1000); }, 1000);
@ -248,9 +249,9 @@ function testSpeaker() {
speechSynthesis.speak(speaker); speechSynthesis.speak(speaker);
} }
function generateMapWithSeed(source) { function generateMapWithSeed() {
if (optionsSeed.value == seed) return tip("The current map already has this seed", false, "error"); if (optionsSeed.value == seed) return tip("The current map already has this seed", false, "error");
regeneratePrompt(source); regeneratePrompt();
} }
function showSeedHistoryDialog() { function showSeedHistoryDialog() {
@ -272,14 +273,15 @@ function showSeedHistoryDialog() {
// generate map with historical seed // generate map with historical seed
function restoreSeed(id) { 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"); if (locked("template")) unlock("template");
regeneratePrompt("seed history");
regeneratePrompt();
} }
function restoreDefaultZoomExtent() { function restoreDefaultZoomExtent() {
@ -456,43 +458,50 @@ function changeZoomExtent(value) {
zoom.scaleTo(svg, scale); zoom.scaleTo(svg, scale);
} }
// control stored options logic // restore options stored in localStorage
function applyStoredOptions() { function applyStoredOptions() {
if (!localStorage.getItem("mapWidth") || !localStorage.getItem("mapHeight")) { if (!stored("mapWidth") || !stored("mapHeight")) {
mapWidthInput.value = window.innerWidth; mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight; mapHeightInput.value = window.innerHeight;
} }
if (localStorage.getItem("distanceUnit")) applyOption(distanceUnitInput, localStorage.getItem("distanceUnit")); const heightmapId = stored("template");
if (localStorage.getItem("heightUnit")) applyOption(heightUnit, localStorage.getItem("heightUnit")); if (heightmapId) {
const name = heightmapTemplates[heightmapId]?.name || precreatedHeightmaps[heightmapId]?.name || heightmapId;
for (let i = 0; i < localStorage.length; i++) { applyOption(byId("templateInput"), heightmapId, name);
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));
} }
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 options.winds = localStorage
.getItem("winds") .getItem("winds")
.split(",") .split(",")
.map(w => +w); .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 (stored("tooltipSize")) changeTooltipSize(stored("tooltipSize"));
if (localStorage.getItem("regions")) changeStatesNumber(localStorage.getItem("regions")); if (stored("regions")) changeStatesNumber(stored("regions"));
uiSizeInput.max = uiSizeOutput.max = getUImaxSize(); 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)); else changeUIsize(minmax(rn(mapWidthInput.value / 1280, 1), 1, 2.5));
// search params overwrite stored and default options // search params overwrite stored and default options
@ -502,8 +511,8 @@ function applyStoredOptions() {
if (width) mapWidthInput.value = width; if (width) mapWidthInput.value = width;
if (height) mapHeightInput.value = height; if (height) mapHeightInput.value = height;
const transparency = localStorage.getItem("transparency") || 5; const transparency = stored("transparency") || 5;
const themeColor = localStorage.getItem("themeColor"); const themeColor = stored("themeColor");
changeDialogsTheme(themeColor, transparency); changeDialogsTheme(themeColor, transparency);
setRendering(shapeRendering.value); setRendering(shapeRendering.value);
@ -512,7 +521,6 @@ function applyStoredOptions() {
// randomize options if randomization is allowed (not locked or options='default') // randomize options if randomization is allowed (not locked or options='default')
function randomizeOptions() { 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 const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options
// 'Options' settings // 'Options' settings
@ -549,22 +557,13 @@ function randomizeOptions() {
// select heightmap template pseudo-randomly // select heightmap template pseudo-randomly
function randomizeHeightmapTemplate() { function randomizeHeightmapTemplate() {
const templates = { const templates = {};
volcano: 3, for (const key in heightmapTemplates) {
highIsland: 19, templates[key] = heightmapTemplates[key].probability || 0;
lowIsland: 9, }
continents: 16, const template = rw(templates);
archipelago: 18, const name = heightmapTemplates[template].name;
mediterranean: 5, applyOption(byId("templateInput"), template, name);
peninsula: 3,
pangea: 5,
isthmus: 2,
atoll: 1,
shattered: 7,
taklamakan: 1,
oldWorld: 11
};
document.getElementById("templateInput").value = rw(templates);
} }
// select culture set pseudo-randomly // select culture set pseudo-randomly
@ -623,6 +622,11 @@ function changeEra() {
options.era = eraInput.value; 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 // remove all saved data from LocalStorage and reload the page
function restoreDefaultOptions() { function restoreDefaultOptions() {
localStorage.clear(); localStorage.clear();
@ -632,17 +636,17 @@ function restoreDefaultOptions() {
// Sticked menu Options listeners // Sticked menu Options listeners
document.getElementById("sticked").addEventListener("click", function (event) { document.getElementById("sticked").addEventListener("click", function (event) {
const id = event.target.id; const id = event.target.id;
if (id === "newMapButton") regeneratePrompt("sticky button"); if (id === "newMapButton") regeneratePrompt();
else if (id === "saveButton") showSavePane(); else if (id === "saveButton") showSavePane();
else if (id === "exportButton") showExportPane(); else if (id === "exportButton") showExportPane();
else if (id === "loadButton") showLoadPane(); else if (id === "loadButton") showLoadPane();
else if (id === "zoomReset") resetZoom(1000); 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"); 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 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 /> 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`; All unsaved changes made to the current map will be lost`;
@ -655,7 +659,7 @@ function regeneratePrompt(source) {
}, },
Generate: function () { Generate: function () {
closeDialogs(); closeDialogs();
regenerateMap(source); regenerateMap(options);
} }
} }
}); });

View file

@ -30,6 +30,9 @@ function overviewRegiments(state) {
// update military types in header and tooltips // update military types in header and tooltips
function updateHeaders() { function updateHeaders() {
const header = document.getElementById("regimentsHeader"); 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()); header.querySelectorAll(".removable").forEach(el => el.remove());
const insert = html => document.getElementById("regimentsTotal").insertAdjacentHTML("beforebegin", html); const insert = html => document.getElementById("regimentsTotal").insertAdjacentHTML("beforebegin", html);
for (const u of options.military) { for (const u of options.military) {

View file

@ -2,7 +2,6 @@
// UI elements for submap generation // UI elements for submap generation
window.UISubmap = (function () { window.UISubmap = (function () {
const byId = document.getElementById.bind(document);
byId("submapPointsInput").addEventListener("input", function () { byId("submapPointsInput").addEventListener("input", function () {
const output = byId("submapPointsOutputFormatted"); const output = byId("submapPointsOutputFormatted");
const cells = cellsDensityMap[+this.value] || 1000; const cells = cellsDensityMap[+this.value] || 1000;
@ -13,7 +12,7 @@ window.UISubmap = (function () {
byId("submapScaleInput").addEventListener("input", function (event) { byId("submapScaleInput").addEventListener("input", function (event) {
const exp = Math.pow(1.1, +event.target.value); 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) { byId("submapAngleInput").addEventListener("input", function (event) {
@ -28,7 +27,6 @@ window.UISubmap = (function () {
function openSubmapMenu() { function openSubmapMenu() {
$("#submapOptionsDialog").dialog({ $("#submapOptionsDialog").dialog({
title: "Create a submap", title: "Create a submap",
width: "30em",
resizable: false, resizable: false,
position: {my: "center", at: "center", of: "svg"}, position: {my: "center", at: "center", of: "svg"},
buttons: { buttons: {
@ -49,8 +47,8 @@ window.UISubmap = (function () {
shiftY: +byId("submapShiftY").value, shiftY: +byId("submapShiftY").value,
ratio: +byId("submapScaleInput").value, ratio: +byId("submapScaleInput").value,
mirrorH: byId("submapMirrorH").checked, mirrorH: byId("submapMirrorH").checked,
mirrorV: byId("submapMirrorV").checked, mirrorV: byId("submapMirrorV").checked
}) });
async function openResampleMenu() { async function openResampleMenu() {
resetZoom(0); resetZoom(0);
@ -64,14 +62,14 @@ window.UISubmap = (function () {
$shiftX.value = 0; $shiftX.value = 0;
$shiftY.value = 0; $shiftY.value = 0;
const previewScale = 400 / graphWidth; const w = Math.min(400, window.innerWidth * 0.5);
const [w, h] = [400, graphHeight * previewScale]; const previewScale = w / graphWidth;
$previewBox.style.width = w + 'px'; const h = graphHeight * previewScale;
$previewBox.style.height = h + 'px'; $previewBox.style.width = w + "px";
$previewBox.style.position = 'relative'; $previewBox.style.height = h + "px";
// handle mouse input // 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 // mouse wheel
$previewBox.onwheel = e => { $previewBox.onwheel = e => {
@ -80,14 +78,16 @@ window.UISubmap = (function () {
}; };
// mouse drag // mouse drag
let mouseIsDown = false, mouseX = 0, mouseY = 0; let mouseIsDown = false,
mouseX = 0,
mouseY = 0;
$previewBox.onmousedown = e => { $previewBox.onmousedown = e => {
mouseIsDown = true; mouseIsDown = true;
mouseX = $shiftX.value - e.clientX / previewScale; mouseX = $shiftX.value - e.clientX / previewScale;
mouseY = $shiftY.value - e.clientY / previewScale; mouseY = $shiftY.value - e.clientY / previewScale;
} };
$previewBox.onmouseup = _ => mouseIsDown = false; $previewBox.onmouseup = _ => (mouseIsDown = false);
$previewBox.onmouseleave = _ => mouseIsDown = false; $previewBox.onmouseleave = _ => (mouseIsDown = false);
$previewBox.onmousemove = e => { $previewBox.onmousemove = e => {
if (!mouseIsDown) return; if (!mouseIsDown) return;
e.preventDefault(); e.preventDefault();
@ -99,7 +99,6 @@ window.UISubmap = (function () {
$("#resampleDialog").dialog({ $("#resampleDialog").dialog({
title: "Transform map", title: "Transform map",
width: "430px",
resizable: false, resizable: false,
position: {my: "center", at: "center", of: "svg"}, position: {my: "center", at: "center", of: "svg"},
buttons: { buttons: {
@ -114,7 +113,7 @@ window.UISubmap = (function () {
}); });
// use double resolution for PNG to get sharper image // 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. // could be done with SVG. Faster to load, slower to use.
// const $preview = await loadPreviewSVG($previewBox, w, h); // const $preview = await loadPreviewSVG($previewBox, w, h);
$preview.style.position = "absolute"; $preview.style.position = "absolute";
@ -122,22 +121,22 @@ window.UISubmap = (function () {
$preview.style.height = h + "px"; $preview.style.height = h + "px";
byId("resampleDialog").oninput = event => { byId("resampleDialog").oninput = event => {
const { angle, shiftX, shiftY, ratio, mirrorH, mirrorV } = getTransformInput(); const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
const scale = Math.pow(1.1,ratio); const scale = Math.pow(1.1, ratio);
const transformStyle = ` const transformStyle = `
translate(${shiftX*previewScale}px, ${shiftY*previewScale}px) translate(${shiftX * previewScale}px, ${shiftY * previewScale}px)
scale(${mirrorH?-scale:scale}, ${mirrorV?-scale:scale}) scale(${mirrorH ? -scale : scale}, ${mirrorV ? -scale : scale})
rotate(${angle}rad) rotate(${angle}rad)
`; `;
$preview.style.transform = transformStyle; $preview.style.transform = transformStyle;
$preview.style['transform-origin'] = 'center'; $preview.style["transform-origin"] = "center";
event.stopPropagation(); event.stopPropagation();
} };
} }
async function loadPreview($container, w, h) { 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 canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
@ -148,16 +147,16 @@ window.UISubmap = (function () {
img.onload = function () { img.onload = function () {
ctx.drawImage(img, 0, 0, w, h); ctx.drawImage(img, 0, 0, w, h);
}; };
$container.textContent = ''; $container.textContent = "";
$container.appendChild(canvas); $container.appendChild(canvas);
return canvas; return canvas;
} }
// currently unused alternative to PNG version // currently unused alternative to PNG version
async function loadPreviewSVG($container, w, h) { async function loadPreviewSVG($container, w, h) {
$container.innerHTML = /*html*/` $container.innerHTML = /*html*/ `
<svg id="submapPreviewSVG" viewBox="0 0 ${graphWidth} ${graphHeight}"> <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%" /> <rect fill="url(#oceanic)" width="100%" height="100%" />
<use href="#map"></use> <use href="#map"></use>
</svg> </svg>
@ -171,12 +170,12 @@ window.UISubmap = (function () {
const cellNumId = +byId("submapPointsInput").value; const cellNumId = +byId("submapPointsInput").value;
if (!cellsDensityMap[cellNumId]) return console.error("Unknown cell number!"); 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 [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 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 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 flipH = (x, y) => [-x + 2 * cx, y];
const flipV = (x, y) => [x, -y + 2 * cy]; const flipV = (x, y) => [x, -y + 2 * cy];
const app = (f, g) => (x, y) => f(...g(x, y)); const app = (f, g) => (x, y) => f(...g(x, y));
@ -186,7 +185,7 @@ window.UISubmap = (function () {
let inverse = id; let inverse = id;
if (angle) [projection, inverse] = [rot(angle), rot(-angle)]; 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 (mirrorH) [projection, inverse] = [app(flipH, projection), app(inverse, flipH)];
if (mirrorV) [projection, inverse] = [app(flipV, projection), app(inverse, flipV)]; if (mirrorV) [projection, inverse] = [app(flipV, projection), app(inverse, flipV)];
if (shiftX || shiftY) { if (shiftX || shiftY) {
@ -230,7 +229,7 @@ window.UISubmap = (function () {
smoothHeightMap: scale > 2, smoothHeightMap: scale > 2,
inverse: (x, y) => [x / origScale + x0, y / origScale + y0], inverse: (x, y) => [x / origScale + x0, y / origScale + y0],
projection: (x, y) => [(x - x0) * origScale, (y - y0) * origScale], projection: (x, y) => [(x - x0) * origScale, (y - y0) * origScale],
scale: origScale, scale: origScale
}; };
// converting map position on the planet // converting map position on the planet

View file

@ -137,7 +137,7 @@ function recalculatePopulation() {
} }
function regenerateStates() { function regenerateStates() {
const localSeed = Math.floor(Math.random() * 1e9); // new random seed const localSeed = generateSeed();
Math.random = aleaPRNG(localSeed); Math.random = aleaPRNG(localSeed);
const statesCount = +regionsOutput.value; const statesCount = +regionsOutput.value;

View file

@ -3,7 +3,7 @@ function editWorld() {
$("#worldConfigurator").dialog({ $("#worldConfigurator").dialog({
title: "Configure World", title: "Configure World",
resizable: false, resizable: false,
width: "42em", width: "minmax(40em, 85vw)",
buttons: { buttons: {
"Whole World": () => applyWorldPreset(100, 50), "Whole World": () => applyWorldPreset(100, 50),
Northern: () => applyWorldPreset(33, 25), Northern: () => applyWorldPreset(33, 25),
@ -73,16 +73,16 @@ function editWorld() {
const eqD = ((graphHeight / 2) * 100) / size; const eqD = ((graphHeight / 2) * 100) / size;
calculateMapCoordinates(); calculateMapCoordinates();
const mc = mapCoordinates; // shortcut const mc = mapCoordinates;
const scale = +distanceScaleInput.value, const scale = +distanceScaleInput.value;
unit = distanceUnitInput.value; const unit = distanceUnitInput.value;
const meridian = toKilometer(eqD * 2 * scale); const meridian = toKilometer(eqD * 2 * scale);
document.getElementById("mapSize").innerHTML = /* html */ `${graphWidth}x${graphHeight}`; document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
document.getElementById("mapSizeFriendly").innerHTML = /* html */ `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`; document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
document.getElementById("meridianLength").innerHTML = rn(eqD * 2); 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("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) { function toKilometer(v) {
if (unit === "km") return v; if (unit === "km") return v;
@ -92,9 +92,11 @@ function editWorld() {
return 0; // 0 if distanceUnitInput is a custom unit return 0; // 0 if distanceUnitInput is a custom unit
} }
// parse latitude value
function lat(lat) { function lat(lat) {
return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S"; return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";
} // parse latitude value }
const area = d3.geoGraticule().extent([ const area = d3.geoGraticule().extent([
[mc.lonW, mc.latN], [mc.lonW, mc.latN],
[mc.lonE, mc.latS] [mc.lonE, mc.latS]

View file

@ -1,7 +1,5 @@
"use strict"; "use strict";
// FMG utils related to arrays
// return the last element of array
function last(array) { function last(array) {
return array[array.length - 1]; return array[array.length - 1];
} }
@ -37,9 +35,24 @@ function deepCopy(obj) {
[Set, s => [...s.values()].map(dcAny)], [Set, s => [...s.values()].map(dcAny)],
[Date, d => new Date(d.getTime())], [Date, d => new Date(d.getTime())],
[Object, dcObject] [Object, dcObject]
// other types will be referenced
// ... extend here to implement their custom deep copy // ... extend here to implement their custom deep copy
]); ]);
return dcAny(obj); 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"; "use strict";
// FMG utils related to graph // 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) { function getBoundaryPoints(width, height, spacing) {
const offset = rn(-1 * spacing); const offset = rn(-1 * spacing);
const bSpacing = spacing * 2; const bSpacing = spacing * 2;
@ -9,15 +62,18 @@ function getBoundaryPoints(width, height, spacing) {
const h = height - offset * 2; const h = height - offset * 2;
const numberX = Math.ceil(w / bSpacing) - 1; const numberX = Math.ceil(w / bSpacing) - 1;
const numberY = Math.ceil(h / bSpacing) - 1; const numberY = Math.ceil(h / bSpacing) - 1;
let points = []; const points = [];
for (let i = 0.5; i < numberX; i++) { for (let i = 0.5; i < numberX; i++) {
let x = Math.ceil((w * i) / numberX + offset); let x = Math.ceil((w * i) / numberX + offset);
points.push([x, offset], [x, h + offset]); points.push([x, offset], [x, h + offset]);
} }
for (let i = 0.5; i < numberY; i++) { for (let i = 0.5; i < numberY; i++) {
let y = Math.ceil((h * i) / numberY + offset); let y = Math.ceil((h * i) / numberY + offset);
points.push([offset, y], [w + offset, y]); points.push([offset, y], [w + offset, y]);
} }
return points; return points;
} }
@ -40,7 +96,7 @@ function getJitteredGrid(width, height, spacing) {
} }
// return cell index on a regular square grid // 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)); 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) { function findGridAll(x, y, radius) {
const c = grid.cells.c; const c = grid.cells.c;
let r = Math.floor(radius / grid.spacing); 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 || radius === 1) return found;
if (r > 0) found = found.concat(c[found[0]]); if (r > 0) found = found.concat(c[found[0]]);
if (r > 1) { if (r > 1) {
@ -261,7 +317,7 @@ function drawCellsValue(data) {
function drawPolygons(data) { function drawPolygons(data) {
const max = d3.max(data), const max = d3.max(data),
min = d3.min(data), min = d3.min(data),
scheme = getColorScheme(); scheme = getColorScheme(terrs.attr("scheme"));
data = data.map(d => 1 - normalize(d, min, max)); data = data.map(d => 1 - normalize(d, min, max));
debug.selectAll("polygon").remove(); debug.selectAll("polygon").remove();

View file

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

View file

@ -1,7 +1,7 @@
"use strict"; "use strict";
// version and caching control // 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; document.title += " v" + version;
@ -28,6 +28,8 @@ const version = "1.83.0"; // generator version, update each time
<ul> <ul>
<strong>Latest changes:</strong> <strong>Latest changes:</strong>
<li>Heightmap selection screen</li>
<li>Dialogs optimization for mobile</li>
<li>New heightmap template: Fractious</li> <li>New heightmap template: Fractious</li>
<li>Template Editor: mask and invert tools</li> <li>Template Editor: mask and invert tools</li>
<li>Ability to install the App</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>Submap tool by Goteguru</li>
<li>Resample tool by Goteguru</li> <li>Resample tool by Goteguru</li>
<li>Pre-defined heightmaps</li> <li>Pre-defined heightmaps</li>
<li>Advanced notes editor</li>
</ul> </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> <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>