+
@@ -7924,14 +7959,14 @@
-
-
+
+
-
-
+
+
@@ -7967,8 +8002,8 @@
-
-
+
+
diff --git a/main.js b/main.js
index e6f95d45..c5dfdf4b 100644
--- a/main.js
+++ b/main.js
@@ -5,8 +5,8 @@
// set debug options
const PRODUCTION = location.hostname && location.hostname !== "localhost" && location.hostname !== "127.0.0.1";
const DEBUG = localStorage.getItem("debug");
-const INFO = DEBUG || !PRODUCTION;
-const TIME = DEBUG || !PRODUCTION;
+const INFO = true;
+const TIME = true;
const WARN = true;
const ERROR = true;
@@ -179,13 +179,17 @@ function onZoom() {
const onZoomDebouced = debounce(onZoom, 50);
const zoom = d3.zoom().scaleExtent([1, 20]).on("zoom", onZoomDebouced);
-// default options
+// default options, based on Earth data
let options = {
pinNotes: false,
showMFCGMap: true,
winds: [225, 45, 225, 315, 135, 315],
+ temperatureEquator: 27,
+ temperatureNorthPole: -30,
+ temperatureSouthPole: -15,
stateLabelsMode: "auto"
};
+
let mapCoordinates = {}; // map coordinates on globe
let populationRate = +document.getElementById("populationRateInput").value;
let distanceScale = +document.getElementById("distanceScaleInput").value;
@@ -467,7 +471,7 @@ function applyDefaultBiomesSystem() {
const biomesMartix = [
// hot ↔ cold [>19°C; <-4°C]; dry ↕ wet
new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10]),
- new Uint8Array([3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10, 10, 10]),
+ new Uint8Array([1, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10, 10, 10]),
new Uint8Array([5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 10, 10, 10]),
new Uint8Array([5, 6, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 10]),
new Uint8Array([7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10, 10])
@@ -968,7 +972,7 @@ function defineMapSize() {
const part = grid.features.some(f => f.land && f.border); // if land goes over map borders
const max = part ? 80 : 100; // max size
- const lat = () => gauss(P(0.5) ? 40 : 60, 15, 25, 75); // latitude shift
+ const lat = () => gauss(P(0.5) ? 40 : 60, 20, 25, 75); // latitude shift
if (!part) {
if (template === "Pangea") return [100, 50];
@@ -1003,30 +1007,49 @@ function calculateMapCoordinates() {
mapCoordinates = {latT, latN, latS, lonT: lon * 2, lonW: -lon, lonE: lon};
}
-// temperature model
+// temperature model, trying to follow real-world data
+// based on http://www-das.uwyo.edu/~geerts/cwx/notes/chap16/Image64.gif
function calculateTemperatures() {
TIME && console.time("calculateTemperatures");
const cells = grid.cells;
cells.temp = new Int8Array(cells.i.length); // temperature array
- const tEq = +temperatureEquatorInput.value;
- const tPole = +temperaturePoleInput.value;
- const tDelta = tEq - tPole;
- const int = d3.easePolyInOut.exponent(0.5); // interpolation function
+ const {temperatureEquator, temperatureNorthPole, temperatureSouthPole} = options;
+ const tropics = [16, -20]; // tropics zone
+ const tropicalGradient = 0.15;
- d3.range(0, cells.i.length, grid.cellsX).forEach(function (r) {
- const y = grid.points[r][1];
- const lat = Math.abs(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT); // [0; 90]
- const initTemp = tEq - int(lat / 90) * tDelta;
- for (let i = r; i < r + grid.cellsX; i++) {
- cells.temp[i] = minmax(initTemp - convertToFriendly(cells.h[i]), -128, 127);
+ const tempNorthTropic = temperatureEquator - tropics[0] * tropicalGradient;
+ const northernGradient = (tempNorthTropic - temperatureNorthPole) / (90 - tropics[0]);
+
+ const tempSouthTropic = temperatureEquator + tropics[1] * tropicalGradient;
+ const southernGradient = (tempSouthTropic - temperatureSouthPole) / (90 + tropics[1]);
+
+ const exponent = +heightExponentInput.value;
+
+ for (let rowCellId = 0; rowCellId < cells.i.length; rowCellId += grid.cellsX) {
+ const [, y] = grid.points[rowCellId];
+ const rowLatitude = mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT; // [90; -90]
+ const tempSeaLevel = calculateSeaLevelTemp(rowLatitude);
+ DEBUG && console.info(`${rn(rowLatitude)}° sea temperature: ${rn(tempSeaLevel)}°C`);
+
+ for (let cellId = rowCellId; cellId < rowCellId + grid.cellsX; cellId++) {
+ const tempAltitudeDrop = getAltitudeTemperatureDrop(cells.h[cellId]);
+ cells.temp[cellId] = minmax(tempSeaLevel - tempAltitudeDrop, -128, 127);
}
- });
+ }
- // temperature decreases by 6.5 degree C per 1km
- function convertToFriendly(h) {
+ function calculateSeaLevelTemp(latitude) {
+ const isTropical = latitude <= 16 && latitude >= -20;
+ if (isTropical) return temperatureEquator - Math.abs(latitude) * tropicalGradient;
+
+ return latitude > 0
+ ? tempNorthTropic - (latitude - tropics[0]) * northernGradient
+ : tempSouthTropic + (latitude - tropics[1]) * southernGradient;
+ }
+
+ // temperature drops by 6.5°C per 1km of altitude
+ function getAltitudeTemperatureDrop(h) {
if (h < 20) return 0;
- const exponent = +heightExponentInput.value;
const height = Math.pow(h - 18, exponent);
return rn((height / 1000) * 6.5);
}
diff --git a/modules/dynamic/export-json.js b/modules/dynamic/export-json.js
index d9e6d42e..8860cbd0 100644
--- a/modules/dynamic/export-json.js
+++ b/modules/dynamic/export-json.js
@@ -108,8 +108,6 @@ function getSettings() {
urbanization: urbanization,
mapSize: mapSizeOutput.value,
latitudeO: latitudeOutput.value,
- temperatureEquator: temperatureEquatorOutput.value,
- temperaturePole: temperaturePoleOutput.value,
prec: precOutput.value,
options: options,
mapName: mapName.value,
diff --git a/modules/io/load.js b/modules/io/load.js
index a3549279..13b2b6e7 100644
--- a/modules/io/load.js
+++ b/modules/io/load.js
@@ -218,10 +218,13 @@ async function parseLoadedData(data) {
if (settings[13]) urbanization = urbanizationInput.value = urbanizationOutput.value = settings[13];
if (settings[14]) mapSizeInput.value = mapSizeOutput.value = minmax(settings[14], 1, 100);
if (settings[15]) latitudeInput.value = latitudeOutput.value = minmax(settings[15], 0, 100);
- if (settings[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = settings[16];
- if (settings[17]) temperaturePoleInput.value = temperaturePoleOutput.value = settings[17];
if (settings[18]) precInput.value = precOutput.value = settings[18];
+
if (settings[19]) options = JSON.parse(settings[19]);
+ // setting 16 and 17 (temperature) are part of options now
+ if (settings[16]) options.temperatureEquator = Number(settings[16]);
+ if (settings[17]) options.temperatureNorthPole = options.temperatureSouthPole = Number(settings[17]);
+
if (settings[20]) mapName.value = settings[20];
if (settings[21]) hideLabels.checked = +settings[21];
if (settings[22]) stylePreset.value = settings[22];
@@ -231,6 +234,9 @@ async function parseLoadedData(data) {
void (function applyOptionsToUI() {
stateLabelsModeInput.value = options.stateLabelsMode;
+ yearInput.value = options.year;
+ eraInput.value = options.era;
+ shapeRendering.value = viewbox.attr("shape-rendering") || "geometricPrecision";
})();
void (function parseConfiguration() {
@@ -586,11 +592,6 @@ async function parseLoadedData(data) {
if (rulers && layerIsOn("toggleRulers")) rulers.draw();
if (layerIsOn("toggleGrid")) drawGrid();
- // set options
- yearInput.value = options.year;
- eraInput.value = options.era;
- shapeRendering.value = viewbox.attr("shape-rendering") || "geometricPrecision";
-
if (window.restoreDefaultEvents) restoreDefaultEvents();
focusOn(); // based on searchParams focus on point, cell or burg
invokeActiveZooming();
diff --git a/modules/io/save.js b/modules/io/save.js
index bf78fd0e..f3e58147 100644
--- a/modules/io/save.js
+++ b/modules/io/save.js
@@ -24,8 +24,8 @@ function getMapData() {
urbanization,
mapSizeOutput.value,
latitudeOutput.value,
- temperatureEquatorOutput.value,
- temperaturePoleOutput.value,
+ 0, // previously used for temperatureEquatorOutput.value
+ 0, // previously used for tempNorthOutput.value
precOutput.value,
JSON.stringify(options),
mapName.value,
diff --git a/modules/ui/options.js b/modules/ui/options.js
index 37eb3f06..04d65494 100644
--- a/modules/ui/options.js
+++ b/modules/ui/options.js
@@ -559,11 +559,10 @@ function applyStoredOptions() {
if (key.slice(0, 5) === "style") applyOption(stylePreset, key, key.slice(5));
}
- if (stored("winds"))
- options.winds = localStorage
- .getItem("winds")
- .split(",")
- .map(w => +w);
+ if (stored("winds")) options.winds = localStorage.getItem("winds").split(",").map(Number);
+ if (stored("temperatureEquator")) options.temperatureEquator = +localStorage.getItem("temperatureEquator");
+ if (stored("temperatureNorthPole")) options.temperatureNorthPole = +localStorage.getItem("temperatureNorthPole");
+ if (stored("temperatureSouthPole")) options.temperatureSouthPole = +localStorage.getItem("temperatureSouthPole");
if (stored("military")) options.military = JSON.parse(stored("military"));
if (stored("tooltipSize")) changeTooltipSize(stored("tooltipSize"));
@@ -607,13 +606,10 @@ function randomizeOptions() {
if (randomize || !locked("culturesSet")) randomizeCultureSet();
// 'Configure World' settings
+ if (randomize || !locked("temperatureEquator")) options.temperatureEquator = gauss(25, 7, 20, 35, 0);
+ if (randomize || !locked("temperatureNorthPole")) options.temperatureNorthPole = gauss(-25, 7, -40, 10, 0);
+ if (randomize || !locked("temperatureSouthPole")) options.temperatureSouthPole = gauss(-15, 7, -40, 10, 0);
if (randomize || !locked("prec")) precInput.value = precOutput.value = gauss(100, 40, 5, 500);
- const tMax = 30,
- tMin = -30; // temperature extremes
- if (randomize || !locked("temperatureEquator"))
- temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax - 10, tMax);
- if (randomize || !locked("temperaturePole"))
- temperaturePoleOutput.value = temperaturePoleInput.value = rand(tMin, tMin + 30);
// 'Units Editor' settings
const US = navigator.language === "en-US";
@@ -1072,8 +1068,7 @@ function toggle3dOptions() {
document.getElementById("options3dGlobeResolution").addEventListener("change", changeResolution);
// document.getElementById("options3dMeshWireframeMode").addEventListener("change",toggleWireframe3d);
document.getElementById("options3dSunColor").addEventListener("input", changeSunColor);
- document.getElementById("options3dSubdivide").addEventListener("change",toggle3dSubdivision);
-
+ document.getElementById("options3dSubdivide").addEventListener("change", toggle3dSubdivision);
function updateValues() {
const globe = document.getElementById("canvas3d").dataset.type === "viewGlobe";
@@ -1113,7 +1108,7 @@ function toggle3dOptions() {
ThreeD.setLightness(this.value / 100);
}
- function changeSunColor(){
+ function changeSunColor() {
ThreeD.setSunColor(options3dSunColor.value);
}
@@ -1134,7 +1129,7 @@ function toggle3dOptions() {
ThreeD.toggleLabels();
}
- function toggle3dSubdivision(){
+ function toggle3dSubdivision() {
ThreeD.toggle3dSubdivision();
}
diff --git a/modules/ui/world-configurator.js b/modules/ui/world-configurator.js
index 73a3621e..ae222a59 100644
--- a/modules/ui/world-configurator.js
+++ b/modules/ui/world-configurator.js
@@ -1,5 +1,6 @@
function editWorld() {
if (customization) return;
+
$("#worldConfigurator").dialog({
title: "Configure World",
resizable: false,
@@ -8,8 +9,7 @@ function editWorld() {
"Whole World": () => applyWorldPreset(100, 50),
Northern: () => applyWorldPreset(33, 25),
Tropical: () => applyWorldPreset(33, 50),
- Southern: () => applyWorldPreset(33, 75),
- "Restore Winds": restoreDefaultWinds
+ Southern: () => applyWorldPreset(33, 75)
},
open: function () {
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
@@ -17,7 +17,6 @@ function editWorld() {
buttons[1].addEventListener("mousemove", () => tip("Click to set map size to cover the Northern latitudes"));
buttons[2].addEventListener("mousemove", () => tip("Click to set map size to cover the Tropical latitudes"));
buttons[3].addEventListener("mousemove", () => tip("Click to set map size to cover the Southern latitudes"));
- buttons[4].addEventListener("mousemove", () => tip("Click to restore default wind directions"));
},
close: function () {
$(this).dialog("destroy");
@@ -25,28 +24,56 @@ function editWorld() {
});
const globe = d3.select("#globe");
- const clr = d3.scaleSequential(d3.interpolateSpectral);
- const tMax = 30,
- tMin = -25; // temperature extremes
const projection = d3.geoOrthographic().translate([100, 100]).scale(100);
const path = d3.geoPath(projection);
+ updateInputValues();
updateGlobeTemperature();
updateGlobePosition();
if (modules.editWorld) return;
modules.editWorld = true;
- document.getElementById("worldControls").addEventListener("input", e => updateWorld(e.target));
+ byId("worldControls").addEventListener("input", e => updateWorld(e.target));
globe.select("#globeWindArrows").on("click", changeWind);
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
updateWindDirections();
+ byId("restoreWinds").addEventListener("click", restoreDefaultWinds);
+
+ function updateInputValues() {
+ byId("temperatureEquatorInput").value = options.temperatureEquator;
+ byId("temperatureEquatorOutput").value = options.temperatureEquator;
+ byId("temperatureEquatorF").innerText = convertTemperature(options.temperatureEquator, "°F");
+
+ byId("temperatureNorthPoleInput").value = options.temperatureNorthPole;
+ byId("temperatureNorthPoleOutput").value = options.temperatureNorthPole;
+ byId("temperatureNorthPoleF").innerText = convertTemperature(options.temperatureNorthPole, "°F");
+
+ byId("temperatureSouthPoleInput").value = options.temperatureSouthPole;
+ byId("temperatureSouthPoleOutput").value = options.temperatureSouthPole;
+ byId("temperatureSouthPoleF").innerText = convertTemperature(options.temperatureSouthPole, "°F");
+ }
+
function updateWorld(el) {
- if (el) {
- document.getElementById(el.dataset.stored + "Input").value = el.value;
- document.getElementById(el.dataset.stored + "Output").value = el.value;
- if (el.dataset.stored) lock(el.dataset.stored);
+ if (el?.dataset.stored) {
+ const stored = el.dataset.stored;
+ byId(stored + "Input").value = el.value;
+ byId(stored + "Output").value = el.value;
+ lock(el.dataset.stored);
+
+ if (stored === "temperatureEquator") {
+ options.temperatureEquator = Number(el.value);
+ byId("temperatureEquatorF").innerText = convertTemperature(options.temperatureEquator, "°F");
+ }
+ if (stored === "temperatureNorthPole") {
+ options.temperatureNorthPole = Number(el.value);
+ byId("temperatureNorthPoleF").innerText = convertTemperature(options.temperatureNorthPole, "°F");
+ }
+ if (stored === "temperatureSouthPole") {
+ options.temperatureSouthPole = Number(el.value);
+ byId("temperatureSouthPoleF").innerText = convertTemperature(options.temperatureSouthPole, "°F");
+ }
}
updateGlobeTemperature();
@@ -65,11 +92,11 @@ function editWorld() {
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (layerIsOn("toggleRivers")) drawRivers();
- if (document.getElementById("canvas3d")) setTimeout(ThreeD.update(), 500);
+ if (byId("canvas3d")) setTimeout(ThreeD.update(), 500);
}
function updateGlobePosition() {
- const size = +document.getElementById("mapSizeOutput").value;
+ const size = +byId("mapSizeOutput").value;
const eqD = ((graphHeight / 2) * 100) / size;
calculateMapCoordinates();
@@ -77,12 +104,12 @@ function editWorld() {
const scale = +distanceScaleInput.value;
const unit = distanceUnitInput.value;
const meridian = toKilometer(eqD * 2 * scale);
- document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
- document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
- document.getElementById("meridianLength").innerHTML = rn(eqD * 2);
- document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
- document.getElementById("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : "";
- document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
+ byId("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
+ byId("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
+ byId("meridianLength").innerHTML = rn(eqD * 2);
+ byId("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
+ byId("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : "";
+ byId("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
function toKilometer(v) {
if (unit === "km") return v;
@@ -104,15 +131,24 @@ function editWorld() {
globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
}
+ // update temperatures on globe (visual-only)
function updateGlobeTemperature() {
- const tEq = +document.getElementById("temperatureEquatorOutput").value;
- document.getElementById("temperatureEquatorF").innerHTML = rn((tEq * 9) / 5 + 32);
- const tPole = +document.getElementById("temperaturePoleOutput").value;
- document.getElementById("temperaturePoleF").innerHTML = rn((tPole * 9) / 5 + 32);
- globe.selectAll(".tempGradient90").attr("stop-color", clr(1 - (tPole - tMin) / (tMax - tMin)));
- globe.selectAll(".tempGradient60").attr("stop-color", clr(1 - (tEq - ((tEq - tPole) * 2) / 3 - tMin) / (tMax - tMin)));
- globe.selectAll(".tempGradient30").attr("stop-color", clr(1 - (tEq - ((tEq - tPole) * 1) / 3 - tMin) / (tMax - tMin)));
- globe.select(".tempGradient0").attr("stop-color", clr(1 - (tEq - tMin) / (tMax - tMin)));
+ const tEq = options.temperatureEquator;
+ const tNP = options.temperatureNorthPole;
+ const tSP = options.temperatureSouthPole;
+
+ const scale = d3.scaleSequential(d3.interpolateSpectral);
+ const getColor = value => scale(1 - value);
+ const [tMin, tMax] = [-25, 30]; // temperature extremes
+ const tDelta = tMax - tMin;
+
+ globe.select("#grad90").attr("stop-color", getColor((tNP - tMin) / tDelta));
+ globe.select("#grad60").attr("stop-color", getColor((tEq - ((tEq - tNP) * 2) / 3 - tMin) / tDelta));
+ globe.select("#grad30").attr("stop-color", getColor((tEq - ((tEq - tNP) * 1) / 4 - tMin) / tDelta));
+ globe.select("#grad0").attr("stop-color", getColor((tEq - tMin) / tDelta));
+ globe.select("#grad-30").attr("stop-color", getColor((tEq - ((tEq - tSP) * 1) / 4 - tMin) / tDelta));
+ globe.select("#grad-60").attr("stop-color", getColor((tEq - ((tEq - tSP) * 2) / 3 - tMin) / tDelta));
+ globe.select("#grad-90").attr("stop-color", getColor((tSP - tMin) / tDelta));
}
function updateWindDirections() {
@@ -146,8 +182,8 @@ function editWorld() {
}
function applyWorldPreset(size, lat) {
- document.getElementById("mapSizeInput").value = document.getElementById("mapSizeOutput").value = size;
- document.getElementById("latitudeInput").value = document.getElementById("latitudeOutput").value = lat;
+ byId("mapSizeInput").value = byId("mapSizeOutput").value = size;
+ byId("latitudeInput").value = byId("latitudeOutput").value = lat;
lock("mapSize");
lock("latitude");
updateWorld();
diff --git a/utils/unitUtils.js b/utils/unitUtils.js
index a9a933df..d940e349 100644
--- a/utils/unitUtils.js
+++ b/utils/unitUtils.js
@@ -13,8 +13,7 @@ const temperatureConversionMap = {
"°Rø": temp => rn((temp * 21) / 40 + 7.5) + "°Rø"
};
-function convertTemperature(temp) {
- const scale = temperatureScale.value || "°C";
+function convertTemperature(temp, scale = temperatureScale.value || "°C") {
return temperatureConversionMap[scale](temp);
}
diff --git a/versioning.js b/versioning.js
index 7b4b88e1..77a1bcda 100644
--- a/versioning.js
+++ b/versioning.js
@@ -1,8 +1,7 @@
"use strict";
// version and caching control
-
-const version = "1.89.39"; // generator version, update each time
+const version = "1.90.00"; // generator version, update each time
{
document.title += " v" + version;
@@ -29,17 +28,16 @@ const version = "1.89.39"; // generator version, update each time
Latest changes:
+ - North and South Poles temperature can be set independently
- More than 70 new heraldic charges
- Multi-color heraldic charges support
- - New 3D scene options
+ - New 3D scene options and improvements
- Autosave feature (in Options)
- Google translation support (in Options)
- Religions can be edited and redrawn like cultures
- Lock states, provinces, cultures, and religions from regeneration
- Heightmap brushes: linear edit option
- Data Charts screen
- - Сultures and religions can have multiple parents in hierarchy tree
- - Heightmap selection screen
Join our Discord server and Reddit community to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.