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