diff --git a/index.html b/index.html
index c1c6364d..dafe0df7 100644
--- a/index.html
+++ b/index.html
@@ -1610,6 +1610,22 @@
+
|
@@ -8074,7 +8090,7 @@
-
+
@@ -8084,6 +8100,7 @@
+
@@ -8111,7 +8128,7 @@
-
+
@@ -8152,7 +8169,7 @@
-
+
diff --git a/main.js b/main.js
index 29af6392..3c8f798f 100644
--- a/main.js
+++ b/main.js
@@ -310,12 +310,45 @@ async function checkLoadParameters() {
generateMapOnLoad();
}
+function debugGrids() {
+ debug
+ .selectAll("circle.grid")
+ .data(grid.points)
+ .enter()
+ .append("circle")
+ .attr("data-id", (d, i) => "point-" + i)
+ .attr("cx", d => d[0])
+ .attr("cy", d => d[1])
+ .attr("r", 0.5)
+ .attr("fill", "blue");
+
+ let path = "";
+ grid.cells.i.forEach(i => (path += "M" + getGridPolygon(i)));
+ debug.append("path").attr("fill", "none").attr("stroke", "blue").attr("stroke-width", 0.3).attr("d", path);
+
+ debug
+ .selectAll("circle.boundary")
+ .data(grid.boundary)
+ .enter()
+ .append("circle")
+ .attr("cx", d => d[0])
+ .attr("cy", d => d[1])
+ .attr("r", 0.3)
+ .attr("fill", "white");
+
+ zoom.translateExtent([
+ [-graphWidth / 2, -graphHeight / 2],
+ [graphWidth * 1.5, graphHeight * 1.5]
+ ]);
+}
+
async function generateMapOnLoad() {
await applyStyleOnLoad(); // apply previously selected default or custom style
await generate(); // generate map
applyPreset(); // apply saved layers preset
fitMapToScreen();
focusOn(); // based on searchParams focus on point, cell or burg from MFCG
+ debugGrids();
}
// focus on coordinates, cell or burg provided in searchParams
@@ -1034,15 +1067,22 @@ function generatePrecipitation() {
const MAX_PASSABLE_ELEVATION = 85;
// define wind directions based on cells latitude and prevailing winds there
- d3.range(0, cells.i.length, cellsX).forEach(function (c, i) {
+ d3.range(0, cells.i.length, cellsX).forEach(function (cellId, i) {
+ debug
+ .append("circle")
+ .attr("cx", grid.points[cellId][0])
+ .attr("cy", grid.points[cellId][1])
+ .attr("r", 2)
+ .attr("fill", "blue");
+
const lat = mapCoordinates.latN - (i / cellsY) * mapCoordinates.latT;
const latBand = ((Math.abs(lat) - 1) / 5) | 0;
const latMod = latitudeModifier[latBand];
const windTier = (Math.abs(lat - 89) / 30) | 0; // 30d tiers from 0 to 5 from N to S
const {isWest, isEast, isNorth, isSouth} = getWindDirections(windTier);
- if (isWest) westerly.push([c, latMod, windTier]);
- if (isEast) easterly.push([c + cellsX - 1, latMod, windTier]);
+ if (isWest) westerly.push([cellId, latMod, windTier]);
+ if (isEast) easterly.push([cellId + cellsX - 1, latMod, windTier]);
if (isNorth) northerly++;
if (isSouth) southerly++;
});
@@ -1066,6 +1106,8 @@ function generatePrecipitation() {
passWind(d3.range(cells.i.length - cellsX, cells.i.length, 1), maxPrecS, -cellsX, cellsY);
}
+ drawWindDirection();
+
function getWindDirections(tier) {
const angle = options.winds[tier];
@@ -1120,24 +1162,25 @@ function generatePrecipitation() {
return minmax(normalLoss + diff * mod, 1, humidity);
}
- void (function drawWindDirection() {
+ function drawWindDirection() {
const wind = prec.append("g").attr("id", "wind");
- d3.range(0, 6).forEach(function (t) {
+ options.winds.forEach((direction, tier) => {
if (westerly.length > 1) {
- const west = westerly.filter(w => w[2] === t);
+ const west = westerly.filter(w => w[2] === tier);
if (west && west.length > 3) {
- const from = west[0][0],
- to = west[west.length - 1][0];
+ const from = west.at(0)[0];
+ const to = west.at(-1)[0];
const y = (grid.points[from][1] + grid.points[to][1]) / 2;
wind.append("text").attr("x", 20).attr("y", y).text("\u21C9");
}
}
+
if (easterly.length > 1) {
- const east = easterly.filter(w => w[2] === t);
+ const east = easterly.filter(w => w[2] === tier);
if (east && east.length > 3) {
- const from = east[0][0],
- to = east[east.length - 1][0];
+ const from = east.at(0)[0];
+ const to = east.at(-1)[0];
const y = (grid.points[from][1] + grid.points[to][1]) / 2;
wind
.append("text")
@@ -1160,7 +1203,7 @@ function generatePrecipitation() {
.attr("x", graphWidth / 2)
.attr("y", graphHeight - 20)
.text("\u21C8");
- })();
+ }
TIME && console.timeEnd("generatePrecipitation");
}
@@ -1169,20 +1212,24 @@ function generatePrecipitation() {
function reGraph() {
TIME && console.time("reGraph");
const {cells: gridCells, points, features} = grid;
+ const repackGridCells = grid.type === "jittered";
const newCells = {p: [], g: [], h: []}; // store new data
const spacing2 = grid.spacing ** 2;
for (const i of gridCells.i) {
const height = gridCells.h[i];
const type = gridCells.t[i];
- if (height < 20 && type !== -1 && type !== -2) continue; // exclude all deep ocean points
- if (type === -2 && (i % 4 === 0 || features[gridCells.f[i]].type === "lake")) continue; // exclude non-coastal lake points
- const [x, y] = points[i];
+ if (repackGridCells) {
+ if (height < 20 && type !== -1 && type !== -2) continue; // exclude all deep ocean points
+ if (type === -2 && (i % 4 === 0 || features[gridCells.f[i]].type === "lake")) continue; // exclude non-coastal lake points
+ }
+
+ const [x, y] = points[i];
addNewPoint(i, x, y, height);
// add additional points for cells along coast
- if (type === 1 || type === -1) {
+ if (repackGridCells && (type === 1 || type === -1)) {
if (gridCells.b[i]) continue; // not for near-border cells
gridCells.c[i].forEach(function (e) {
if (i > e) return;
@@ -1253,6 +1300,8 @@ function drawCoastline() {
vchain.map(v => vertices.p[v]),
1
);
+ if (points.length < 3) debugger;
+
const area = d3.polygonArea(points); // area with lakes/islands
if (area > 0 && features[f].type === "lake") {
points = points.reverse();
@@ -1962,6 +2011,8 @@ const regenerateMap = debounce(async function (options) {
fitMapToScreen();
shouldShowLoading && hideLoading();
clearMainTip();
+
+ debugGrids();
}, 250);
// clear the map
diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js
index 38ae4403..3ee8dc2f 100644
--- a/modules/dynamic/auto-update.js
+++ b/modules/dynamic/auto-update.js
@@ -858,4 +858,9 @@ export function resolveVersionConflicts(version) {
shiftCompass();
}
}
+
+ if (version < 1.99) {
+ // v1.99.00 added alternative graph point sampling methods
+ if (!graph.type) graph.type = "jittered";
+ }
}
diff --git a/modules/io/load.js b/modules/io/load.js
index 1b782686..1e81f99a 100644
--- a/modules/io/load.js
+++ b/modules/io/load.js
@@ -457,7 +457,7 @@ async function parseLoadedData(data, mapVersion) {
{
// dynamically import and run auto-update script
const versionNumber = parseFloat(params[0]);
- const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.98.00");
+ const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.99.00");
resolveVersionConflicts(versionNumber);
}
diff --git a/modules/points-generator.js b/modules/points-generator.js
new file mode 100644
index 00000000..59cbf9f1
--- /dev/null
+++ b/modules/points-generator.js
@@ -0,0 +1,101 @@
+"use strict";
+
+const pointsGenerators = {
+ jittered: generateJitteredPoints,
+ hexFlat: generateHexFlatPoints,
+ hexPointy: generateHexPointyPoints,
+ square: generateSquarePoints
+};
+
+function generatePoints() {
+ TIME && console.time("placePoints");
+ const cellsDesired = +byId("pointsInput").dataset.cells;
+ const spacing = Math.sqrt((graphWidth * graphHeight) / cellsDesired); // spacing between points
+ const boundary = getBoundaryPoints(graphWidth, graphHeight, spacing);
+
+ const type = byId("gridType").value;
+ const {points, cellsX, cellsY} = pointsGenerators[type](graphWidth, graphHeight, spacing);
+ TIME && console.timeEnd("placePoints");
+
+ return {spacing, cellsDesired, type, boundary, points, cellsX, cellsY};
+}
+
+function generateJitteredPoints(width, height, spacing) {
+ return generateSquareJitteredPoints(0.9, width, height, spacing);
+}
+
+function generateSquarePoints(width, height, spacing) {
+ return generateSquareJitteredPoints(0, width, height, spacing);
+}
+
+function generateSquareJitteredPoints(jittering, width, height, spacing) {
+ const radius = spacing / 2;
+ const maxDeviation = radius * jittering;
+ const jitter = () => (jittering ? Math.random() * maxDeviation * 2 - maxDeviation : 0);
+
+ let points = [];
+ for (let y = radius; y < height; y += spacing) {
+ for (let x = radius; x < width; x += spacing) {
+ const xj = Math.min(rn(x + jitter(), 2), width);
+ const yj = Math.min(rn(y + jitter(), 2), height);
+ points.push([xj, yj]);
+ }
+ }
+
+ const cellsX = Math.floor((width + 0.5 * spacing - 1e-10) / spacing);
+ const cellsY = Math.floor((height + 0.5 * spacing - 1e-10) / spacing);
+ return {points, cellsX, cellsY};
+}
+
+function generateHexFlatPoints(width, height, spacing) {
+ return generateHexPoints(false, width, height, spacing);
+}
+
+function generateHexPointyPoints(width, height, spacing) {
+ return generateHexPoints(true, width, height, spacing);
+}
+
+function generateHexPoints(isPointy, width, height, spacing) {
+ const hexRatio = Math.sqrt(3) / 2;
+ const spacingX = isPointy ? spacing / hexRatio : spacing * 2;
+ const spacingY = isPointy ? spacing : spacing / hexRatio / 2;
+ const maxWidth = width + spacingX / 2;
+ const maxHeight = height + spacingY / 2;
+ const indentionX = spacingX / 2;
+
+ let points = [];
+ for (let y = 0, row = 0; y < maxHeight; y += spacingY, row++) {
+ for (let x = row % 2 ? 0 : indentionX; x < maxWidth; x += spacingX) {
+ if (x > width) x = width;
+ if (y > height) y = height;
+ points.push([rn(x, 2), rn(y, 2)]);
+ }
+ }
+
+ const cellsX = Math.ceil(width / spacingX);
+ const cellsY = Math.ceil(height / spacingY);
+ return {points, cellsX, cellsY};
+}
+
+// add points along map edge to pseudo-clip voronoi cells
+function getBoundaryPoints(width, height, spacing) {
+ const offset = rn(-1 * spacing);
+ const bSpacing = spacing * 2;
+ const w = width - offset * 2;
+ const h = height - offset * 2;
+ const numberX = Math.ceil(w / bSpacing) - 1;
+ const numberY = Math.ceil(h / bSpacing) - 1;
+ const points = [];
+
+ for (let i = 0.5; i < numberX; i++) {
+ let x = Math.ceil((w * i) / numberX + offset);
+ points.push([x, offset], [x, h + offset]);
+ }
+
+ for (let i = 0.5; i < numberY; i++) {
+ let y = Math.ceil((h * i) / numberY + offset);
+ points.push([offset, y], [w + offset, y]);
+ }
+
+ return points;
+}
diff --git a/utils/graphUtils.js b/utils/graphUtils.js
index ec6fb59a..a734cd85 100644
--- a/utils/graphUtils.js
+++ b/utils/graphUtils.js
@@ -8,6 +8,9 @@ function shouldRegenerateGrid(grid, expectedSeed) {
const cellsDesired = +byId("pointsInput").dataset.cells;
if (cellsDesired !== grid.cellsDesired) return true;
+ const gridType = byId("gridType").value;
+ if (gridType !== grid.type) 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);
@@ -17,24 +20,9 @@ function shouldRegenerateGrid(grid, expectedSeed) {
function generateGrid() {
Math.random = aleaPRNG(seed); // reset PRNG
- const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints();
+ const {spacing, cellsDesired, type, boundary, points, cellsX, cellsY} = generatePoints();
const {cells, vertices} = calculateVoronoi(points, boundary);
- return {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, seed};
-}
-
-// 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};
+ return {spacing, cellsDesired, type, boundary, points, cellsX, cellsY, cells, vertices, seed};
}
// calculate Delaunay and then Voronoi diagram
@@ -55,47 +43,6 @@ function calculateVoronoi(points, boundary) {
return {cells, vertices};
}
-// add points along map edge to pseudo-clip voronoi cells
-function getBoundaryPoints(width, height, spacing) {
- const offset = rn(-1 * spacing);
- const bSpacing = spacing * 2;
- const w = width - offset * 2;
- const h = height - offset * 2;
- const numberX = Math.ceil(w / bSpacing) - 1;
- const numberY = Math.ceil(h / bSpacing) - 1;
- const points = [];
-
- for (let i = 0.5; i < numberX; i++) {
- let x = Math.ceil((w * i) / numberX + offset);
- points.push([x, offset], [x, h + offset]);
- }
-
- for (let i = 0.5; i < numberY; i++) {
- let y = Math.ceil((h * i) / numberY + offset);
- points.push([offset, y], [w + offset, y]);
- }
-
- return points;
-}
-
-// get points on a regular square grid and jitter them a bit
-function getJitteredGrid(width, height, spacing) {
- const radius = spacing / 2; // square radius
- const jittering = radius * 0.9; // max deviation
- const doubleJittering = jittering * 2;
- const jitter = () => Math.random() * doubleJittering - jittering;
-
- let points = [];
- for (let y = radius; y < height; y += spacing) {
- for (let x = radius; x < width; x += spacing) {
- const xj = Math.min(rn(x + jitter(), 2), width);
- const yj = Math.min(rn(y + jitter(), 2), height);
- points.push([xj, yj]);
- }
- }
- return points;
-}
-
// return cell index on a regular square grid
function findGridCell(x, y, grid) {
return (
diff --git a/versioning.js b/versioning.js
index 79b475dd..92136bc9 100644
--- a/versioning.js
+++ b/versioning.js
@@ -1,7 +1,7 @@
"use strict";
// version and caching control
-const version = "1.98.03"; // generator version, update each time
+const version = "1.99.00"; // generator version, update each time
{
document.title += " v" + version;
|