diff --git a/main.js b/main.js index 7a13bd5d..2b4356b6 100644 --- a/main.js +++ b/main.js @@ -629,7 +629,7 @@ void (function addDragToUpload() { async function generate(options) { try { const timeStart = performance.now(); - const {seed: precreatedSeed} = options || {}; + const {seed: precreatedSeed, graph: precreatedGraph} = options || {}; invokeActiveZooming(); setSeed(precreatedSeed); @@ -638,11 +638,9 @@ async function generate(options) { applyMapSize(); randomizeOptions(); - if (shouldRegenerateGrid()) { - placePoints(); - calculateVoronoi(grid, grid.points); - } - await HeightmapGenerator.generate(); + if (shouldRegenerateGrid(grid)) grid = precreatedGraph || generateGrid(); + else delete grid.cells.h; + grid.cells.h = await HeightmapGenerator.generate(grid); markFeatures(); markupGridOcean(); @@ -654,6 +652,7 @@ async function generate(options) { calculateMapCoordinates(); calculateTemperatures(); generatePrecipitation(); + reGraph(); drawCoastline(); @@ -736,52 +735,13 @@ function setSeed(precreatedSeed) { Math.random = aleaPRNG(seed); } -// check if new grid graph should be generated or we can use the existing one -function shouldRegenerateGrid() { - if (!grid.spacing) return true; - const cellsDesired = +byId("pointsInput").dataset.cells; - const newSpacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); - return grid.spacing !== newSpacing; -} - -// Place points to calculate Voronoi diagram -function placePoints() { - TIME && console.time("placePoints"); - Math.random = aleaPRNG(seed); // reset PRNG - - const cellsDesired = +byId("pointsInput").dataset.cells; - const spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); // spacing between points before jirrering - grid.spacing = spacing; - grid.boundary = getBoundaryPoints(graphWidth, graphHeight, spacing); - grid.points = getJitteredGrid(graphWidth, graphHeight, spacing); // jittered square grid - grid.cellsX = Math.floor((graphWidth + 0.5 * spacing - 1e-10) / spacing); - grid.cellsY = Math.floor((graphHeight + 0.5 * spacing - 1e-10) / spacing); - TIME && console.timeEnd("placePoints"); -} - -// calculate Delaunay and then Voronoi diagram -function calculateVoronoi(graph, points) { - TIME && console.time("calculateDelaunay"); - const n = points.length; - const allPoints = points.concat(grid.boundary); - const delaunay = Delaunator.from(allPoints); - TIME && console.timeEnd("calculateDelaunay"); - - TIME && console.time("calculateVoronoi"); - const voronoi = new Voronoi(delaunay, allPoints, n); - graph.cells = voronoi.cells; - graph.cells.i = n < 65535 ? Uint16Array.from(d3.range(n)) : Uint32Array.from(d3.range(n)); // array of indexes - graph.vertices = voronoi.vertices; - TIME && console.timeEnd("calculateVoronoi"); -} - // Mark features (ocean, lakes, islands) and calculate distance field function markFeatures() { TIME && console.time("markFeatures"); Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode - const cells = grid.cells, - heights = grid.cells.h; + const cells = grid.cells; + const heights = grid.cells.h; cells.f = new Uint16Array(cells.i.length); // cell feature number cells.t = new Int8Array(cells.i.length); // cell type: 1 = land coast; -1 = water near coast grid.features = [0]; @@ -1201,8 +1161,8 @@ function generatePrecipitation() { // recalculate Voronoi Graph to pack cells function reGraph() { TIME && console.time("reGraph"); - let {cells, points, features} = grid; - const newCells = {p: [], g: [], h: []}; // to store new data + const {cells, points, features} = grid; + const newCells = {p: [], g: [], h: []}; // store new data const spacing2 = grid.spacing ** 2; for (const i of cells.i) { @@ -1236,14 +1196,17 @@ function reGraph() { newCells.h.push(height); } - calculateVoronoi(pack, newCells.p); - cells = pack.cells; - cells.p = newCells.p; // points coordinates [x, y] - cells.g = grid.cells.i.length < 65535 ? Uint16Array.from(newCells.g) : Uint32Array.from(newCells.g); // reference to initial grid cell - cells.q = d3.quadtree(cells.p.map((p, d) => [p[0], p[1], d])); // points quadtree for fast search - cells.h = new Uint8Array(newCells.h); // heights - cells.area = new Uint16Array(cells.i.length); // cell area - cells.i.forEach(i => (cells.area[i] = Math.abs(d3.polygonArea(getPackPolygon(i))))); + function getCellArea(i) { + const area = Math.abs(d3.polygonArea(getPackPolygon(i))); + return Math.min(area, 65535); + } + + pack = calculateVoronoi(newCells.p, grid.boundary); + pack.cells.p = newCells.p; + pack.cells.g = getTypedArray(grid.points.length).from(newCells.g); + pack.cells.q = d3.quadtree(newCells.p.map(([x, y], i) => [x, y, i])); + pack.cells.h = getTypedArray(100).from(newCells.h); + pack.cells.area = getTypedArray(65535).from(pack.cells.i).map(getCellArea); TIME && console.timeEnd("reGraph"); } @@ -1946,7 +1909,11 @@ function showStatistics() { const regenerateMap = debounce(async function (options) { WARN && console.warn("Generate new random map"); - showLoading(); + + const cellsDesired = +byId("pointsInput").dataset.cells; + const shouldShowLoading = cellsDesired > 10000; + + shouldShowLoading && showLoading(); closeDialogs("#worldConfigurator, #options3d"); customization = 0; resetZoom(1000); @@ -1955,7 +1922,7 @@ const regenerateMap = debounce(async function (options) { restoreLayers(); if (ThreeD.options.isOn) ThreeD.redraw(); if ($("#worldConfigurator").is(":visible")) editWorld(); - hideLoading(); + shouldShowLoading && hideLoading(); }, 1000); // clear the map diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js index 08b9c056..abeee814 100644 --- a/modules/dynamic/auto-update.js +++ b/modules/dynamic/auto-update.js @@ -520,4 +520,9 @@ export function resolveVersionConflicts(version) { if (!zone.dataset.type) zone.dataset.type = "Unknown"; }); } + + if (version < 1.84) { + // v1.84.0 added grid.cellsDesired to stored data + if (!grid.cellsDesired) grid.cellsDesired = rn((graphWidth * graphHeight) / grid.spacing ** 2, -3); + } } diff --git a/modules/dynamic/heightmap-selection.js b/modules/dynamic/heightmap-selection.js index 69b89815..90bfdec8 100644 --- a/modules/dynamic/heightmap-selection.js +++ b/modules/dynamic/heightmap-selection.js @@ -1,4 +1,6 @@ const initialSeed = generateSeed(); +let graph = getGraph(grid); + appendStyleSheet(); insertEditorHtml(); addListeners(); @@ -8,6 +10,7 @@ export function open() { const $templateInput = byId("templateInput"); setSelected($templateInput.value); + graph = getGraph(graph); $("#heightmapSelection").dialog({ title: "Select Heightmap", @@ -30,8 +33,7 @@ export function open() { lock("template"); const seed = getSeed(); - Math.random = aleaPRNG(seed); - regeneratePrompt({seed}); + regeneratePrompt({seed, graph}); $(this).dialog("close"); } @@ -182,13 +184,14 @@ function insertEditorHtml() { `; 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 = generateHeightmap(key); + const heights = HeightmapGenerator.fromTemplate(graph, key); const dataUrl = drawHeights(heights); return /* html */ `
@@ -220,12 +223,8 @@ function addListeners() { if (!article) return; const id = article.dataset.id; - if (event.target.matches("span.icon-cw")) { - const seed = generateSeed(); - article.dataset.seed = seed; - Math.random = aleaPRNG(seed); - drawTemplatePreview(id); - } else setSelected(id); + if (event.target.matches("span.icon-cw")) regeneratePreview(article, id); + setSelected(id); }); byId("heightmapSelectionRenderOcean").on("change", redrawAll); @@ -254,12 +253,18 @@ function getName(id) { 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 = grid.cellsX; - canvas.height = grid.cellsY; + canvas.width = graph.cellsX; + canvas.height = graph.cellsY; const ctx = canvas.getContext("2d"); - const imageData = ctx.createImageData(grid.cellsX, grid.cellsY); + const imageData = ctx.createImageData(graph.cellsX, graph.cellsY); const schemeId = byId("heightmapSelectionColorScheme").value; const scheme = getColorScheme(schemeId); @@ -281,32 +286,30 @@ function drawHeights(heights) { return canvas.toDataURL("image/png"); } -function generateHeightmap(id) { - const heights = new Uint8Array(grid.points.length); - // use cells number of the current graph, no matter what UI input value is - const cellsDesired = rn((graphWidth * graphHeight) / grid.spacing ** 2, -3); - - HeightmapGenerator.setHeights(heights, cellsDesired); - const newHeights = HeightmapGenerator.fromTemplate(id); - HeightmapGenerator.cleanup(); - return newHeights; -} - function drawTemplatePreview(id) { - const heights = generateHeightmap(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(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; diff --git a/modules/heightmap-generator.js b/modules/heightmap-generator.js index 9fd95cc1..feae9c1e 100644 --- a/modules/heightmap-generator.js +++ b/modules/heightmap-generator.js @@ -1,47 +1,48 @@ "use strict"; window.HeightmapGenerator = (function () { + let grid = null; let heights = null; let blobPower; let linePower; - const setHeights = (savedHeights, cellsNumber) => { - heights = savedHeights; - blobPower = getBlobPower(cellsNumber); - linePower = getLinePower(cellsNumber); + const setGraph = graph => { + const {cellsDesired, cells, points} = graph; + heights = cells.h || createTypedArray({maxValue: 100, length: points.length}); + blobPower = getBlobPower(cellsDesired); + linePower = getLinePower(cellsDesired); + grid = graph; }; - const resetHeights = () => { - heights = new Uint8Array(grid.points.length); - const cellsNumber = +byId("pointsInput").dataset.cells; - blobPower = getBlobPower(cellsNumber); - linePower = getLinePower(cellsNumber); - }; const getHeights = () => heights; - const cleanup = () => (heights = null); + const clearData = () => { + heights = null; + grid = null; + }; - const fromTemplate = template => { - const templateString = heightmapTemplates[template]?.template || ""; + const fromTemplate = (graph, id) => { + const templateString = heightmapTemplates[id]?.template || ""; const steps = templateString.split("\n"); - if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${template}. Steps: ${steps}`); + if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`); + setGraph(graph); for (const step of steps) { const elements = step.trim().split(" "); - if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${template}. Step: ${elements}`); + if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${id}. Step: ${elements}`); addStep(...elements); } return heights; }; - const fromPrecreated = id => { + const fromPrecreated = (graph, id) => { return new Promise(resolve => { // create canvas where 1px corresponts to a cell const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); - const {cellsX, cellsY} = grid; + const {cellsX, cellsY} = graph; canvas.width = cellsX; canvas.height = cellsY; @@ -51,7 +52,8 @@ window.HeightmapGenerator = (function () { img.onload = () => { ctx.drawImage(img, 0, 0, cellsX, cellsY); const imageData = ctx.getImageData(0, 0, cellsX, cellsY); - const heights = getHeightsFromImageData(imageData.data); + setGraph(graph); + getHeightsFromImageData(imageData.data); canvas.remove(); img.remove(); resolve(heights); @@ -59,18 +61,17 @@ window.HeightmapGenerator = (function () { }); }; - const generate = async function () { - Math.random = aleaPRNG(seed); - + const generate = async function (graph) { TIME && console.time("defineHeightmap"); const id = byId("templateInput").value; - resetHeights(); + Math.random = aleaPRNG(seed); const isTemplate = id in heightmapTemplates; - grid.cells.h = isTemplate ? fromTemplate(id) : await fromPrecreated(id); - - cleanup(); + const heights = isTemplate ? fromTemplate(graph, id) : await fromPrecreated(graph, id); TIME && console.timeEnd("defineHeightmap"); + + clearData(); + return heights; }; function addStep(tool, a2, a3, a4, a5) { @@ -141,7 +142,7 @@ window.HeightmapGenerator = (function () { do { const x = getPointInRange(rangeX, graphWidth); const y = getPointInRange(rangeY, graphHeight); - start = findGridCell(x, y); + start = findGridCell(x, y, grid); limit++; } while (heights[start] + h > 90 && limit < 50); @@ -177,7 +178,7 @@ window.HeightmapGenerator = (function () { do { const x = getPointInRange(rangeX, graphWidth); const y = getPointInRange(rangeY, graphHeight); - start = findGridCell(x, y); + start = findGridCell(x, y, grid); limit++; } while (heights[start] < 20 && limit < 50); @@ -223,7 +224,9 @@ window.HeightmapGenerator = (function () { limit++; } while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50); - let range = getRange(findGridCell(startX, startY), findGridCell(endX, endY)); + const startCell = findGridCell(startX, startY, grid); + const endCell = findGridCell(endX, endY, grid); + let range = getRange(startCell, endCell); // get main ridge function getRange(cur, end) { @@ -305,7 +308,7 @@ window.HeightmapGenerator = (function () { do { startX = getPointInRange(rangeX, graphWidth); startY = getPointInRange(rangeY, graphHeight); - start = findGridCell(startX, startY); + start = findGridCell(startX, startY, grid); limit++; } while (heights[start] < 20 && limit < 50); @@ -317,7 +320,7 @@ window.HeightmapGenerator = (function () { limit++; } while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50); - let range = getRange(start, findGridCell(endX, endY)); + let range = getRange(start, findGridCell(endX, endY, grid)); // get main ridge function getRange(cur, end) { @@ -388,8 +391,8 @@ window.HeightmapGenerator = (function () { const endX = vert ? Math.floor(graphWidth - startX - graphWidth * 0.1 + Math.random() * graphWidth * 0.2) : graphWidth - 5; const endY = vert ? graphHeight - 5 : Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2); - const start = findGridCell(startX, startY); - const end = findGridCell(endX, endY); + const start = findGridCell(startX, startY, grid); + const end = findGridCell(endX, endY, grid); let range = getRange(start, end); const query = []; @@ -502,20 +505,16 @@ window.HeightmapGenerator = (function () { } function getHeightsFromImageData(imageData) { - const heights = new Uint8Array(grid.points.length); for (let i = 0; i < heights.length; i++) { const lightness = imageData[i * 4] / 255; const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8; heights[i] = minmax(Math.floor(powered * 100), 0, 100); } - return heights; } return { - setHeights, - resetHeights, + setGraph, getHeights, - cleanup, generate, fromTemplate, fromPrecreated, diff --git a/modules/io/load.js b/modules/io/load.js index 30736043..5bd8c69d 100644 --- a/modules/io/load.js +++ b/modules/io/load.js @@ -324,7 +324,11 @@ async function parseLoadedData(data) { void (function parseGridData() { grid = JSON.parse(data[6]); - calculateVoronoi(grid, grid.points); + + const {cells, vertices} = calculateVoronoi(grid.points, grid.boundary); + grid.cells = cells; + grid.vertices = vertices; + grid.cells.h = Uint8Array.from(data[7].split(",")); grid.cells.prec = Uint8Array.from(data[8].split(",")); grid.cells.f = Uint16Array.from(data[9].split(",")); @@ -333,7 +337,6 @@ async function parseLoadedData(data) { })(); void (function parsePackData() { - pack = {}; reGraph(); reMarkFeatures(); pack.features = JSON.parse(data[12]); diff --git a/modules/io/save.js b/modules/io/save.js index 8e0a74e8..db303eff 100644 --- a/modules/io/save.js +++ b/modules/io/save.js @@ -54,8 +54,8 @@ function getMapData() { const serializedSVG = new XMLSerializer().serializeToString(cloneEl); - const {spacing, cellsX, cellsY, boundary, points, features} = grid; - const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features}); + const {spacing, cellsX, cellsY, boundary, points, features, cellsDesired} = grid; + const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features, cellsDesired}); const packFeatures = JSON.stringify(pack.features); const cultures = JSON.stringify(pack.cultures); const states = JSON.stringify(pack.states); diff --git a/modules/submap.js b/modules/submap.js index dde13a19..ad35e38b 100644 --- a/modules/submap.js +++ b/modules/submap.js @@ -41,8 +41,7 @@ window.Submap = (function () { // create new grid applyMapSize(); - placePoints(); - calculateVoronoi(grid, grid.points); + grid = generateGrid(); drawScaleBar(scale); const resampler = (points, qtree, f) => { diff --git a/modules/ui/general.js b/modules/ui/general.js index 7cb0f5c3..533b3f47 100644 --- a/modules/ui/general.js +++ b/modules/ui/general.js @@ -81,10 +81,10 @@ function handleMouseMove() { if (i === undefined) return; showNotes(d3.event); - const g = findGridCell(point[0], point[1]); // grid cell id + const gridCell = findGridCell(point[0], point[1], grid); if (tooltip.dataset.main) showMainTip(); - else showMapTooltip(point, d3.event, i, g); - if (cellInfo?.offsetParent) updateCellInfo(point, i, g); + else showMapTooltip(point, d3.event, i, gridCell); + if (cellInfo?.offsetParent) updateCellInfo(point, i, gridCell); } // show note box on hover (if any) @@ -244,7 +244,7 @@ function updateCellInfo(point, i, g) { infoCell.innerHTML = i; infoArea.innerHTML = cells.area[i] ? si(getArea(cells.area[i])) + " " + getAreaUnit() : "n/a"; infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]); - infoDepth.innerHTML = getDepth(pack.features[f], pack.cells.h[i], point); + infoDepth.innerHTML = getDepth(pack.features[f], point); infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]); infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a"; infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : "no"; @@ -276,11 +276,11 @@ function getElevation(f, h) { } // get water depth -function getDepth(f, h, p) { +function getDepth(f, p) { if (f.land) return "0 " + heightUnit.value; // land: 0 // lake: difference between surface and bottom - const gridH = grid.cells.h[findGridCell(p[0], p[1])]; + const gridH = grid.cells.h[findGridCell(p[0], p[1], grid)]; if (f.type === "lake") { const depth = gridH === 19 ? f.height / 2 : gridH; return getHeight(depth, "abs"); @@ -290,9 +290,9 @@ function getDepth(f, h, p) { } // get user-friendly (real-world) height value from map data -function getFriendlyHeight(p) { - const packH = pack.cells.h[findCell(p[0], p[1])]; - const gridH = grid.cells.h[findGridCell(p[0], p[1])]; +function getFriendlyHeight([x, y]) { + const packH = pack.cells.h[findCell(x, y, grid)]; + const gridH = grid.cells.h[findGridCell(x, y, grid)]; const h = packH < 20 ? gridH : packH; return getHeight(h); } diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index 23974ad4..aab80c48 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -117,7 +117,7 @@ function editHeightmap(options) { function moveCursor() { const [x, y] = d3.mouse(this); - const cell = findGridCell(x, y); + const cell = findGridCell(x, y, grid); heightmapInfoX.innerHTML = rn(x); heightmapInfoY.innerHTML = rn(y); heightmapInfoCell.innerHTML = cell; @@ -605,8 +605,8 @@ function editHeightmap(options) { function dragBrush() { const r = brushRadius.valueAsNumber; - const point = d3.mouse(this); - const start = findGridCell(point[0], point[1]); + const [x, y] = d3.mouse(this); + const start = findGridCell(x, y, grid); d3.event.on("drag", () => { const p = d3.mouse(this); @@ -664,7 +664,7 @@ function editHeightmap(options) { if (Number.isNaN(operand)) return tip("Operand should be a number", false, "error"); if ((operator === "add" || operator === "subtract") && !Number.isInteger(operand)) return tip("Operand should be an integer", false, "error"); - HeightmapGenerator.setHeights(grid.cells.h); + HeightmapGenerator.setGraph(grid); if (operator === "multiply") HeightmapGenerator.modify(range, 0, operand, 0); else if (operator === "divide") HeightmapGenerator.modify(range, 0, 1 / operand, 0); @@ -673,15 +673,13 @@ function editHeightmap(options) { else if (operator === "exponent") HeightmapGenerator.modify(range, 0, 1, operand); grid.cells.h = HeightmapGenerator.getHeights(); - HeightmapGenerator.cleanup(); updateHeightmap(); } function smoothAllHeights() { - HeightmapGenerator.setHeights(grid.cells.h); + HeightmapGenerator.setGraph(grid); HeightmapGenerator.smooth(4, 1.5); grid.cells.h = HeightmapGenerator.getHeights(); - HeightmapGenerator.cleanup(); updateHeightmap(); } @@ -940,11 +938,8 @@ function editHeightmap(options) { const seed = byId("templateSeed").value; if (seed) Math.random = aleaPRNG(seed); - const heights = new Uint8Array(grid.points.length); - // use cells number of the current graph, no matter what UI input value is - const cellsDesired = rn((graphWidth * graphHeight) / grid.spacing ** 2, -3); - HeightmapGenerator.setHeights(heights, cellsDesired); - + grid.cells.h = createTypedArray({maxValue: 100, length: grid.points.length}); + HeightmapGenerator.setGraph(grid); restartHistory(); for (const step of steps) { @@ -973,7 +968,6 @@ function editHeightmap(options) { } grid.cells.h = HeightmapGenerator.getHeights(); - HeightmapGenerator.cleanup(); updateStatistics(); mockHeightmap(); if (byId("preview")) drawHeightmapPreview(); // update heightmap preview if opened diff --git a/modules/ui/ice-editor.js b/modules/ui/ice-editor.js index 5951e7aa..f07cb6f9 100644 --- a/modules/ui/ice-editor.js +++ b/modules/ui/ice-editor.js @@ -65,8 +65,8 @@ function editIce() { } function addIcebergOnClick() { - const point = d3.mouse(this); - const i = findGridCell(point[0], point[1]); + const [x, y] = d3.mouse(this); + const i = findGridCell(x, y, grid); const c = grid.points[i]; const s = +document.getElementById("iceSize").value; diff --git a/utils/arrayUtils.js b/utils/arrayUtils.js index a7f0c35f..5fa9c81d 100644 --- a/utils/arrayUtils.js +++ b/utils/arrayUtils.js @@ -1,7 +1,5 @@ "use strict"; -// FMG utils related to arrays -// return the last element of array function last(array) { return array[array.length - 1]; } @@ -37,9 +35,24 @@ function deepCopy(obj) { [Set, s => [...s.values()].map(dcAny)], [Date, d => new Date(d.getTime())], [Object, dcObject] - // other types will be referenced // ... extend here to implement their custom deep copy ]); return dcAny(obj); } + +function getTypedArray(maxValue) { + console.assert( + Number.isInteger(maxValue) && maxValue >= 0 && maxValue <= 4294967295, + `Array maxValue must be an integer between 0 and 4294967295, got ${maxValue}` + ); + + if (maxValue <= 255) return Uint8Array; + if (maxValue <= 65535) return Uint16Array; + if (maxValue <= 4294967295) return Uint32Array; + return Uint32Array; +} + +function createTypedArray({maxValue, length}) { + return new (getTypedArray(maxValue))(length); +} diff --git a/utils/graphUtils.js b/utils/graphUtils.js index 3755f7c1..b345e286 100644 --- a/utils/graphUtils.js +++ b/utils/graphUtils.js @@ -1,7 +1,60 @@ "use strict"; // FMG utils related to graph -// add boundary points to pseudo-clip voronoi cells +// check if new grid graph should be generated or we can use the existing one +function shouldRegenerateGrid(grid) { + const cellsDesired = +byId("pointsInput").dataset.cells; + if (cellsDesired !== grid.cellsDesired) return true; + + const newSpacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); + const newCellsX = Math.floor((graphWidth + 0.5 * newSpacing - 1e-10) / newSpacing); + const newCellsY = Math.floor((graphHeight + 0.5 * newSpacing - 1e-10) / newSpacing); + + return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY; +} + +function generateGrid() { + Math.random = aleaPRNG(seed); // reset PRNG + const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints(); + const {cells, vertices} = calculateVoronoi(points, boundary); + return {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices}; +} + +// place random points to calculate Voronoi diagram +function placePoints() { + TIME && console.time("placePoints"); + const cellsDesired = +byId("pointsInput").dataset.cells; + const spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); // spacing between points before jirrering + + const boundary = getBoundaryPoints(graphWidth, graphHeight, spacing); + const points = getJitteredGrid(graphWidth, graphHeight, spacing); // points of jittered square grid + const cellsX = Math.floor((graphWidth + 0.5 * spacing - 1e-10) / spacing); + const cellsY = Math.floor((graphHeight + 0.5 * spacing - 1e-10) / spacing); + TIME && console.timeEnd("placePoints"); + + return {spacing, cellsDesired, boundary, points, cellsX, cellsY}; +} + +// calculate Delaunay and then Voronoi diagram +function calculateVoronoi(points, boundary) { + TIME && console.time("calculateDelaunay"); + const allPoints = points.concat(boundary); + const delaunay = Delaunator.from(allPoints); + TIME && console.timeEnd("calculateDelaunay"); + + TIME && console.time("calculateVoronoi"); + const n = points.length; + const voronoi = new Voronoi(delaunay, allPoints, n); + + const cells = voronoi.cells; + cells.i = getTypedArray(n).from(d3.range(n)); // array of indexes + const vertices = voronoi.vertices; + TIME && console.timeEnd("calculateVoronoi"); + + return {cells, vertices}; +} + +// add points along map edge to pseudo-clip voronoi cells function getBoundaryPoints(width, height, spacing) { const offset = rn(-1 * spacing); const bSpacing = spacing * 2; @@ -9,15 +62,18 @@ function getBoundaryPoints(width, height, spacing) { const h = height - offset * 2; const numberX = Math.ceil(w / bSpacing) - 1; const numberY = Math.ceil(h / bSpacing) - 1; - let points = []; + const points = []; + for (let i = 0.5; i < numberX; i++) { let x = Math.ceil((w * i) / numberX + offset); points.push([x, offset], [x, h + offset]); } + for (let i = 0.5; i < numberY; i++) { let y = Math.ceil((h * i) / numberY + offset); points.push([offset, y], [w + offset, y]); } + return points; } @@ -40,7 +96,7 @@ function getJitteredGrid(width, height, spacing) { } // return cell index on a regular square grid -function findGridCell(x, y) { +function findGridCell(x, y, grid) { return Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX + Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1)); } @@ -48,7 +104,7 @@ function findGridCell(x, y) { function findGridAll(x, y, radius) { const c = grid.cells.c; let r = Math.floor(radius / grid.spacing); - let found = [findGridCell(x, y)]; + let found = [findGridCell(x, y, grid)]; if (!r || radius === 1) return found; if (r > 0) found = found.concat(c[found[0]]); if (r > 1) {