From 4a96c24cf304a622d1dbcbe007d933416f41fa9e Mon Sep 17 00:00:00 2001 From: Azgaar Date: Mon, 31 Oct 2022 00:24:54 +0300 Subject: [PATCH] feat: heightmap brushes - linear edit option (1.88.00) --- index.css | 5 -- index.html | 42 ++++++++++-- modules/heightmap-generator.js | 96 ++++++++++++++------------ modules/ui/heightmap-editor.js | 121 +++++++++++++++++++++++++-------- modules/ui/hotkeys.js | 10 +-- utils/probabilityUtils.js | 2 +- versioning.js | 3 +- 7 files changed, 194 insertions(+), 85 deletions(-) diff --git a/index.css b/index.css index 1cb69877..58e92533 100644 --- a/index.css +++ b/index.css @@ -1303,11 +1303,6 @@ div.slider .ui-slider-handle { color: white; } -#brushPower, -#brushRadius { - width: 12em; -} - #rescaleHigher, #rescaleLower, #rescaleModifier { diff --git a/index.html b/index.html index 946c53d1..ae7345ee 100644 --- a/index.html +++ b/index.html @@ -108,7 +108,7 @@ } - + @@ -3836,6 +3836,12 @@ + + + +
- + @@ -7804,7 +7836,7 @@ - + @@ -7834,7 +7866,7 @@ - + @@ -7864,7 +7896,7 @@ - + diff --git a/modules/heightmap-generator.js b/modules/heightmap-generator.js index 66696953..5a69337e 100644 --- a/modules/heightmap-generator.js +++ b/modules/heightmap-generator.js @@ -8,7 +8,7 @@ window.HeightmapGenerator = (function () { const setGraph = graph => { const {cellsDesired, cells, points} = graph; - heights = cells.h || createTypedArray({maxValue: 100, length: points.length}); + heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({maxValue: 100, length: points.length}); blobPower = getBlobPower(cellsDesired); linePower = getLinePower(cellsDesired); grid = graph; @@ -198,7 +198,8 @@ window.HeightmapGenerator = (function () { } }; - const addRange = (count, height, rangeX, rangeY) => { + // fromCell, toCell are options cell ids + const addRange = (count, height, rangeX, rangeY, startCell, endCell) => { count = getNumberInRange(count); while (count > 0) { addOneRange(); @@ -209,23 +210,27 @@ window.HeightmapGenerator = (function () { const used = new Uint8Array(heights.length); let h = lim(getNumberInRange(height)); - // find start and end points - const startX = getPointInRange(rangeX, graphWidth); - const startY = getPointInRange(rangeY, graphHeight); + if (rangeX && rangeY) { + // find start and end points + const startX = getPointInRange(rangeX, graphWidth); + const startY = getPointInRange(rangeY, graphHeight); - let dist = 0, - limit = 0, - endX, - endY; - do { - endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1; - endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15; - dist = Math.abs(endY - startY) + Math.abs(endX - startX); - limit++; - } while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50); + let dist = 0, + limit = 0, + endX, + endY; + + do { + endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1; + endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15; + dist = Math.abs(endY - startY) + Math.abs(endX - startX); + limit++; + } while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50); + + startCell = findGridCell(startX, startY, grid); + endCell = findGridCell(endX, endY, grid); + } - const startCell = findGridCell(startX, startY, grid); - const endCell = findGridCell(endX, endY, grid); let range = getRange(startCell, endCell); // get main ridge @@ -286,7 +291,7 @@ window.HeightmapGenerator = (function () { } }; - const addTrough = (count, height, rangeX, rangeY) => { + const addTrough = (count, height, rangeX, rangeY, startCell, endCell) => { count = getNumberInRange(count); while (count > 0) { addOneTrough(); @@ -297,30 +302,33 @@ window.HeightmapGenerator = (function () { const used = new Uint8Array(heights.length); let h = lim(getNumberInRange(height)); - // find start and end points - let limit = 0, - startX, - startY, - start, - dist = 0, - endX, - endY; - do { - startX = getPointInRange(rangeX, graphWidth); - startY = getPointInRange(rangeY, graphHeight); - start = findGridCell(startX, startY, grid); - limit++; - } while (heights[start] < 20 && limit < 50); + if (rangeX && rangeY) { + // find start and end points + let limit = 0, + startX, + startY, + dist = 0, + endX, + endY; + do { + startX = getPointInRange(rangeX, graphWidth); + startY = getPointInRange(rangeY, graphHeight); + startCell = findGridCell(startX, startY, grid); + limit++; + } while (heights[startCell] < 20 && limit < 50); - limit = 0; - do { - endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1; - endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15; - dist = Math.abs(endY - startY) + Math.abs(endX - startX); - limit++; - } while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50); + limit = 0; + do { + endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1; + endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15; + dist = Math.abs(endY - startY) + Math.abs(endX - startX); + limit++; + } while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50); - let range = getRange(start, findGridCell(endX, endY, grid)); + endCell = findGridCell(endX, endY, grid); + } + + let range = getRange(startCell, endCell); // get main ridge function getRange(cur, end) { @@ -388,8 +396,12 @@ window.HeightmapGenerator = (function () { 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); - 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 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, grid); const end = findGridCell(endX, endY, grid); diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index f72afd13..7f83354a 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -130,6 +130,12 @@ function editHeightmap(options) { // move radius circle if drag mode is active const pressed = byId("brushesButtons").querySelector("button.pressed"); if (!pressed) return; + + if (pressed.id === "brushLine") { + debug.select("line").attr("x2", x).attr("y2", y); + return; + } + moveCircle(x, y, brushRadius.valueAsNumber, "#333"); } @@ -181,7 +187,6 @@ function editHeightmap(options) { else if (mode === "risk") restoreRiskedData(); // restore initial layers - //viewbox.select("#heights").remove(); byId("heights").remove(); turnButtonOff("toggleHeight"); document @@ -494,10 +499,8 @@ function editHeightmap(options) { selection.forEach(function (i) { let cell = viewbox.select("#heights").select("#cell" + i); - if (!ocean && grid.cells.h[i] < 20) { - cell.remove(); - return; - } + if (!ocean && grid.cells.h[i] < 20) return cell.remove(); + if (!cell.size()) cell = viewbox .select("#heights") @@ -594,24 +597,81 @@ function editHeightmap(options) { function exitBrushMode() { const pressed = document.querySelector("#brushesButtons > button.pressed"); - if (!pressed) return; - pressed.classList.remove("pressed"); + if (pressed) pressed.classList.remove("pressed"); - viewbox.style("cursor", "default").on(".drag", null); + viewbox.style("cursor", "default").on(".drag", null).on("click", clicked); + debug.selectAll(".lineCircle").remove(); removeCircle(); + byId("brushesSliders").style.display = "none"; + byId("lineSlider").style.display = "none"; } const dragBrushThrottled = throttle(dragBrush, 100); - function toggleBrushMode(e) { - if (e.target.classList.contains("pressed")) { + + function toggleBrushMode(event) { + if (event.target.classList.contains("pressed")) { exitBrushMode(); return; } + exitBrushMode(); - byId("brushesSliders").style.display = "block"; - e.target.classList.add("pressed"); - viewbox.style("cursor", "crosshair").call(d3.drag().on("start", dragBrushThrottled)); + event.target.classList.add("pressed"); + + if (event.target.id === "brushLine") { + byId("lineSlider").style.display = "block"; + viewbox.style("cursor", "crosshair").on("click", placeLinearFeature); + } else { + byId("brushesSliders").style.display = "block"; + viewbox.style("cursor", "crosshair").call(d3.drag().on("start", dragBrushThrottled)); + } + } + + function placeLinearFeature() { + const [x, y] = d3.mouse(this); + const toCell = findGridCell(x, y, grid); + + const lineCircle = debug.selectAll(".lineCircle"); + if (!lineCircle.size()) { + // first click: add 1st control point + debug.append("line").attr("id", "brushCircle").attr("x1", x).attr("y1", y).attr("x2", x).attr("y2", y); + + debug + .append("circle") + .attr("data-cell", toCell) + .attr("class", "lineCircle") + .attr("r", 6) + .attr("cx", x) + .attr("cy", y) + .attr("fill", "yellow") + .attr("stroke", "#333") + .attr("stroke-width", 2); + return; + } + + // second click: execute operation and remove control points + const fromCell = +lineCircle.attr("data-cell"); + debug.selectAll("*").remove(); + + const power = byId("linePower").valueAsNumber; + if (power === 0) return tip("Power should not be zero", false, "error"); + + const heights = grid.cells.h; + const operation = power > 0 ? HeightmapGenerator.addRange : HeightmapGenerator.addTrough; + HeightmapGenerator.setGraph(grid); + operation("1", String(Math.abs(power)), null, null, fromCell, toCell); + const changedHeights = HeightmapGenerator.getHeights(); + + let selection = []; + for (let i = 0; i < heights.length; i++) { + if (changedHeights[i] === heights[i]) continue; + if (changeOnlyLand.checked && heights[i] < 20) continue; + heights[i] = changedHeights[i]; + selection.push(i); + } + + mockHeightmapSelection(selection); + updateHistory(); } function dragBrush() { @@ -632,37 +692,44 @@ function editHeightmap(options) { d3.event.on("end", updateHeightmap); } - function changeHeightForSelection(s, start) { + function changeHeightForSelection(selection, start) { const power = brushPower.valueAsNumber; + const interpolate = d3.interpolateRound(power, 1); const land = changeOnlyLand.checked; const lim = v => minmax(v, land ? 20 : 0, 100); - const h = grid.cells.h; + const heights = grid.cells.h; const brush = document.querySelector("#brushesButtons > button.pressed").id; - if (brush === "brushRaise") s.forEach(i => (h[i] = h[i] < 20 ? 20 : lim(h[i] + power))); + if (brush === "brushRaise") selection.forEach(i => (heights[i] = heights[i] < 20 ? 20 : lim(heights[i] + power))); else if (brush === "brushElevate") - s.forEach((i, d) => (h[i] = lim(h[i] + interpolate(d / Math.max(s.length - 1, 1))))); - else if (brush === "brushLower") s.forEach(i => (h[i] = lim(h[i] - power))); + selection.forEach( + (i, d) => (heights[i] = lim(heights[i] + interpolate(d / Math.max(selection.length - 1, 1)))) + ); + else if (brush === "brushLower") selection.forEach(i => (heights[i] = lim(heights[i] - power))); else if (brush === "brushDepress") - s.forEach((i, d) => (h[i] = lim(h[i] - interpolate(d / Math.max(s.length - 1, 1))))); - else if (brush === "brushAlign") s.forEach(i => (h[i] = lim(h[start]))); + selection.forEach( + (i, d) => (heights[i] = lim(heights[i] - interpolate(d / Math.max(selection.length - 1, 1)))) + ); + else if (brush === "brushAlign") selection.forEach(i => (heights[i] = lim(heights[start]))); else if (brush === "brushSmooth") - s.forEach( + selection.forEach( i => - (h[i] = rn( - (d3.mean(grid.cells.c[i].filter(i => (land ? h[i] >= 20 : 1)).map(c => h[c])) + - h[i] * (10 - power) + + (heights[i] = rn( + (d3.mean(grid.cells.c[i].filter(i => (land ? heights[i] >= 20 : 1)).map(c => heights[c])) + + heights[i] * (10 - power) + 0.6) / (11 - power), 1 )) ); else if (brush === "brushDisrupt") - s.forEach(i => (h[i] = h[i] < 15 ? h[i] : lim(h[i] + power / 1.6 - Math.random() * power))); + selection.forEach( + i => (heights[i] = heights[i] < 15 ? heights[i] : lim(heights[i] + power / 1.6 - Math.random() * power)) + ); - mockHeightmapSelection(s); - // updateHistory(); uncomment to update history every step + mockHeightmapSelection(selection); + // updateHistory(); uncomment to update history on every step } function changeOnlyLandClick(e) { diff --git a/modules/ui/hotkeys.js b/modules/ui/hotkeys.js index d8fcaaf8..f7443469 100644 --- a/modules/ui/hotkeys.js +++ b/modules/ui/hotkeys.js @@ -89,13 +89,13 @@ function handleKeyup(event) { else if (code === "KeyI") toggleIcons(); else if (code === "KeyM") toggleMilitary(); else if (code === "KeyK") toggleMarkers(); - else if (code === "Equal") toggleRulers(); + else if (code === "Equal" && !customization) toggleRulers(); else if (code === "Slash") toggleScaleBar(); else if (code === "ArrowLeft") zoom.translateBy(svg, 10, 0); else if (code === "ArrowRight") zoom.translateBy(svg, -10, 0); else if (code === "ArrowUp") zoom.translateBy(svg, 0, 10); else if (code === "ArrowDown") zoom.translateBy(svg, 0, -10); - else if (key === "+" || key === "-") pressNumpadSign(key); + else if (key === "+" || key === "-" || key === "=") handleSizeChange(key); else if (key === "0") resetZoom(1000); else if (key === "1") zoom.scaleTo(svg, 1); else if (key === "2") zoom.scaleTo(svg, 2); @@ -117,11 +117,12 @@ function allowHotkeys() { return true; } -function pressNumpadSign(key) { - const change = key === "+" ? 1 : -1; +// "+", "-" and "=" keys on numpad. "=" is for "+" on Mac +function handleSizeChange(key) { let brush = null; if (document.getElementById("brushRadius")?.offsetParent) brush = document.getElementById("brushRadius"); + else if (document.getElementById("linePower")?.offsetParent) brush = document.getElementById("linePower"); else if (document.getElementById("biomesManuallyBrush")?.offsetParent) brush = document.getElementById("biomesManuallyBrush"); else if (document.getElementById("statesManuallyBrush")?.offsetParent) @@ -135,6 +136,7 @@ function pressNumpadSign(key) { brush = document.getElementById("religionsManuallyBrush"); if (brush) { + const change = key === "-" ? -5 : 5; const value = minmax(+brush.value + change, +brush.min, +brush.max); brush.value = document.getElementById(brush.id + "Number").value = value; return; diff --git a/utils/probabilityUtils.js b/utils/probabilityUtils.js index 7759e330..b0234251 100644 --- a/utils/probabilityUtils.js +++ b/utils/probabilityUtils.js @@ -56,7 +56,7 @@ function biased(min, max, ex) { // get number from string in format "1-3" or "2" or "0.5" function getNumberInRange(r) { if (typeof r !== "string") { - ERROR && console.error("The value should be a string", r); + ERROR && console.error("Range value should be a string", r); return 0; } if (!isNaN(+r)) return ~~r + +P(r - ~~r); diff --git a/versioning.js b/versioning.js index ea45eff7..6dac46df 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.87.15"; // generator version, update each time +const version = "1.88.00"; // generator version, update each time { document.title += " v" + version; @@ -28,6 +28,7 @@ const version = "1.87.15"; // generator version, update each time
    Latest changes: +
  • Heightmap brushes: linear edit option
  • Data Charts screen
  • Сultures and religions can have multiple parents in hierarchy tree
  • Heightmap selection screen