From 62fa8d517b398738690faf3a18748523f2eff37d Mon Sep 17 00:00:00 2001 From: Azgaar Date: Mon, 15 Jul 2024 22:21:04 +0200 Subject: [PATCH] feat: alternative point samplers WIP --- index.html | 23 +++++++- main.js | 83 +++++++++++++++++++++------ modules/dynamic/auto-update.js | 5 ++ modules/io/load.js | 2 +- modules/points-generator.js | 101 +++++++++++++++++++++++++++++++++ utils/graphUtils.js | 63 ++------------------ versioning.js | 2 +- 7 files changed, 200 insertions(+), 79 deletions(-) create mode 100644 modules/points-generator.js diff --git a/index.html b/index.html index c1c6364d..dafe0df7 100644 --- a/index.html +++ b/index.html @@ -1610,6 +1610,22 @@ + + + + + Grid type + + + + + + @@ -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;