diff --git a/public/modules/heightmap-generator.js b/public/modules/heightmap-generator.js
deleted file mode 100644
index 87bc02d5..00000000
--- a/public/modules/heightmap-generator.js
+++ /dev/null
@@ -1,543 +0,0 @@
-"use strict";
-
-window.HeightmapGenerator = (function () {
- let grid = null;
- let heights = null;
- let blobPower;
- let linePower;
-
- const setGraph = graph => {
- const {cellsDesired, cells, points} = graph;
- heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({maxValue: 100, length: points.length});
- blobPower = getBlobPower(cellsDesired);
- linePower = getLinePower(cellsDesired);
- grid = graph;
- };
-
- const getHeights = () => heights;
-
- const clearData = () => {
- heights = null;
- grid = null;
- };
-
- 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: ${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: ${id}. Step: ${elements}`);
- addStep(...elements);
- }
-
- return heights;
- };
-
- 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} = graph;
- canvas.width = cellsX;
- canvas.height = cellsY;
-
- // load heightmap into image and render to canvas
- const img = new Image();
- img.src = `./heightmaps/${id}.png`;
- img.onload = () => {
- ctx.drawImage(img, 0, 0, cellsX, cellsY);
- const imageData = ctx.getImageData(0, 0, cellsX, cellsY);
- setGraph(graph);
- getHeightsFromImageData(imageData.data);
- canvas.remove();
- img.remove();
- resolve(heights);
- };
- });
- };
-
- const generate = async function (graph) {
- TIME && console.time("defineHeightmap");
- const id = byId("templateInput").value;
-
- Math.random = aleaPRNG(seed);
- const isTemplate = id in heightmapTemplates;
- const heights = isTemplate ? fromTemplate(graph, id) : await fromPrecreated(graph, id);
- TIME && console.timeEnd("defineHeightmap");
-
- clearData();
- return heights;
- };
-
- function addStep(tool, a2, a3, a4, a5) {
- if (tool === "Hill") return addHill(a2, a3, a4, a5);
- if (tool === "Pit") return addPit(a2, a3, a4, a5);
- if (tool === "Range") return addRange(a2, a3, a4, a5);
- if (tool === "Trough") return addTrough(a2, a3, a4, a5);
- if (tool === "Strait") return addStrait(a2, a3);
- if (tool === "Mask") return mask(a2);
- if (tool === "Invert") return invert(a2, a3);
- if (tool === "Add") return modify(a3, +a2, 1);
- if (tool === "Multiply") return modify(a3, 0, +a2);
- if (tool === "Smooth") return smooth(a2);
- }
-
- function getBlobPower(cells) {
- const blobPowerMap = {
- 1000: 0.93,
- 2000: 0.95,
- 5000: 0.97,
- 10000: 0.98,
- 20000: 0.99,
- 30000: 0.991,
- 40000: 0.993,
- 50000: 0.994,
- 60000: 0.995,
- 70000: 0.9955,
- 80000: 0.996,
- 90000: 0.9964,
- 100000: 0.9973
- };
- return blobPowerMap[cells] || 0.98;
- }
-
- function getLinePower(cells) {
- const linePowerMap = {
- 1000: 0.75,
- 2000: 0.77,
- 5000: 0.79,
- 10000: 0.81,
- 20000: 0.82,
- 30000: 0.83,
- 40000: 0.84,
- 50000: 0.86,
- 60000: 0.87,
- 70000: 0.88,
- 80000: 0.91,
- 90000: 0.92,
- 100000: 0.93
- };
-
- return linePowerMap[cells] || 0.81;
- }
-
- const addHill = (count, height, rangeX, rangeY) => {
- count = getNumberInRange(count);
- while (count > 0) {
- addOneHill();
- count--;
- }
-
- function addOneHill() {
- const change = new Uint8Array(heights.length);
- let limit = 0;
- let start;
- let h = lim(getNumberInRange(height));
-
- do {
- const x = getPointInRange(rangeX, graphWidth);
- const y = getPointInRange(rangeY, graphHeight);
- start = findGridCell(x, y, grid);
- limit++;
- } while (heights[start] + h > 90 && limit < 50);
-
- change[start] = h;
- const queue = [start];
- while (queue.length) {
- const q = queue.shift();
-
- for (const c of grid.cells.c[q]) {
- if (change[c]) continue;
- change[c] = change[q] ** blobPower * (Math.random() * 0.2 + 0.9);
- if (change[c] > 1) queue.push(c);
- }
- }
-
- heights = heights.map((h, i) => lim(h + change[i]));
- }
- };
-
- const addPit = (count, height, rangeX, rangeY) => {
- count = getNumberInRange(count);
- while (count > 0) {
- addOnePit();
- count--;
- }
-
- function addOnePit() {
- const used = new Uint8Array(heights.length);
- let limit = 0,
- start;
- let h = lim(getNumberInRange(height));
-
- do {
- const x = getPointInRange(rangeX, graphWidth);
- const y = getPointInRange(rangeY, graphHeight);
- start = findGridCell(x, y, grid);
- limit++;
- } while (heights[start] < 20 && limit < 50);
-
- const queue = [start];
- while (queue.length) {
- const q = queue.shift();
- h = h ** blobPower * (Math.random() * 0.2 + 0.9);
- if (h < 1) return;
-
- grid.cells.c[q].forEach(function (c, i) {
- if (used[c]) return;
- heights[c] = lim(heights[c] - h * (Math.random() * 0.2 + 0.9));
- used[c] = 1;
- queue.push(c);
- });
- }
- }
- };
-
- // fromCell, toCell are options cell ids
- const addRange = (count, height, rangeX, rangeY, startCell, endCell) => {
- count = getNumberInRange(count);
- while (count > 0) {
- addOneRange();
- count--;
- }
-
- function addOneRange() {
- const used = new Uint8Array(heights.length);
- let h = lim(getNumberInRange(height));
-
- 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);
-
- startCell = findGridCell(startX, startY, grid);
- endCell = findGridCell(endX, endY, grid);
- }
-
- let range = getRange(startCell, endCell);
-
- // get main ridge
- function getRange(cur, end) {
- const range = [cur];
- const p = grid.points;
- used[cur] = 1;
-
- while (cur !== end) {
- let min = Infinity;
- grid.cells.c[cur].forEach(function (e) {
- if (used[e]) return;
- let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
- if (Math.random() > 0.85) diff = diff / 2;
- if (diff < min) {
- min = diff;
- cur = e;
- }
- });
- if (min === Infinity) return range;
- range.push(cur);
- used[cur] = 1;
- }
-
- return range;
- }
-
- // add height to ridge and cells around
- let queue = range.slice(),
- i = 0;
- while (queue.length) {
- const frontier = queue.slice();
- (queue = []), i++;
- frontier.forEach(i => {
- heights[i] = lim(heights[i] + h * (Math.random() * 0.3 + 0.85));
- });
- h = h ** linePower - 1;
- if (h < 2) break;
- frontier.forEach(f => {
- grid.cells.c[f].forEach(i => {
- if (!used[i]) {
- queue.push(i);
- used[i] = 1;
- }
- });
- });
- }
-
- // generate prominences
- range.forEach((cur, d) => {
- if (d % 6 !== 0) return;
- for (const l of d3.range(i)) {
- const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
- heights[min] = (heights[cur] * 2 + heights[min]) / 3;
- cur = min;
- }
- });
- }
- };
-
- const addTrough = (count, height, rangeX, rangeY, startCell, endCell) => {
- count = getNumberInRange(count);
- while (count > 0) {
- addOneTrough();
- count--;
- }
-
- function addOneTrough() {
- const used = new Uint8Array(heights.length);
- let h = lim(getNumberInRange(height));
-
- 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);
-
- endCell = findGridCell(endX, endY, grid);
- }
-
- let range = getRange(startCell, endCell);
-
- // get main ridge
- function getRange(cur, end) {
- const range = [cur];
- const p = grid.points;
- used[cur] = 1;
-
- while (cur !== end) {
- let min = Infinity;
- grid.cells.c[cur].forEach(function (e) {
- if (used[e]) return;
- let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
- if (Math.random() > 0.8) diff = diff / 2;
- if (diff < min) {
- min = diff;
- cur = e;
- }
- });
- if (min === Infinity) return range;
- range.push(cur);
- used[cur] = 1;
- }
-
- return range;
- }
-
- // add height to ridge and cells around
- let queue = range.slice(),
- i = 0;
- while (queue.length) {
- const frontier = queue.slice();
- (queue = []), i++;
- frontier.forEach(i => {
- heights[i] = lim(heights[i] - h * (Math.random() * 0.3 + 0.85));
- });
- h = h ** linePower - 1;
- if (h < 2) break;
- frontier.forEach(f => {
- grid.cells.c[f].forEach(i => {
- if (!used[i]) {
- queue.push(i);
- used[i] = 1;
- }
- });
- });
- }
-
- // generate prominences
- range.forEach((cur, d) => {
- if (d % 6 !== 0) return;
- for (const l of d3.range(i)) {
- const min = grid.cells.c[cur][d3.scan(grid.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);
- heights[min] = (heights[cur] * 2 + heights[min]) / 3;
- cur = min;
- }
- });
- }
- };
-
- const addStrait = (width, direction = "vertical") => {
- width = Math.min(getNumberInRange(width), grid.cellsX / 3);
- if (width < 1 && P(width)) return;
- 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);
- 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);
- let range = getRange(start, end);
- const query = [];
-
- function getRange(cur, end) {
- const range = [];
- const p = grid.points;
-
- while (cur !== end) {
- let min = Infinity;
- grid.cells.c[cur].forEach(function (e) {
- let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
- if (Math.random() > 0.8) diff = diff / 2;
- if (diff < min) {
- min = diff;
- cur = e;
- }
- });
- range.push(cur);
- }
-
- return range;
- }
-
- const step = 0.1 / width;
-
- while (width > 0) {
- const exp = 0.9 - step * width;
- range.forEach(function (r) {
- grid.cells.c[r].forEach(function (e) {
- if (used[e]) return;
- used[e] = 1;
- query.push(e);
- heights[e] **= exp;
- if (heights[e] > 100) heights[e] = 5;
- });
- });
- range = query.slice();
-
- width--;
- }
- };
-
- const modify = (range, add, mult, power) => {
- const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
- const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
- const isLand = min === 20;
-
- heights = heights.map(h => {
- if (h < min || h > max) return h;
-
- if (add) h = isLand ? Math.max(h + add, 20) : h + add;
- if (mult !== 1) h = isLand ? (h - 20) * mult + 20 : h * mult;
- if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
- return lim(h);
- });
- };
-
- const smooth = (fr = 2, add = 0) => {
- heights = heights.map((h, i) => {
- const a = [h];
- grid.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);
- });
- };
-
- const mask = (power = 1) => {
- const fr = power ? Math.abs(power) : 1;
-
- heights = heights.map((h, i) => {
- const [x, y] = grid.points[i];
- const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center
- const ny = (2 * y) / graphHeight - 1; // [-1, 1], 0 is center
- let distance = (1 - nx ** 2) * (1 - ny ** 2); // 1 is center, 0 is edge
- if (power < 0) distance = 1 - distance; // inverted, 0 is center, 1 is edge
- const masked = h * distance;
- return lim((h * (fr - 1) + masked) / fr);
- });
- };
-
- const invert = (count, axes) => {
- if (!P(count)) return;
-
- const invertX = axes !== "y";
- const invertY = axes !== "x";
- const {cellsX, cellsY} = grid;
-
- 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 heights[invertedI];
- });
-
- heights = inverted;
- };
-
- function getPointInRange(range, length) {
- if (typeof range !== "string") {
- ERROR && console.error("Range should be a string");
- return;
- }
-
- const min = range.split("-")[0] / 100 || 0;
- const max = range.split("-")[1] / 100 || min;
- return rand(min * length, max * length);
- }
-
- function getHeightsFromImageData(imageData) {
- 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 {
- setGraph,
- getHeights,
- generate,
- fromTemplate,
- fromPrecreated,
- addHill,
- addRange,
- addTrough,
- addStrait,
- addPit,
- smooth,
- modify,
- mask,
- invert
- };
-})();
diff --git a/src/index.html b/src/index.html
index b5fbf8e6..9e892b63 100644
--- a/src/index.html
+++ b/src/index.html
@@ -8465,11 +8465,10 @@
+
-
-
diff --git a/src/modules/heightmap-generator.ts b/src/modules/heightmap-generator.ts
new file mode 100644
index 00000000..eb48f9f4
--- /dev/null
+++ b/src/modules/heightmap-generator.ts
@@ -0,0 +1,582 @@
+import Alea from "alea";
+import { range as d3Range, leastIndex, mean } from "d3";
+import { createTypedArray, byId, findGridCell, getNumberInRange, lim, minmax, P, rand } from "../utils";
+
+declare global {
+ interface Window {
+ HeightmapGenerator: HeightmapGenerator;
+ }
+ var heightmapTemplates: any;
+ var TIME: boolean;
+ var ERROR: boolean;
+}
+
+type Tool = "Hill" | "Pit" | "Range" | "Trough" | "Strait" | "Mask" | "Invert" | "Add" | "Multiply" | "Smooth";
+
+class HeightmapGenerator {
+ grid: any = null;
+ heights: Uint8Array | null = null;
+ blobPower: number = 0;
+ linePower: number = 0;
+
+ // TODO: remove after migration to TS and use param in constructor
+ get seed() {
+ return (window as any).seed;
+ }
+ get graphWidth() {
+ return (window as any).graphWidth;
+ }
+ get graphHeight() {
+ return (window as any).graphHeight;
+ }
+
+ constructor() {
+
+ }
+
+ private clearData() {
+ this.heights = null;
+ this.grid = null;
+ };
+
+
+ private getBlobPower(cells: number): number {
+ const blobPowerMap: Record = {
+ 1000: 0.93,
+ 2000: 0.95,
+ 5000: 0.97,
+ 10000: 0.98,
+ 20000: 0.99,
+ 30000: 0.991,
+ 40000: 0.993,
+ 50000: 0.994,
+ 60000: 0.995,
+ 70000: 0.9955,
+ 80000: 0.996,
+ 90000: 0.9964,
+ 100000: 0.9973
+ };
+ return blobPowerMap[cells] || 0.98;
+ }
+
+ private getLinePower(cells: number): number {
+ const linePowerMap: Record = {
+ 1000: 0.75,
+ 2000: 0.77,
+ 5000: 0.79,
+ 10000: 0.81,
+ 20000: 0.82,
+ 30000: 0.83,
+ 40000: 0.84,
+ 50000: 0.86,
+ 60000: 0.87,
+ 70000: 0.88,
+ 80000: 0.91,
+ 90000: 0.92,
+ 100000: 0.93
+ };
+
+ return linePowerMap[cells] || 0.81;
+ }
+
+ private getPointInRange(range: string, length: number): number | undefined {
+ if (typeof range !== "string") {
+ window.ERROR && console.error("Range should be a string");
+ return;
+ }
+
+ const min = parseInt(range.split("-")[0]) / 100 || 0;
+ const max = parseInt(range.split("-")[1]) / 100 || min;
+ return rand(min * length, max * length);
+ }
+
+ setGraph(graph: any) {
+ const {cellsDesired, cells, points} = graph;
+ this.heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({maxValue: 100, length: points.length}) as Uint8Array;
+ this.blobPower = this.getBlobPower(cellsDesired);
+ this.linePower = this.getLinePower(cellsDesired);
+ this.grid = graph;
+ };
+
+ addHill(count: string, height: string, rangeX: string, rangeY: string): void {
+ const addOneHill = () => {
+ if(!this.heights || !this.grid) return;
+ const change = new Uint8Array(this.heights.length);
+ let limit = 0;
+ let start: number;
+ let h = lim(getNumberInRange(height));
+
+ do {
+ const x = this.getPointInRange(rangeX, this.graphWidth);
+ const y = this.getPointInRange(rangeY, this.graphHeight);
+ if (x === undefined || y === undefined) return;
+ start = findGridCell(x, y, this.grid);
+ limit++;
+ } while (this.heights[start] + h > 90 && limit < 50);
+ change[start] = h;
+ const queue = [start];
+ while (queue.length) {
+ const q = queue.shift() as number;
+
+ for (const c of this.grid.cells.c[q]) {
+ if (change[c]) continue;
+ change[c] = change[q] ** this.blobPower * (Math.random() * 0.2 + 0.9);
+ if (change[c] > 1) queue.push(c);
+ }
+ }
+
+ this.heights = this.heights.map((h, i) => lim(h + change[i]));
+ }
+
+ const desiredHillCount = getNumberInRange(count);
+ for (let i = 0; i < desiredHillCount; i++) {
+ addOneHill();
+ }
+ };
+
+ addPit(count: string, height: string, rangeX: string, rangeY: string): void {
+ const addOnePit = () => {
+ if(!this.heights || !this.grid) return;
+ const used = new Uint8Array(this.heights.length);
+ let limit = 0;
+ let start: number;
+ let h = lim(getNumberInRange(height));
+
+ do {
+ const x = this.getPointInRange(rangeX, this.graphWidth);
+ const y = this.getPointInRange(rangeY, this.graphHeight);
+ if (x === undefined || y === undefined) return;
+ start = findGridCell(x, y, this.grid);
+ limit++;
+ } while (this.heights[start] < 20 && limit < 50);
+
+ const queue = [start];
+ while (queue.length) {
+ const q = queue.shift() as number;
+ h = h ** this.blobPower * (Math.random() * 0.2 + 0.9);
+ if (h < 1) return;
+
+ this.grid.cells.c[q].forEach((c: number) => {
+ if (used[c] || this.heights === null) return;
+ this.heights[c] = lim(this.heights[c] - h * (Math.random() * 0.2 + 0.9));
+ used[c] = 1;
+ queue.push(c);
+ });
+ }
+ }
+
+ const desiredPitCount = getNumberInRange(count);
+ for (let i = 0; i < desiredPitCount; i++) {
+ addOnePit();
+ }
+ };
+
+ addRange(count: string, height: string, rangeX: string, rangeY: string, startCellId?: number, endCellId?: number): void {
+ if(!this.heights || !this.grid) return;
+
+ const addOneRange = () => {
+ if(!this.heights || !this.grid) return;
+
+ // get main ridge
+ const getRange = (cur: number, end: number) => {
+ const range = [cur];
+ const p = this.grid.points;
+ used[cur] = 1;
+
+ while (cur !== end) {
+ let min = Infinity;
+ this.grid.cells.c[cur].forEach((e: number) => {
+ if (used[e]) return;
+ let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
+ if (Math.random() > 0.85) diff = diff / 2;
+ if (diff < min) {
+ min = diff;
+ cur = e;
+ }
+ });
+ if (min === Infinity) return range;
+ range.push(cur);
+ used[cur] = 1;
+ }
+
+ return range;
+ }
+
+ const used = new Uint8Array(this.heights.length);
+ let h = lim(getNumberInRange(height));
+
+ if (rangeX && rangeY) {
+ // find start and end points
+ const startX = this.getPointInRange(rangeX, this.graphWidth) as number;
+ const startY = this.getPointInRange(rangeY, this.graphHeight) as number;
+
+ let dist = 0;
+ let limit = 0;
+ let endY;
+ let endX;
+
+ do {
+ endX = Math.random() * this.graphWidth * 0.8 + this.graphWidth * 0.1;
+ endY = Math.random() * this.graphHeight * 0.7 + this.graphHeight * 0.15;
+ dist = Math.abs(endY - startY) + Math.abs(endX - startX);
+ limit++;
+ } while ((dist < this.graphWidth / 8 || dist > this.graphWidth / 3) && limit < 50);
+
+ startCellId = findGridCell(startX, startY, this.grid);
+ endCellId = findGridCell(endX, endY, this.grid);
+ }
+
+ let range = getRange(startCellId as number, endCellId as number);
+
+
+ // add height to ridge and cells around
+ let queue = range.slice();
+ let i = 0;
+ while (queue.length) {
+ const frontier = queue.slice();
+ (queue = []), i++;
+ frontier.forEach((i: number) => {
+ if(!this.heights) return;
+ this.heights[i] = lim(this.heights[i] + h * (Math.random() * 0.3 + 0.85));
+ });
+ h = h ** this.linePower - 1;
+ if (h < 2) break;
+ frontier.forEach((f: number) => {
+ this.grid.cells.c[f].forEach((i: number) => {
+ if (!used[i]) {
+ queue.push(i);
+ used[i] = 1;
+ }
+ });
+ });
+ }
+
+ // generate prominences
+ range.forEach((cur: number, d: number) => {
+ if (d % 6 !== 0) return;
+ for (const _l of d3Range(i)) {
+ const index = leastIndex(this.grid.cells.c[cur], (a: number, b: number) => this.heights![a] - this.heights![b]);
+ if(index === undefined) continue;
+ const min = this.grid.cells.c[cur][index]; // downhill cell
+ this.heights![min] = (this.heights![cur] * 2 + this.heights![min]) / 3;
+ cur = min;
+ }
+ });
+ }
+
+ const desiredRangeCount = getNumberInRange(count);
+ for (let i = 0; i < desiredRangeCount; i++) {
+ addOneRange();
+ }
+ };
+
+ addTrough(count: string, height: string, rangeX: string, rangeY: string, startCellId?: number, endCellId?: number): void {
+ const addOneTrough = () => {
+ if(!this.heights || !this.grid) return;
+
+ // get main ridge
+ const getRange = (cur: number, end: number) => {
+ const range = [cur];
+ const p = this.grid.points;
+ used[cur] = 1;
+
+ while (cur !== end) {
+ let min = Infinity;
+ this.grid.cells.c[cur].forEach((e: number) => {
+ if (used[e]) return;
+ let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
+ if (Math.random() > 0.8) diff = diff / 2;
+ if (diff < min) {
+ min = diff;
+ cur = e;
+ }
+ });
+ if (min === Infinity) return range;
+ range.push(cur);
+ used[cur] = 1;
+ }
+
+ return range;
+ }
+
+ const used = new Uint8Array(this.heights.length);
+ let h = lim(getNumberInRange(height));
+
+ if (rangeX && rangeY) {
+ // find start and end points
+ let limit = 0;
+ let startX: number;
+ let startY: number;
+ let dist = 0;
+ let endX: number;
+ let endY: number;
+ do {
+ startX = this.getPointInRange(rangeX, this.graphWidth) as number;
+ startY = this.getPointInRange(rangeY, this.graphHeight) as number;
+ startCellId = findGridCell(startX, startY, this.grid);
+ limit++;
+ } while (this.heights[startCellId] < 20 && limit < 50);
+
+ limit = 0;
+ do {
+ endX = Math.random() * this.graphWidth * 0.8 + this.graphWidth * 0.1;
+ endY = Math.random() * this.graphHeight * 0.7 + this.graphHeight * 0.15;
+ dist = Math.abs(endY - startY) + Math.abs(endX - startX);
+ limit++;
+ } while ((dist < this.graphWidth / 8 || dist > this.graphWidth / 2) && limit < 50);
+
+ endCellId = findGridCell(endX, endY, this.grid);
+ }
+
+ let range = getRange(startCellId as number, endCellId as number);
+
+
+ // add height to ridge and cells around
+ let queue = range.slice(),
+ i = 0;
+ while (queue.length) {
+ const frontier = queue.slice();
+ (queue = []), i++;
+ frontier.forEach((i: number) => {
+ this.heights![i] = lim(this.heights![i] - h * (Math.random() * 0.3 + 0.85));
+ });
+ h = h ** this.linePower - 1;
+ if (h < 2) break;
+ frontier.forEach((f: number) => {
+ this.grid.cells.c[f].forEach((i: number) => {
+ if (!used[i]) {
+ queue.push(i);
+ used[i] = 1;
+ }
+ });
+ });
+ }
+
+ // generate prominences
+ range.forEach((cur: number, d: number) => {
+ if (d % 6 !== 0) return;
+ for (const _l of d3Range(i)) {
+ const index = leastIndex(this.grid.cells.c[cur], (a: number, b: number) => this.heights![a] - this.heights![b]);
+ if(index === undefined) continue;
+ const min = this.grid.cells.c[cur][index]; // downhill cell
+ //debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
+ this.heights![min] = (this.heights![cur] * 2 + this.heights![min]) / 3;
+ cur = min;
+ }
+ });
+ }
+
+ const desiredTroughCount = getNumberInRange(count);
+ for(let i = 0; i < desiredTroughCount; i++) {
+ addOneTrough();
+ }
+ };
+
+ addStrait(width: string, direction = "vertical"): void {
+ if(!this.heights || !this.grid) return;
+ const desiredWidth = Math.min(getNumberInRange(width), this.grid.cellsX / 3);
+ if (desiredWidth < 1 && P(desiredWidth)) return;
+ const used = new Uint8Array(this.heights.length);
+ const vert = direction === "vertical";
+ const startX = vert ? Math.floor(Math.random() * this.graphWidth * 0.4 + this.graphWidth * 0.3) : 5;
+ const startY = vert ? 5 : Math.floor(Math.random() * this.graphHeight * 0.4 + this.graphHeight * 0.3);
+ const endX = vert
+ ? Math.floor(this.graphWidth - startX - this.graphWidth * 0.1 + Math.random() * this.graphWidth * 0.2)
+ : this.graphWidth - 5;
+ const endY = vert
+ ? this.graphHeight - 5
+ : Math.floor(this.graphHeight - startY - this.graphHeight * 0.1 + Math.random() * this.graphHeight * 0.2);
+
+ const start = findGridCell(startX, startY, this.grid);
+ const end = findGridCell(endX, endY, this.grid);
+
+ const getRange = (cur: number, end: number) => {
+ const range = [];
+ const p = this.grid.points;
+
+ while (cur !== end) {
+ let min = Infinity;
+ this.grid.cells.c[cur].forEach((e: number) => {
+ let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
+ if (Math.random() > 0.8) diff = diff / 2;
+ if (diff < min) {
+ min = diff;
+ cur = e;
+ }
+ });
+ range.push(cur);
+ }
+
+ return range;
+ }
+ let range = getRange(start, end);
+ const query: number[] = [];
+
+
+ const step = 0.1 / desiredWidth;
+
+ for(let i = 0; i < desiredWidth; i++) {
+ const exp = 0.9 - step * desiredWidth;
+ range.forEach((r: number) => {
+ this.grid.cells.c[r].forEach((e: number) => {
+ if (used[e]) return;
+ used[e] = 1;
+ query.push(e);
+ this.heights![e] **= exp;
+ if (this.heights![e] > 100) this.heights![e] = 5;
+ });
+ });
+ range = query.slice();
+ }
+ };
+
+ modify(range: string, add: number, mult: number, power?: number): void {
+ if(!this.heights) return;
+ const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
+ const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
+ const isLand = min === 20;
+
+ this.heights = this.heights.map(h => {
+ if (h < min || h > max) return h;
+
+ if (add) h = isLand ? Math.max(h + add, 20) : h + add;
+ if (mult !== 1) h = isLand ? (h - 20) * mult + 20 : h * mult;
+ if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
+ return lim(h);
+ });
+ };
+
+ smooth(fr = 2, add = 0): void {
+ if(!this.heights || !this.grid) return;
+ this.heights = this.heights.map((h, i) => {
+ const a = [h];
+ this.grid.cells.c[i].forEach((c: number) => a.push(this.heights![c]));
+ if (fr === 1) return (mean(a) as number) + add;
+ return lim((h * (fr - 1) + (mean(a) as number) + add) / fr);
+ });
+ };
+
+ mask(power = 1): void {
+ if(!this.heights || !this.grid) return;
+ const fr = power ? Math.abs(power) : 1;
+
+ this.heights = this.heights.map((h, i) => {
+ const [x, y] = this.grid.points[i];
+ const nx = (2 * x) / this.graphWidth - 1; // [-1, 1], 0 is center
+ const ny = (2 * y) / this.graphHeight - 1; // [-1, 1], 0 is center
+ let distance = (1 - nx ** 2) * (1 - ny ** 2); // 1 is center, 0 is edge
+ if (power < 0) distance = 1 - distance; // inverted, 0 is center, 1 is edge
+ const masked = h * distance;
+ return lim((h * (fr - 1) + masked) / fr);
+ });
+ };
+
+ invert(count: number, axes: string): void {
+ if (!P(count) || !this.heights || !this.grid) return;
+
+ const invertX = axes !== "y";
+ const invertY = axes !== "x";
+ const {cellsX, cellsY} = this.grid;
+
+ const inverted = this.heights.map((_h: number, i: number) => {
+ if(!this.heights) return 0;
+ 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 this.heights[invertedI];
+ });
+
+ this.heights = inverted;
+ };
+
+ addStep(tool: Tool, a2: string, a3: string, a4: string, a5: string): void {
+ if (tool === "Hill") return this.addHill(a2, a3, a4, a5);
+ if (tool === "Pit") return this.addPit(a2, a3, a4, a5);
+ if (tool === "Range") return this.addRange(a2, a3, a4, a5);
+ if (tool === "Trough") return this.addTrough(a2, a3, a4, a5);
+ if (tool === "Strait") return this.addStrait(a2, a3);
+ if (tool === "Mask") return this.mask(+a2);
+ if (tool === "Invert") return this.invert(+a2, a3);
+ if (tool === "Add") return this.modify(a3, +a2, 1);
+ if (tool === "Multiply") return this.modify(a3, 0, +a2);
+ if (tool === "Smooth") return this.smooth(+a2);
+ }
+
+ async generate(graph: any): Promise {
+ TIME && console.time("defineHeightmap");
+ const id = (byId("templateInput")! as HTMLInputElement).value;
+
+ Math.random = Alea(this.seed);
+ const isTemplate = id in heightmapTemplates;
+
+ const heights = isTemplate ? this.fromTemplate(graph, id) : await this.fromPrecreated(graph, id);
+ TIME && console.timeEnd("defineHeightmap");
+
+ this.clearData();
+ return heights as Uint8Array;
+ }
+
+ fromTemplate(graph: any, id: string): Uint8Array | null {
+ const templateString = heightmapTemplates[id]?.template || "";
+ const steps = templateString.split("\n");
+
+ if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`);
+ this.setGraph(graph);
+
+ for (const step of steps) {
+ const elements = step.trim().split(" ");
+ if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${id}. Step: ${elements}`);
+ this.addStep(...elements as [Tool, string, string, string, string]);
+ }
+
+ return this.heights;
+ };
+
+ private getHeightsFromImageData(imageData: Uint8ClampedArray): void {
+ if(!this.heights) return;
+ for (let i = 0; i < this.heights.length; i++) {
+ const lightness = imageData[i * 4] / 255;
+ const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8;
+ this.heights[i] = minmax(Math.floor(powered * 100), 0, 100);
+ }
+ }
+
+ fromPrecreated(graph: any, id: string): Promise {
+ return new Promise(resolve => {
+ // create canvas where 1px corresponds to a cell
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
+ const {cellsX, cellsY} = graph;
+ canvas.width = cellsX;
+ canvas.height = cellsY;
+
+ // load heightmap into image and render to canvas
+ const img = new Image();
+ img.src = `./heightmaps/${id}.png`;
+ img.onload = () => {
+ if(!ctx) {
+ throw new Error("Could not get canvas context");
+ }
+ if(!this.heights) {
+ throw new Error("Heights array is not initialized");
+ }
+ ctx.drawImage(img, 0, 0, cellsX, cellsY);
+ const imageData = ctx.getImageData(0, 0, cellsX, cellsY);
+ this.setGraph(graph);
+ this.getHeightsFromImageData(imageData.data);
+ canvas.remove();
+ img.remove();
+ resolve(this.heights);
+ };
+ });
+ };
+
+ getHeights() {
+ return this.heights;
+ }
+}
+
+window.HeightmapGenerator = new HeightmapGenerator();
\ No newline at end of file
diff --git a/src/modules/index.ts b/src/modules/index.ts
new file mode 100644
index 00000000..fe1135c0
--- /dev/null
+++ b/src/modules/index.ts
@@ -0,0 +1,2 @@
+import "./voronoi";
+import "./heightmap-generator";
\ No newline at end of file
diff --git a/public/modules/voronoi.js b/src/modules/voronoi.ts
similarity index 60%
rename from public/modules/voronoi.js
rename to src/modules/voronoi.ts
index 6c504014..55ac77ab 100644
--- a/public/modules/voronoi.js
+++ b/src/modules/voronoi.ts
@@ -1,17 +1,27 @@
-class Voronoi {
- /**
- * Creates a Voronoi diagram from the given Delaunator, a list of points, and the number of points. The Voronoi diagram is constructed using (I think) the {@link https://en.wikipedia.org/wiki/Bowyer%E2%80%93Watson_algorithm |Bowyer-Watson Algorithm}
- * The {@link https://github.com/mapbox/delaunator/ |Delaunator} library uses {@link https://en.wikipedia.org/wiki/Doubly_connected_edge_list |half-edges} to represent the relationship between points and triangles.
- * @param {{triangles: Uint32Array, halfedges: Int32Array}} delaunay A {@link https://github.com/mapbox/delaunator/blob/master/index.js |Delaunator} instance.
- * @param {[number, number][]} points A list of coordinates.
- * @param {number} pointsN The number of points.
- */
- constructor(delaunay, points, pointsN) {
+import Delaunator from "delaunator";
+export type Vertices = { p: Point[], v: number[][], c: number[][] };
+export type Cells = { v: number[][], c: number[][], b: number[], i: Uint32Array } ;
+export type Point = [number, number];
+
+/**
+ * Creates a Voronoi diagram from the given Delaunator, a list of points, and the number of points. The Voronoi diagram is constructed using (I think) the {@link https://en.wikipedia.org/wiki/Bowyer%E2%80%93Watson_algorithm |Bowyer-Watson Algorithm}
+ * The {@link https://github.com/mapbox/delaunator/ |Delaunator} library uses {@link https://en.wikipedia.org/wiki/Doubly_connected_edge_list |half-edges} to represent the relationship between points and triangles.
+ * @param {{triangles: Uint32Array, halfedges: Int32Array}} delaunay A {@link https://github.com/mapbox/delaunator/blob/master/index.js |Delaunator} instance.
+ * @param {[number, number][]} points A list of coordinates.
+ * @param {number} pointsN The number of points.
+ */
+export class Voronoi {
+ delaunay: Delaunator>
+ points: Point[];
+ pointsN: number;
+ cells: Cells = { v: [], c: [], b: [], i: new Uint32Array() }; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell, i = cell indexes;
+ vertices: Vertices = { p: [], v: [], c: [] }; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
+
+ constructor(delaunay: Delaunator>, points: Point[], pointsN: number) {
this.delaunay = delaunay;
this.points = points;
this.pointsN = pointsN;
- this.cells = { v: [], c: [], b: [] }; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
- this.vertices = { p: [], v: [], c: [] }; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
+ this.vertices
// Half-edges are the indices into the delaunator outputs:
// delaunay.triangles[e] gives the point ID where the half-edge starts
@@ -40,18 +50,18 @@ class Voronoi {
* @param {number} t The index of the triangle
* @returns {[number, number, number]} The IDs of the points comprising the given triangle.
*/
- pointsOfTriangle(t) {
- return this.edgesOfTriangle(t).map(edge => this.delaunay.triangles[edge]);
+ private pointsOfTriangle(triangleIndex: number): [number, number, number] {
+ return this.edgesOfTriangle(triangleIndex).map(edge => this.delaunay.triangles[edge]) as [number, number, number];
}
/**
* Identifies what triangles are adjacent to the given triangle. Taken from {@link https://mapbox.github.io/delaunator/#triangle-to-triangles| the Delaunator docs.}
- * @param {number} t The index of the triangle
+ * @param {number} triangleIndex The index of the triangle
* @returns {number[]} The indices of the triangles that share half-edges with this triangle.
*/
- trianglesAdjacentToTriangle(t) {
+ private trianglesAdjacentToTriangle(triangleIndex: number): number[] {
let triangles = [];
- for (let edge of this.edgesOfTriangle(t)) {
+ for (let edge of this.edgesOfTriangle(triangleIndex)) {
let opposite = this.delaunay.halfedges[edge];
triangles.push(this.triangleOfEdge(opposite));
}
@@ -61,9 +71,9 @@ class Voronoi {
/**
* Gets the indices of all the incoming and outgoing half-edges that touch the given point. Taken from {@link https://mapbox.github.io/delaunator/#point-to-edges| the Delaunator docs.}
* @param {number} start The index of an incoming half-edge that leads to the desired point
- * @returns {number[]} The indices of all half-edges (incoming or outgoing) that touch the point.
+ * @returns {[number, number, number]} The indices of all half-edges (incoming or outgoing) that touch the point.
*/
- edgesAroundPoint(start) {
+ private edgesAroundPoint(start: number): [number, number, number] {
const result = [];
let incoming = start;
do {
@@ -71,46 +81,46 @@ class Voronoi {
const outgoing = this.nextHalfedge(incoming);
incoming = this.delaunay.halfedges[outgoing];
} while (incoming !== -1 && incoming !== start && result.length < 20);
- return result;
+ return result as [number, number, number];
}
/**
* Returns the center of the triangle located at the given index.
- * @param {number} t The index of the triangle
- * @returns {[number, number]}
+ * @param {number} triangleIndex The index of the triangle
+ * @returns {[number, number]} The coordinates of the triangle's circumcenter.
*/
- triangleCenter(t) {
- let vertices = this.pointsOfTriangle(t).map(p => this.points[p]);
+ private triangleCenter(triangleIndex: number): Point {
+ let vertices = this.pointsOfTriangle(triangleIndex).map(p => this.points[p]);
return this.circumcenter(vertices[0], vertices[1], vertices[2]);
}
/**
- * Retrieves all of the half-edges for a specific triangle `t`. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
- * @param {number} t The index of the triangle
+ * Retrieves all of the half-edges for a specific triangle `triangleIndex`. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
+ * @param {number} triangleIndex The index of the triangle
* @returns {[number, number, number]} The edges of the triangle.
*/
- edgesOfTriangle(t) { return [3 * t, 3 * t + 1, 3 * t + 2]; }
+ private edgesOfTriangle(triangleIndex: number): [number, number, number] { return [3 * triangleIndex, 3 * triangleIndex + 1, 3 * triangleIndex + 2]; }
/**
* Enables lookup of a triangle, given one of the half-edges of that triangle. Taken from {@link https://mapbox.github.io/delaunator/#edge-and-triangle| the Delaunator docs.}
* @param {number} e The index of the edge
* @returns {number} The index of the triangle
*/
- triangleOfEdge(e) { return Math.floor(e / 3); }
+ private triangleOfEdge(e: number): number { return Math.floor(e / 3); }
/**
* Moves to the next half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
* @param {number} e The index of the current half edge
* @returns {number} The index of the next half edge
*/
- nextHalfedge(e) { return (e % 3 === 2) ? e - 2 : e + 1; }
+ private nextHalfedge(e: number): number { return (e % 3 === 2) ? e - 2 : e + 1; }
/**
* Moves to the previous half-edge of a triangle, given the current half-edge's index. Taken from {@link https://mapbox.github.io/delaunator/#edge-to-edges| the Delaunator docs.}
* @param {number} e The index of the current half edge
* @returns {number} The index of the previous half edge
*/
- prevHalfedge(e) { return (e % 3 === 0) ? e + 2 : e - 1; }
+ // private prevHalfedge(e: number): number { return (e % 3 === 0) ? e + 2 : e - 1; }
/**
* Finds the circumcenter of the triangle identified by points a, b, and c. Taken from {@link https://en.wikipedia.org/wiki/Circumscribed_circle#Circumcenter_coordinates| Wikipedia}
@@ -119,7 +129,7 @@ class Voronoi {
* @param {[number, number]} c The coordinates of the third point of the triangle
* @return {[number, number]} The coordinates of the circumcenter of the triangle.
*/
- circumcenter(a, b, c) {
+ private circumcenter(a: Point, b: Point, c: Point): Point {
const [ax, ay] = a;
const [bx, by] = b;
const [cx, cy] = c;
@@ -132,6 +142,4 @@ class Voronoi {
Math.floor(1 / D * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax)))
];
}
-}
-
-window.Voronoi = Voronoi;
+}
\ No newline at end of file
diff --git a/src/utils/arrayUtils.ts b/src/utils/arrayUtils.ts
index 5772cb14..ad2f9486 100644
--- a/src/utils/arrayUtils.ts
+++ b/src/utils/arrayUtils.ts
@@ -78,7 +78,7 @@ export const getTypedArray = (maxValue: number) => {
* @param {Array} [options.from] - An optional array to create the typed array from
* @returns The created typed array
*/
-export const createTypedArray = ({maxValue, length, from}: {maxValue: number; length: number; from?: ArrayLike}) => {
+export const createTypedArray = ({maxValue, length, from}: {maxValue: number; length: number; from?: ArrayLike}): Uint8Array | Uint16Array | Uint32Array => {
const typedArray = getTypedArray(maxValue);
if (!from) return new typedArray(length);
return typedArray.from(from);
diff --git a/src/utils/graphUtils.ts b/src/utils/graphUtils.ts
index 9b756624..875445fb 100644
--- a/src/utils/graphUtils.ts
+++ b/src/utils/graphUtils.ts
@@ -4,6 +4,7 @@ import { color } from "d3";
import { byId } from "./shorthands";
import { rn } from "./numberUtils";
import { createTypedArray } from "./arrayUtils";
+import { Cells, Vertices, Voronoi, Point } from "../modules/voronoi";
/**
* Get boundary points on a regular square grid
@@ -12,14 +13,14 @@ import { createTypedArray } from "./arrayUtils";
* @param {number} spacing - The spacing between points
* @returns {Array} - An array of boundary points
*/
-const getBoundaryPoints = (width: number, height: number, spacing: number) => {
+const getBoundaryPoints = (width: number, height: number, spacing: number): Point[] => {
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 = [];
+ const points: Point[] = [];
for (let i = 0.5; i < numberX; i++) {
let x = Math.ceil((w * i) / numberX + offset);
@@ -41,13 +42,13 @@ const getBoundaryPoints = (width: number, height: number, spacing: number) => {
* @param {number} spacing - The spacing between points
* @returns {Array} - An array of jittered grid points
*/
-const getJitteredGrid = (width: number, height: number, spacing: number): number[][] => {
+const getJitteredGrid = (width: number, height: number, spacing: number): Point[] => {
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: number[][] = [];
+ let points: Point[] = [];
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);
@@ -64,18 +65,18 @@ const getJitteredGrid = (width: number, height: number, spacing: number): number
* @param {number} graphHeight - The height of the graph
* @returns {Object} - An object containing spacing, cellsDesired, boundary points, grid points, cellsX, and cellsY
*/
-const placePoints = (graphWidth: number, graphHeight: number) => {
- window.TIME && console.time("placePoints");
+const placePoints = (graphWidth: number, graphHeight: number): {spacing: number, cellsDesired: number, boundary: Point[], points: Point[], cellsX: number, cellsY: number} => {
+ TIME && console.time("placePoints");
const cellsDesired = +(byId("pointsInput")?.dataset.cells || 0);
- const spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); // spacing between points before jirrering
+ const spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); // spacing between points before jittering
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);
- window.TIME && console.timeEnd("placePoints");
+ const cellCountX = Math.floor((graphWidth + 0.5 * spacing - 1e-10) / spacing); // number of cells in x direction
+ const cellCountY = Math.floor((graphHeight + 0.5 * spacing - 1e-10) / spacing); // number of cells in y direction
+ TIME && console.timeEnd("placePoints");
- return {spacing, cellsDesired, boundary, points, cellsX, cellsY};
+ return {spacing, cellsDesired, boundary, points, cellsX: cellCountX, cellsY: cellCountY};
}
@@ -100,11 +101,22 @@ export const shouldRegenerateGrid = (grid: any, expectedSeed: number, graphWidth
return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY;
}
+interface Grid {
+ spacing: number;
+ cellsDesired: number;
+ boundary: Point[];
+ points: Point[];
+ cellsX: number;
+ cellsY: number;
+ seed: string | number;
+ cells: Cells;
+ vertices: Vertices;
+}
/**
* Generates a Voronoi grid based on jittered grid points
* @returns {Object} - The generated grid object containing spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, and seed
*/
-export const generateGrid = (seed: string, graphWidth: number, graphHeight: number) => {
+export const generateGrid = (seed: string, graphWidth: number, graphHeight: number): Grid => {
Math.random = Alea(seed); // reset PRNG
const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints(graphWidth, graphHeight);
const {cells, vertices} = calculateVoronoi(points, boundary);
@@ -117,19 +129,19 @@ export const generateGrid = (seed: string, graphWidth: number, graphHeight: numb
* @param {Array} boundary - The boundary points to clip the Voronoi cells
* @returns {Object} - An object containing Voronoi cells and vertices
*/
-export const calculateVoronoi = (points: number[][], boundary: number[][]) => {
- window.TIME && console.time("calculateDelaunay");
+export const calculateVoronoi = (points: Point[], boundary: Point[]): {cells: Cells, vertices: Vertices} => {
+ TIME && console.time("calculateDelaunay");
const allPoints = points.concat(boundary);
const delaunay = Delaunator.from(allPoints);
- window.TIME && console.timeEnd("calculateDelaunay");
+ TIME && console.timeEnd("calculateDelaunay");
- window.TIME && console.time("calculateVoronoi");
- const voronoi = new window.Voronoi(delaunay, allPoints, points.length);
+ TIME && console.time("calculateVoronoi");
+ const voronoi = new Voronoi(delaunay, allPoints, points.length);
const cells = voronoi.cells;
- cells.i = createTypedArray({maxValue: points.length, length: points.length}).map((_, i) => i); // array of indexes
+ cells.i = createTypedArray({maxValue: points.length, length: points.length}).map((_, i) => i) as Uint32Array; // array of indexes
const vertices = voronoi.vertices;
- window.TIME && console.timeEnd("calculateVoronoi");
+ TIME && console.timeEnd("calculateVoronoi");
return {cells, vertices};
}
@@ -432,9 +444,8 @@ export const drawHeights = ({heights, width, height, scheme, renderOcean}: {heig
}
declare global {
+ var TIME: boolean;
interface Window {
- TIME: boolean;
- Voronoi: any;
shouldRegenerateGrid: typeof shouldRegenerateGrid;
generateGrid: typeof generateGrid;
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 82439ac7..73581a38 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -143,4 +143,94 @@ window.drawCellsValue = (data:any[]) => drawCellsValue(data, (window as any).pac
window.drawPolygons = (data: any[]) => drawPolygons(data, (window as any).terrs, (window as any).grid);
window.drawRouteConnections = () => drawRouteConnections((window as any).packedGraph);
window.drawPoint = drawPoint;
-window.drawPath = drawPath;
\ No newline at end of file
+window.drawPath = drawPath;
+
+
+export {
+ rn,
+ lim,
+ minmax,
+ normalize,
+ lerp,
+ isVowel,
+ trimVowels,
+ getAdjective,
+ nth,
+ abbreviate,
+ list,
+ last,
+ unique,
+ deepCopy,
+ getTypedArray,
+ createTypedArray,
+ TYPED_ARRAY_MAX_VALUES,
+ rand,
+ P,
+ each,
+ gauss,
+ Pint,
+ biased,
+ generateSeed,
+ getNumberInRange,
+ ra,
+ rw,
+ convertTemperature,
+ si,
+ getIntegerFromSI,
+ toHEX,
+ getColors,
+ getRandomColor,
+ getMixedColor,
+ C_12,
+ getComposedPath,
+ getNextId,
+ rollups,
+ distanceSquared,
+ getIsolines,
+ getPolesOfInaccessibility,
+ connectVertices,
+ findPath,
+ getVertexPath,
+ round,
+ capitalize,
+ splitInTwo,
+ parseTransform,
+ isValidJSON,
+ safeParseJSON,
+ sanitizeId,
+ byId,
+ shouldRegenerateGrid,
+ generateGrid,
+ findGridAll,
+ findGridCell,
+ findClosestCell,
+ calculateVoronoi,
+ findAllCellsInRadius,
+ getPackPolygon,
+ getGridPolygon,
+ poissonDiscSampler,
+ isLand,
+ isWater,
+ findAllInQuadtree,
+ drawHeights,
+ clipPoly,
+ getSegmentId,
+ debounce,
+ throttle,
+ parseError,
+ getBase64,
+ openURL,
+ wiki,
+ link,
+ isCtrlClick,
+ generateDate,
+ getLongitude,
+ getLatitude,
+ getCoordinates,
+ initializePrompt,
+ drawCellsValue,
+ drawPolygons,
+ drawRouteConnections,
+ drawPoint,
+ drawPath
+}
\ No newline at end of file