diff --git a/modules/dynamic/heightmap-selection.js b/modules/dynamic/heightmap-selection.js index d1e9d000..271ef5ac 100644 --- a/modules/dynamic/heightmap-selection.js +++ b/modules/dynamic/heightmap-selection.js @@ -41,6 +41,8 @@ const heightmaps = [ {id: "world-from-pacific", name: "World from Pacific"} ]; +let seed = Math.floor(Math.random() * 1e9); + appendStyleSheet(); insertEditorHtml(); addListeners(); @@ -109,11 +111,18 @@ function appendStyleSheet() { color: #000; } + .heightmap-selection article > div > span.icon-cw:active { + color: #666; + } + .heightmap-selection article > img { width: 100%; aspect-ratio: 16/9; border-radius: 8px; object-fit: cover; + } + + img.heightmap-selection_precreated { filter: contrast(1.3); } `; @@ -126,8 +135,12 @@ function appendStyleSheet() { function insertEditorHtml() { const templatesHtml = templates .map(({id, name}) => { - return /* html */ `
- ${name} + Math.random = aleaPRNG(seed); + const heights = HeightmapGenerator.fromTemplate(id); + const dataUrl = drawHeights(heights); + + return /* html */ `
+ ${name}
${name} @@ -139,7 +152,7 @@ function insertEditorHtml() { const heightmapsHtml = heightmaps .map(({id, name}) => { return /* html */ `
- ${name} + ${name}
${name}
`; }) @@ -168,7 +181,11 @@ function insertEditorHtml() { function addListeners() { byId("heightmapSelection").on("click", event => { const article = event.target.closest("#heightmapSelection article"); - if (article) setSelected(article.dataset.id); + if (!article) return; + + const id = article.dataset.id; + if (event.target.matches("span.icon-cw")) regeneratePreview(article, id); + else setSelected(id); }); } @@ -181,3 +198,33 @@ function setSelected(id) { $heightmapSelection.querySelector(".selected")?.classList?.remove("selected"); $heightmapSelection.querySelector(`[data-id="${id}"]`)?.classList?.add("selected"); } + +function drawHeights(heights) { + const canvas = document.createElement("canvas"); + canvas.width = grid.cellsX; + canvas.height = grid.cellsY; + const ctx = canvas.getContext("2d"); + const imageData = ctx.createImageData(grid.cellsX, grid.cellsY); + + heights.forEach((height, i) => { + const h = height < 20 ? Math.max(height / 1.5, 0) : height; + const v = (h / 100) * 255; + imageData.data[i * 4] = v; + imageData.data[i * 4 + 1] = v; + imageData.data[i * 4 + 2] = v; + imageData.data[i * 4 + 3] = 255; + }); + + ctx.putImageData(imageData, 0, 0); + return canvas.toDataURL("image/png"); +} + +function regeneratePreview(article, id) { + seed = Math.floor(Math.random() * 1e9); + article.dataset.seed = seed; + Math.random = aleaPRNG(seed); + + const heights = HeightmapGenerator.fromTemplate(id); + const dataUrl = drawHeights(heights); + article.querySelector("img").src = dataUrl; +} diff --git a/modules/heightmap-generator.js b/modules/heightmap-generator.js index 762350ce..d7ff0ff5 100644 --- a/modules/heightmap-generator.js +++ b/modules/heightmap-generator.js @@ -1,12 +1,12 @@ "use strict"; window.HeightmapGenerator = (function () { - let cells, p; + let cells, p, heights; const generate = async function () { cells = grid.cells; p = grid.points; - cells.h = new Uint8Array(grid.points.length); + heights = new Uint8Array(grid.points.length); const input = document.getElementById("templateInput"); const selectedId = input.selectedIndex >= 0 ? input.selectedIndex : 0; @@ -32,6 +32,9 @@ window.HeightmapGenerator = (function () { assignColorsToHeight(imageData.data); canvas.remove(); img.remove(); + + cells.h = heights; + heights = null; TIME && console.timeEnd("defineHeightmap"); resolve(); }; @@ -52,9 +55,28 @@ window.HeightmapGenerator = (function () { addStep(...elements); } + cells.h = heights; + heights = null; TIME && console.timeEnd("generateHeightmap"); }; + const fromTemplate = template => { + const templateString = HeightmapTemplates[template]; + const steps = templateString.split("\n"); + + if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${template}. Steps: ${steps}`); + + heights = new Uint8Array(grid.points.length); + + 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}`); + addStep(...elements); + } + + return heights; + }; + function addStep(a1, a2, a3, a4, a5) { if (a1 === "Hill") return addHill(a2, a3, a4, a5); if (a1 === "Pit") return addPit(a2, a3, a4, a5); @@ -110,7 +132,7 @@ window.HeightmapGenerator = (function () { } function addOneHill() { - const change = new Uint8Array(cells.h.length); + const change = new Uint8Array(heights.length); let limit = 0; let start; let h = lim(getNumberInRange(height)); @@ -120,7 +142,7 @@ window.HeightmapGenerator = (function () { const y = getPointInRange(rangeY, graphHeight); start = findGridCell(x, y); limit++; - } while (cells.h[start] + h > 90 && limit < 50); + } while (heights[start] + h > 90 && limit < 50); change[start] = h; const queue = [start]; @@ -134,7 +156,7 @@ window.HeightmapGenerator = (function () { } } - cells.h = cells.h.map((h, i) => lim(h + change[i])); + heights = heights.map((h, i) => lim(h + change[i])); } }; @@ -146,7 +168,7 @@ window.HeightmapGenerator = (function () { } function addOnePit() { - const used = new Uint8Array(cells.h.length); + const used = new Uint8Array(heights.length); let limit = 0, start; let h = lim(getNumberInRange(height)); @@ -156,7 +178,7 @@ window.HeightmapGenerator = (function () { const y = getPointInRange(rangeY, graphHeight); start = findGridCell(x, y); limit++; - } while (cells.h[start] < 20 && limit < 50); + } while (heights[start] < 20 && limit < 50); const queue = [start]; while (queue.length) { @@ -166,7 +188,7 @@ window.HeightmapGenerator = (function () { cells.c[q].forEach(function (c, i) { 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; queue.push(c); }); @@ -183,7 +205,7 @@ window.HeightmapGenerator = (function () { } function addOneRange() { - const used = new Uint8Array(cells.h.length); + const used = new Uint8Array(heights.length); let h = lim(getNumberInRange(height)); // find start and end points @@ -234,7 +256,7 @@ window.HeightmapGenerator = (function () { const frontier = queue.slice(); (queue = []), 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; if (h < 2) break; @@ -252,8 +274,8 @@ window.HeightmapGenerator = (function () { range.forEach((cur, d) => { if (d % 6 !== 0) return; 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 - cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3; + const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell + heights[min] = (heights[cur] * 2 + heights[min]) / 3; cur = min; } }); @@ -269,7 +291,7 @@ window.HeightmapGenerator = (function () { } function addOneTrough() { - const used = new Uint8Array(cells.h.length); + const used = new Uint8Array(heights.length); let h = lim(getNumberInRange(height)); // find start and end points @@ -285,7 +307,7 @@ window.HeightmapGenerator = (function () { startY = getPointInRange(rangeY, graphHeight); start = findGridCell(startX, startY); limit++; - } while (cells.h[start] < 20 && limit < 50); + } while (heights[start] < 20 && limit < 50); limit = 0; do { @@ -328,7 +350,7 @@ window.HeightmapGenerator = (function () { const frontier = queue.slice(); (queue = []), 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; if (h < 2) break; @@ -346,9 +368,9 @@ window.HeightmapGenerator = (function () { range.forEach((cur, d) => { if (d % 6 !== 0) return; 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 = cells.c[cur][d3.scan(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); - cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3; + heights[min] = (heights[cur] * 2 + heights[min]) / 3; cur = min; } }); @@ -358,7 +380,7 @@ window.HeightmapGenerator = (function () { const addStrait = (width, direction = "vertical") => { width = Math.min(getNumberInRange(width), grid.cellsX / 3); if (width < 1 && P(width)) return; - const used = new Uint8Array(cells.h.length); + const used = new Uint8Array(heights.length); const vert = direction === "vertical"; 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); @@ -398,8 +420,8 @@ window.HeightmapGenerator = (function () { if (used[e]) return; used[e] = 1; query.push(e); - cells.h[e] **= exp; - if (cells.h[e] > 100) cells.h[e] = 5; + heights[e] **= exp; + if (heights[e] > 100) heights[e] = 5; }); }); range = query.slice(); @@ -413,7 +435,7 @@ window.HeightmapGenerator = (function () { const max = range === "land" || range === "all" ? 100 : +range.split("-")[1]; const isLand = min === 20; - grid.cells.h = grid.cells.h.map(h => { + heights = heights.map(h => { if (h < min || h > max) return h; if (add) h = isLand ? Math.max(h + add, 20) : h + add; @@ -424,9 +446,9 @@ window.HeightmapGenerator = (function () { }; const smooth = (fr = 2, add = 0) => { - cells.h = cells.h.map((h, i) => { + heights = heights.map((h, i) => { const a = [h]; - cells.c[i].forEach(c => a.push(cells.h[c])); + cells.c[i].forEach(c => a.push(heights[c])); if (fr === 1) return d3.mean(a) + add; return lim((h * (fr - 1) + d3.mean(a) + add) / fr); }); @@ -435,7 +457,7 @@ window.HeightmapGenerator = (function () { const mask = (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 nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center const ny = (2 * y) / graphHeight - 1; // [-1, 1], 0 is center @@ -453,17 +475,17 @@ window.HeightmapGenerator = (function () { const invertY = axes !== "x"; const {cellsX, cellsY} = grid; - const inverted = cells.h.map((h, i) => { + const inverted = heights.map((h, i) => { const x = i % cellsX; const y = Math.floor(i / cellsX); const nx = invertX ? cellsX - x - 1 : x; const ny = invertY ? cellsY - y - 1 : y; const invertedI = nx + ny * cellsX; - return cells.h[invertedI]; + return heights[invertedI]; }); - cells.h = inverted; + heights = inverted; }; function getPointInRange(range, length) { @@ -481,9 +503,9 @@ window.HeightmapGenerator = (function () { for (let i = 0; i < cells.i.length; i++) { const lightness = imageData[i * 4] / 255; 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 {generate, fromTemplate, addHill, addRange, addTrough, addStrait, addPit, smooth, modify, mask, invert}; })(); diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index 12d8b283..5345d4fa 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -1360,7 +1360,7 @@ function editHeightmap() { const imageData = ctx.createImageData(grid.cellsX, grid.cellsY); 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; imageData.data[i * 4] = v; imageData.data[i * 4 + 1] = v;