mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-02-05 01:51:23 +01:00
chore: add biome for linting/formatting + CI action for linting in SRC folder (#1284)
* chore: add npm + vite for progressive enhancement * fix: update Dockerfile to copy only the dist folder contents * fix: update Dockerfile to use multi-stage build for optimized production image * fix: correct nginx config file copy command in Dockerfile * chore: add netlify configuration for build and redirects * fix: add NODE_VERSION to environment in Netlify configuration * remove wrong dist folder * Update package.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: split public and src * migrating all util files from js to ts * feat: Implement HeightmapGenerator and Voronoi module - Added HeightmapGenerator class for generating heightmaps with various tools (Hill, Pit, Range, Trough, Strait, etc.). - Introduced Voronoi class for creating Voronoi diagrams using Delaunator. - Updated index.html to include new modules. - Created index.ts to manage module imports. - Enhanced arrayUtils and graphUtils with type definitions and improved functionality. - Added utility functions for generating grids and calculating Voronoi cells. * chore: add GitHub Actions workflow for deploying to GitHub Pages * fix: update branch name in GitHub Actions workflow from 'main' to 'master' * chore: update package.json to specify Node.js engine version and remove unused launch.json * Initial plan * Update copilot guidelines to reflect NPM/Vite/TypeScript migration Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com> * Update src/modules/heightmap-generator.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/utils/graphUtils.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/heightmap-generator.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feat: Add TIME and ERROR variables to global scope in HeightmapGenerator * fix: Update base path in vite.config.ts for Netlify deployment * refactor: Migrate features to a new module and remove legacy script reference * refactor: Update feature interfaces and improve type safety in FeatureModule * refactor: Add documentation for markupPack and defineGroups methods in FeatureModule * refactor: Remove legacy ocean-layers.js and migrate functionality to ocean-layers.ts * refactor: Remove river-generator.js script reference and migrate river generation logic to river-generator.ts * refactor: Remove river-generator.js reference and add biomes module * refactor: Migrate lakes functionality to lakes.ts and update related interfaces * refactor: clean up global variable declarations and improve type definitions * refactor: update shoreline calculation and improve type imports in PackedGraph * fix: e2e tests * chore: add biome for linting/formatting * chore: add linting workflow using Biome * refactor: improve code readability by standardizing string quotes and simplifying function calls --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Azgaar <maxganiev@yandex.com> Co-authored-by: Azgaar <azgaar.fmg@yandex.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com>
This commit is contained in:
parent
e37fce1eed
commit
9db40a5230
31 changed files with 2001 additions and 782 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { range, mean } from "d3";
|
||||
import { mean, range } from "d3";
|
||||
import { rn } from "../utils";
|
||||
|
||||
declare global {
|
||||
|
|
@ -22,7 +22,7 @@ class BiomesModule {
|
|||
"Taiga",
|
||||
"Tundra",
|
||||
"Glacier",
|
||||
"Wetland"
|
||||
"Wetland",
|
||||
];
|
||||
|
||||
const color: string[] = [
|
||||
|
|
@ -38,33 +38,54 @@ class BiomesModule {
|
|||
"#4b6b32",
|
||||
"#96784b",
|
||||
"#d5e7eb",
|
||||
"#0b9131"
|
||||
"#0b9131",
|
||||
];
|
||||
const habitability: number[] = [0, 4, 10, 22, 30, 50, 100, 80, 90, 12, 4, 0, 12];
|
||||
const iconsDensity: number[] = [0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 250];
|
||||
const icons: Array<{[key: string]: number}> = [
|
||||
{},
|
||||
{dune: 3, cactus: 6, deadTree: 1},
|
||||
{dune: 9, deadTree: 1},
|
||||
{acacia: 1, grass: 9},
|
||||
{grass: 1},
|
||||
{acacia: 8, palm: 1},
|
||||
{deciduous: 1},
|
||||
{acacia: 5, palm: 3, deciduous: 1, swamp: 1},
|
||||
{deciduous: 6, swamp: 1},
|
||||
{conifer: 1},
|
||||
{grass: 1},
|
||||
{},
|
||||
{swamp: 1}
|
||||
const habitability: number[] = [
|
||||
0, 4, 10, 22, 30, 50, 100, 80, 90, 12, 4, 0, 12,
|
||||
];
|
||||
const cost: number[] = [10, 200, 150, 60, 50, 70, 70, 80, 90, 200, 1000, 5000, 150]; // biome movement cost
|
||||
const iconsDensity: number[] = [
|
||||
0, 3, 2, 120, 120, 120, 120, 150, 150, 100, 5, 0, 250,
|
||||
];
|
||||
const icons: Array<{ [key: string]: number }> = [
|
||||
{},
|
||||
{ dune: 3, cactus: 6, deadTree: 1 },
|
||||
{ dune: 9, deadTree: 1 },
|
||||
{ acacia: 1, grass: 9 },
|
||||
{ grass: 1 },
|
||||
{ acacia: 8, palm: 1 },
|
||||
{ deciduous: 1 },
|
||||
{ acacia: 5, palm: 3, deciduous: 1, swamp: 1 },
|
||||
{ deciduous: 6, swamp: 1 },
|
||||
{ conifer: 1 },
|
||||
{ grass: 1 },
|
||||
{},
|
||||
{ swamp: 1 },
|
||||
];
|
||||
const cost: number[] = [
|
||||
10, 200, 150, 60, 50, 70, 70, 80, 90, 200, 1000, 5000, 150,
|
||||
]; // biome movement cost
|
||||
const biomesMatrix: Uint8Array[] = [
|
||||
// hot ↔ cold [>19°C; <-4°C]; dry ↕ wet
|
||||
new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10]),
|
||||
new Uint8Array([3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10, 10, 10]),
|
||||
new Uint8Array([5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 10, 10, 10]),
|
||||
new Uint8Array([5, 6, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 10]),
|
||||
new Uint8Array([7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10, 10])
|
||||
new Uint8Array([
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 10,
|
||||
]),
|
||||
new Uint8Array([
|
||||
3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 9, 9, 9, 9, 10,
|
||||
10, 10,
|
||||
]),
|
||||
new Uint8Array([
|
||||
5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 10,
|
||||
10, 10,
|
||||
]),
|
||||
new Uint8Array([
|
||||
5, 6, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10,
|
||||
10, 10,
|
||||
]),
|
||||
new Uint8Array([
|
||||
7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9,
|
||||
10, 10,
|
||||
]),
|
||||
];
|
||||
|
||||
// parse icons weighted array into a simple array
|
||||
|
|
@ -79,14 +100,29 @@ class BiomesModule {
|
|||
parsedIcons[i] = parsed;
|
||||
}
|
||||
|
||||
return {i: range(0, name.length), name, color, biomesMatrix, habitability, iconsDensity, icons: parsedIcons, cost};
|
||||
};
|
||||
return {
|
||||
i: range(0, name.length),
|
||||
name,
|
||||
color,
|
||||
biomesMatrix,
|
||||
habitability,
|
||||
iconsDensity,
|
||||
icons: parsedIcons,
|
||||
cost,
|
||||
};
|
||||
}
|
||||
|
||||
define() {
|
||||
TIME && console.time("defineBiomes");
|
||||
|
||||
const {fl: flux, r: riverIds, h: heights, c: neighbors, g: gridReference} = pack.cells;
|
||||
const {temp, prec} = grid.cells;
|
||||
const {
|
||||
fl: flux,
|
||||
r: riverIds,
|
||||
h: heights,
|
||||
c: neighbors,
|
||||
g: gridReference,
|
||||
} = pack.cells;
|
||||
const { temp, prec } = grid.cells;
|
||||
pack.cells.biome = new Uint8Array(pack.cells.i.length); // biomes array
|
||||
|
||||
const calculateMoisture = (cellId: number) => {
|
||||
|
|
@ -94,23 +130,36 @@ class BiomesModule {
|
|||
if (riverIds[cellId]) moisture += Math.max(flux[cellId] / 10, 2);
|
||||
|
||||
const moistAround = neighbors[cellId]
|
||||
.filter((neibCellId: number) => heights[neibCellId] >= this.MIN_LAND_HEIGHT)
|
||||
.filter(
|
||||
(neibCellId: number) => heights[neibCellId] >= this.MIN_LAND_HEIGHT,
|
||||
)
|
||||
.map((c: number) => prec[gridReference[c]])
|
||||
.concat([moisture]);
|
||||
return rn(4 + (mean(moistAround) as number));
|
||||
}
|
||||
};
|
||||
|
||||
for (let cellId = 0; cellId < heights.length; cellId++) {
|
||||
const height = heights[cellId];
|
||||
const moisture = height < this.MIN_LAND_HEIGHT ? 0 : calculateMoisture(cellId);
|
||||
const moisture =
|
||||
height < this.MIN_LAND_HEIGHT ? 0 : calculateMoisture(cellId);
|
||||
const temperature = temp[gridReference[cellId]];
|
||||
pack.cells.biome[cellId] = this.getId(moisture, temperature, height, Boolean(riverIds[cellId]));
|
||||
pack.cells.biome[cellId] = this.getId(
|
||||
moisture,
|
||||
temperature,
|
||||
height,
|
||||
Boolean(riverIds[cellId]),
|
||||
);
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("defineBiomes");
|
||||
}
|
||||
|
||||
getId(moisture: number, temperature: number, height: number, hasRiver: boolean) {
|
||||
getId(
|
||||
moisture: number,
|
||||
temperature: number,
|
||||
height: number,
|
||||
hasRiver: boolean,
|
||||
) {
|
||||
if (height < 20) return 0; // all water cells: marine biome
|
||||
if (temperature < -5) return 11; // too cold: permafrost biome
|
||||
if (temperature >= 25 && !hasRiver && moisture < 8) return 1; // too hot and dry: hot desert biome
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
import { clipPoly, connectVertices, createTypedArray, distanceSquared, isLand, isWater, rn, TYPED_ARRAY_MAX_VALUES, unique } from "../utils";
|
||||
import Alea from "alea";
|
||||
import { polygonArea } from "d3";
|
||||
import {
|
||||
clipPoly,
|
||||
connectVertices,
|
||||
createTypedArray,
|
||||
distanceSquared,
|
||||
isLand,
|
||||
isWater,
|
||||
rn,
|
||||
TYPED_ARRAY_MAX_VALUES,
|
||||
unique,
|
||||
} from "../utils";
|
||||
|
||||
declare global {
|
||||
var Features: FeatureModule;
|
||||
|
|
@ -52,14 +62,24 @@ class FeatureModule {
|
|||
/**
|
||||
* calculate distance to coast for every cell
|
||||
*/
|
||||
private markup({ distanceField, neighbors, start, increment, limit = TYPED_ARRAY_MAX_VALUES.INT8_MAX }: {
|
||||
private markup({
|
||||
distanceField,
|
||||
neighbors,
|
||||
start,
|
||||
increment,
|
||||
limit = TYPED_ARRAY_MAX_VALUES.INT8_MAX,
|
||||
}: {
|
||||
distanceField: Int8Array;
|
||||
neighbors: number[][];
|
||||
start: number;
|
||||
increment: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
for (let distance = start, marked = Infinity; marked > 0 && distance !== limit; distance += increment) {
|
||||
for (
|
||||
let distance = start, marked = Infinity;
|
||||
marked > 0 && distance !== limit;
|
||||
distance += increment
|
||||
) {
|
||||
marked = 0;
|
||||
const prevDistance = distance - increment;
|
||||
for (let cellId = 0; cellId < neighbors.length; cellId++) {
|
||||
|
|
@ -115,11 +135,17 @@ class FeatureModule {
|
|||
const type = land ? "island" : border ? "ocean" : "lake";
|
||||
features.push({ i: featureId, land, border, type });
|
||||
|
||||
queue[0] = featureIds.findIndex(f => f === this.UNMARKED); // find unmarked cell
|
||||
queue[0] = featureIds.indexOf(this.UNMARKED); // find unmarked cell
|
||||
}
|
||||
|
||||
// markup deep ocean cells
|
||||
this.markup({ distanceField, neighbors, start: this.DEEP_WATER, increment: -1, limit: -10 });
|
||||
this.markup({
|
||||
distanceField,
|
||||
neighbors,
|
||||
start: this.DEEP_WATER,
|
||||
increment: -1,
|
||||
limit: -10,
|
||||
});
|
||||
grid.cells.t = distanceField;
|
||||
grid.cells.f = featureIds;
|
||||
grid.features = [0, ...features];
|
||||
|
|
@ -132,15 +158,22 @@ class FeatureModule {
|
|||
*/
|
||||
markupPack() {
|
||||
const defineHaven = (cellId: number) => {
|
||||
const waterCells = neighbors[cellId].filter((index: number) => isWater(index, pack));
|
||||
const distances = waterCells.map((neibCellId: number) => distanceSquared(cells.p[cellId], cells.p[neibCellId]));
|
||||
const waterCells = neighbors[cellId].filter((index: number) =>
|
||||
isWater(index, pack),
|
||||
);
|
||||
const distances = waterCells.map((neibCellId: number) =>
|
||||
distanceSquared(cells.p[cellId], cells.p[neibCellId]),
|
||||
);
|
||||
const closest = distances.indexOf(Math.min.apply(Math, distances));
|
||||
|
||||
haven[cellId] = waterCells[closest];
|
||||
harbor[cellId] = waterCells.length;
|
||||
}
|
||||
};
|
||||
|
||||
const getCellsData = (featureType: string, firstCell: number): [number, number[]] => {
|
||||
const getCellsData = (
|
||||
featureType: string,
|
||||
firstCell: number,
|
||||
): [number, number[]] => {
|
||||
if (featureType === "ocean") return [firstCell, []];
|
||||
|
||||
const getType = (cellId: number) => featureIds[cellId];
|
||||
|
|
@ -153,29 +186,55 @@ class FeatureModule {
|
|||
return [startCell, featureVertices];
|
||||
|
||||
function findOnBorderCell(firstCell: number) {
|
||||
const isOnBorder = (cellId: number) => borderCells[cellId] || neighbors[cellId].some(ofDifferentType);
|
||||
const isOnBorder = (cellId: number) =>
|
||||
borderCells[cellId] || neighbors[cellId].some(ofDifferentType);
|
||||
if (isOnBorder(firstCell)) return firstCell;
|
||||
|
||||
const startCell = cells.i.filter(ofSameType).find(isOnBorder);
|
||||
if (startCell === undefined)
|
||||
throw new Error(`Markup: firstCell ${firstCell} is not on the feature or map border`);
|
||||
throw new Error(
|
||||
`Markup: firstCell ${firstCell} is not on the feature or map border`,
|
||||
);
|
||||
|
||||
return startCell;
|
||||
}
|
||||
|
||||
function getFeatureVertices(startCell: number) {
|
||||
const startingVertex = cells.v[startCell].find((v: number) => vertices.c[v].some(ofDifferentType));
|
||||
const startingVertex = cells.v[startCell].find((v: number) =>
|
||||
vertices.c[v].some(ofDifferentType),
|
||||
);
|
||||
if (startingVertex === undefined)
|
||||
throw new Error(`Markup: startingVertex for cell ${startCell} is not found`);
|
||||
throw new Error(
|
||||
`Markup: startingVertex for cell ${startCell} is not found`,
|
||||
);
|
||||
|
||||
return connectVertices({ vertices, startingVertex, ofSameType, closeRing: false });
|
||||
return connectVertices({
|
||||
vertices,
|
||||
startingVertex,
|
||||
ofSameType,
|
||||
closeRing: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addFeature = ({ firstCell, land, border, featureId, totalCells }: { firstCell: number; land: boolean; border: boolean; featureId: number; totalCells: number }): PackedGraphFeature => {
|
||||
const addFeature = ({
|
||||
firstCell,
|
||||
land,
|
||||
border,
|
||||
featureId,
|
||||
totalCells,
|
||||
}: {
|
||||
firstCell: number;
|
||||
land: boolean;
|
||||
border: boolean;
|
||||
featureId: number;
|
||||
totalCells: number;
|
||||
}): PackedGraphFeature => {
|
||||
const type = land ? "island" : border ? "ocean" : "lake";
|
||||
const [startCell, featureVertices] = getCellsData(type, firstCell);
|
||||
const points = clipPoly(featureVertices.map((vertex: number) => vertices.p[vertex]));
|
||||
const points = clipPoly(
|
||||
featureVertices.map((vertex: number) => vertices.p[vertex]),
|
||||
);
|
||||
const area = polygonArea(points); // feature perimiter area
|
||||
const absArea = Math.abs(rn(area));
|
||||
|
||||
|
|
@ -193,20 +252,20 @@ class FeatureModule {
|
|||
};
|
||||
|
||||
if (type === "lake") {
|
||||
if (area > 0) feature.vertices = (feature.vertices as number[]).reverse();
|
||||
if (area > 0)
|
||||
feature.vertices = (feature.vertices as number[]).reverse();
|
||||
feature.shoreline = unique(
|
||||
(feature.vertices as number[])
|
||||
.flatMap(
|
||||
vertexIndex => vertices.c[vertexIndex].filter((index) => isLand(index, pack))
|
||||
)
|
||||
(feature.vertices as number[]).flatMap((vertexIndex) =>
|
||||
vertices.c[vertexIndex].filter((index) => isLand(index, pack)),
|
||||
),
|
||||
);
|
||||
feature.height = Lakes.getHeight(feature as PackedGraphFeature);
|
||||
}
|
||||
|
||||
return {
|
||||
...feature
|
||||
...feature,
|
||||
} as PackedGraphFeature;
|
||||
}
|
||||
};
|
||||
|
||||
TIME && console.time("markupPack");
|
||||
|
||||
|
|
@ -217,7 +276,10 @@ class FeatureModule {
|
|||
|
||||
const distanceField = new Int8Array(packCellsNumber); // pack.cells.t
|
||||
const featureIds = new Uint16Array(packCellsNumber); // pack.cells.f
|
||||
const haven = createTypedArray({ maxValue: packCellsNumber, length: packCellsNumber }); // haven: opposite water cell
|
||||
const haven = createTypedArray({
|
||||
maxValue: packCellsNumber,
|
||||
length: packCellsNumber,
|
||||
}); // haven: opposite water cell
|
||||
const harbor = new Uint8Array(packCellsNumber); // harbor: number of adjacent water cells
|
||||
const features: PackedGraphFeature[] = [];
|
||||
|
||||
|
|
@ -242,9 +304,15 @@ class FeatureModule {
|
|||
distanceField[neighborId] = this.WATER_COAST;
|
||||
if (!haven[cellId]) defineHaven(cellId);
|
||||
} else if (land && isNeibLand) {
|
||||
if (distanceField[neighborId] === this.UNMARKED && distanceField[cellId] === this.LAND_COAST)
|
||||
if (
|
||||
distanceField[neighborId] === this.UNMARKED &&
|
||||
distanceField[cellId] === this.LAND_COAST
|
||||
)
|
||||
distanceField[neighborId] = this.LANDLOCKED;
|
||||
else if (distanceField[cellId] === this.UNMARKED && distanceField[neighborId] === this.LAND_COAST)
|
||||
else if (
|
||||
distanceField[cellId] === this.UNMARKED &&
|
||||
distanceField[neighborId] === this.LAND_COAST
|
||||
)
|
||||
distanceField[cellId] = this.LANDLOCKED;
|
||||
}
|
||||
|
||||
|
|
@ -256,12 +324,25 @@ class FeatureModule {
|
|||
}
|
||||
}
|
||||
|
||||
features.push(addFeature({ firstCell, land, border, featureId, totalCells }));
|
||||
queue[0] = featureIds.findIndex(f => f === this.UNMARKED); // find unmarked cell
|
||||
features.push(
|
||||
addFeature({ firstCell, land, border, featureId, totalCells }),
|
||||
);
|
||||
queue[0] = featureIds.indexOf(this.UNMARKED); // find unmarked cell
|
||||
}
|
||||
|
||||
this.markup({ distanceField, neighbors, start: this.DEEPER_LAND, increment: 1 }); // markup pack land
|
||||
this.markup({ distanceField, neighbors, start: this.DEEP_WATER, increment: -1, limit: -10 }); // markup pack water
|
||||
this.markup({
|
||||
distanceField,
|
||||
neighbors,
|
||||
start: this.DEEPER_LAND,
|
||||
increment: 1,
|
||||
}); // markup pack land
|
||||
this.markup({
|
||||
distanceField,
|
||||
neighbors,
|
||||
start: this.DEEP_WATER,
|
||||
increment: -1,
|
||||
limit: -10,
|
||||
}); // markup pack water
|
||||
|
||||
pack.cells.t = distanceField;
|
||||
pack.cells.f = featureIds;
|
||||
|
|
@ -287,34 +368,40 @@ class FeatureModule {
|
|||
if (feature.cells > CONTINENT_MIN_SIZE) return "continent";
|
||||
if (feature.cells > ISLAND_MIN_SIZE) return "island";
|
||||
return "isle";
|
||||
}
|
||||
};
|
||||
|
||||
const defineOceanGroup = (feature: PackedGraphFeature) => {
|
||||
if (feature.cells > OCEAN_MIN_SIZE) return "ocean";
|
||||
if (feature.cells > SEA_MIN_SIZE) return "sea";
|
||||
return "gulf";
|
||||
}
|
||||
};
|
||||
|
||||
const defineLakeGroup = (feature: PackedGraphFeature) => {
|
||||
if (feature.temp < -3) return "frozen";
|
||||
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
|
||||
if (
|
||||
feature.height > 60 &&
|
||||
feature.cells < 10 &&
|
||||
feature.firstCell % 10 === 0
|
||||
)
|
||||
return "lava";
|
||||
|
||||
if (!feature.inlets && !feature.outlet) {
|
||||
if (feature.evaporation > feature.flux * 4) return "dry";
|
||||
if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
|
||||
if (feature.cells < 3 && feature.firstCell % 10 === 0)
|
||||
return "sinkhole";
|
||||
}
|
||||
|
||||
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
|
||||
|
||||
return "freshwater";
|
||||
}
|
||||
};
|
||||
|
||||
const defineGroup = (feature: PackedGraphFeature) => {
|
||||
if (feature.type === "island") return defineIslandGroup(feature);
|
||||
if (feature.type === "ocean") return defineOceanGroup(feature);
|
||||
if (feature.type === "lake") return defineLakeGroup(feature);
|
||||
throw new Error(`Markup: unknown feature type ${feature.type}`);
|
||||
}
|
||||
};
|
||||
|
||||
for (const feature of pack.features) {
|
||||
if (!feature || feature.type === "ocean") continue;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,33 @@
|
|||
import Alea from "alea";
|
||||
import { range as d3Range, leastIndex, mean } from "d3";
|
||||
import { createTypedArray, byId, findGridCell, getNumberInRange, lim, minmax, P, rand } from "../utils";
|
||||
import {
|
||||
byId,
|
||||
createTypedArray,
|
||||
findGridCell,
|
||||
getNumberInRange,
|
||||
lim,
|
||||
minmax,
|
||||
P,
|
||||
rand,
|
||||
} from "../utils";
|
||||
|
||||
declare global {
|
||||
var HeightmapGenerator: HeightmapGenerator;
|
||||
var HeightmapGenerator: HeightmapModule;
|
||||
}
|
||||
|
||||
type Tool = "Hill" | "Pit" | "Range" | "Trough" | "Strait" | "Mask" | "Invert" | "Add" | "Multiply" | "Smooth";
|
||||
type Tool =
|
||||
| "Hill"
|
||||
| "Pit"
|
||||
| "Range"
|
||||
| "Trough"
|
||||
| "Strait"
|
||||
| "Mask"
|
||||
| "Invert"
|
||||
| "Add"
|
||||
| "Multiply"
|
||||
| "Smooth";
|
||||
|
||||
class HeightmapGenerator {
|
||||
class HeightmapModule {
|
||||
grid: any = null;
|
||||
heights: Uint8Array | null = null;
|
||||
blobPower: number = 0;
|
||||
|
|
@ -17,9 +36,8 @@ class HeightmapGenerator {
|
|||
private clearData() {
|
||||
this.heights = null;
|
||||
this.grid = null;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private getBlobPower(cells: number): number {
|
||||
const blobPowerMap: Record<number, number> = {
|
||||
1000: 0.93,
|
||||
|
|
@ -34,11 +52,11 @@ class HeightmapGenerator {
|
|||
70000: 0.9955,
|
||||
80000: 0.996,
|
||||
90000: 0.9964,
|
||||
100000: 0.9973
|
||||
100000: 0.9973,
|
||||
};
|
||||
return blobPowerMap[cells] || 0.98;
|
||||
}
|
||||
|
||||
|
||||
private getLinePower(cells: number): number {
|
||||
const linePowerMap: Record<number, number> = {
|
||||
1000: 0.75,
|
||||
|
|
@ -53,38 +71,43 @@ class HeightmapGenerator {
|
|||
70000: 0.88,
|
||||
80000: 0.91,
|
||||
90000: 0.92,
|
||||
100000: 0.93
|
||||
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;
|
||||
|
||||
const min = parseInt(range.split("-")[0], 10) / 100 || 0;
|
||||
const max = parseInt(range.split("-")[1], 10) / 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;
|
||||
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;
|
||||
if (!this.heights || !this.grid) return;
|
||||
const change = new Uint8Array(this.heights.length);
|
||||
let limit = 0;
|
||||
let start: number;
|
||||
let h = lim(getNumberInRange(height));
|
||||
const h = lim(getNumberInRange(height));
|
||||
|
||||
do {
|
||||
const x = this.getPointInRange(rangeX, graphWidth);
|
||||
|
|
@ -106,17 +129,17 @@ class HeightmapGenerator {
|
|||
}
|
||||
|
||||
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;
|
||||
if (!this.heights || !this.grid) return;
|
||||
const used = new Uint8Array(this.heights.length);
|
||||
let limit = 0;
|
||||
let start: number;
|
||||
|
|
@ -138,24 +161,33 @@ class HeightmapGenerator {
|
|||
|
||||
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));
|
||||
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;
|
||||
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;
|
||||
if (!this.heights || !this.grid) return;
|
||||
|
||||
// get main ridge
|
||||
const getRange = (cur: number, end: number) => {
|
||||
|
|
@ -180,7 +212,7 @@ class HeightmapGenerator {
|
|||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
};
|
||||
|
||||
const used = new Uint8Array(this.heights.length);
|
||||
let h = lim(getNumberInRange(height));
|
||||
|
|
@ -192,32 +224,37 @@ class HeightmapGenerator {
|
|||
|
||||
let dist = 0;
|
||||
let limit = 0;
|
||||
let endY;
|
||||
let endX;
|
||||
let endY: number;
|
||||
let endX: number;
|
||||
|
||||
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);
|
||||
} while (
|
||||
(dist < graphWidth / 8 || dist > 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);
|
||||
|
||||
const 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++;
|
||||
queue = [];
|
||||
i++;
|
||||
frontier.forEach((i: number) => {
|
||||
if(!this.heights) return;
|
||||
this.heights[i] = lim(this.heights[i] + h * (Math.random() * 0.3 + 0.85));
|
||||
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;
|
||||
|
|
@ -235,31 +272,42 @@ class HeightmapGenerator {
|
|||
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 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;
|
||||
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 {
|
||||
addTrough(
|
||||
count: string,
|
||||
height: string,
|
||||
rangeX: string,
|
||||
rangeY: string,
|
||||
startCellId?: number,
|
||||
endCellId?: number,
|
||||
): void {
|
||||
const addOneTrough = () => {
|
||||
if(!this.heights || !this.grid) return;
|
||||
if (!this.heights || !this.grid) return;
|
||||
|
||||
// get main ridge
|
||||
// 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) => {
|
||||
|
|
@ -275,13 +323,13 @@ class HeightmapGenerator {
|
|||
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;
|
||||
|
|
@ -296,29 +344,34 @@ class HeightmapGenerator {
|
|||
startCellId = findGridCell(startX, startY, this.grid);
|
||||
limit++;
|
||||
} while (this.heights[startCellId] < 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);
|
||||
|
||||
} while (
|
||||
(dist < graphWidth / 8 || dist > graphWidth / 2) &&
|
||||
limit < 50
|
||||
);
|
||||
|
||||
endCellId = findGridCell(endX, endY, this.grid);
|
||||
}
|
||||
|
||||
let range = getRange(startCellId as number, endCellId as number);
|
||||
|
||||
|
||||
|
||||
const range = getRange(startCellId as number, endCellId as number);
|
||||
|
||||
// add height to ridge and cells around
|
||||
let queue = range.slice(),
|
||||
i = 0;
|
||||
i = 0;
|
||||
while (queue.length) {
|
||||
const frontier = queue.slice();
|
||||
(queue = []), i++;
|
||||
queue = [];
|
||||
i++;
|
||||
frontier.forEach((i: number) => {
|
||||
this.heights![i] = lim(this.heights![i] - h * (Math.random() * 0.3 + 0.85));
|
||||
this.heights![i] = lim(
|
||||
this.heights![i] - h * (Math.random() * 0.3 + 0.85),
|
||||
);
|
||||
});
|
||||
h = h ** this.linePower - 1;
|
||||
if (h < 2) break;
|
||||
|
|
@ -331,41 +384,62 @@ class HeightmapGenerator {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 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 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;
|
||||
this.heights![min] =
|
||||
(this.heights![cur] * 2 + this.heights![min]) / 3;
|
||||
cur = min;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const desiredTroughCount = getNumberInRange(count);
|
||||
for(let i = 0; i < desiredTroughCount; i++) {
|
||||
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 (!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() * graphWidth * 0.4 + graphWidth * 0.3) : 5;
|
||||
const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * 0.4 + graphHeight * 0.3);
|
||||
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)
|
||||
? 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);
|
||||
: Math.floor(
|
||||
graphHeight -
|
||||
startY -
|
||||
graphHeight * 0.1 +
|
||||
Math.random() * graphHeight * 0.2,
|
||||
);
|
||||
|
||||
const start = findGridCell(startX, startY, this.grid);
|
||||
const end = findGridCell(endX, endY, this.grid);
|
||||
|
|
@ -388,14 +462,13 @@ class HeightmapGenerator {
|
|||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
};
|
||||
let range = getRange(start, end);
|
||||
const query: number[] = [];
|
||||
|
||||
|
||||
const step = 0.1 / desiredWidth;
|
||||
|
||||
for(let i = 0; i < desiredWidth; i++) {
|
||||
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) => {
|
||||
|
|
@ -408,15 +481,17 @@ class HeightmapGenerator {
|
|||
});
|
||||
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];
|
||||
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 => {
|
||||
this.heights = this.heights.map((h) => {
|
||||
if (h < min || h > max) return h;
|
||||
|
||||
if (add) h = isLand ? Math.max(h + add, 20) : h + add;
|
||||
|
|
@ -424,20 +499,22 @@ class HeightmapGenerator {
|
|||
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;
|
||||
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]));
|
||||
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;
|
||||
if (!this.heights || !this.grid) return;
|
||||
const fr = power ? Math.abs(power) : 1;
|
||||
|
||||
this.heights = this.heights.map((h, i) => {
|
||||
|
|
@ -449,17 +526,17 @@ class HeightmapGenerator {
|
|||
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 { cellsX, cellsY } = this.grid;
|
||||
|
||||
const inverted = this.heights.map((_h: number, i: number) => {
|
||||
if(!this.heights) return 0;
|
||||
if (!this.heights) return 0;
|
||||
const x = i % cellsX;
|
||||
const y = Math.floor(i / cellsX);
|
||||
|
||||
|
|
@ -470,66 +547,104 @@ class HeightmapGenerator {
|
|||
});
|
||||
|
||||
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);
|
||||
if (tool === "Hill") {
|
||||
this.addHill(a2, a3, a4, a5);
|
||||
return;
|
||||
}
|
||||
if (tool === "Pit") {
|
||||
this.addPit(a2, a3, a4, a5);
|
||||
return;
|
||||
}
|
||||
if (tool === "Range") {
|
||||
this.addRange(a2, a3, a4, a5);
|
||||
return;
|
||||
}
|
||||
if (tool === "Trough") {
|
||||
this.addTrough(a2, a3, a4, a5);
|
||||
return;
|
||||
}
|
||||
if (tool === "Strait") {
|
||||
this.addStrait(a2, a3);
|
||||
return;
|
||||
}
|
||||
if (tool === "Mask") {
|
||||
this.mask(+a2);
|
||||
return;
|
||||
}
|
||||
if (tool === "Invert") {
|
||||
this.invert(+a2, a3);
|
||||
return;
|
||||
}
|
||||
if (tool === "Add") {
|
||||
this.modify(a3, +a2, 1);
|
||||
return;
|
||||
}
|
||||
if (tool === "Multiply") {
|
||||
this.modify(a3, 0, +a2);
|
||||
return;
|
||||
}
|
||||
if (tool === "Smooth") {
|
||||
this.smooth(+a2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async generate(graph: any): Promise<Uint8Array> {
|
||||
TIME && console.time("defineHeightmap");
|
||||
const id = (byId("templateInput")! as HTMLInputElement).value;
|
||||
|
||||
Math.random = Alea(seed);
|
||||
const isTemplate = id in heightmapTemplates;
|
||||
|
||||
const heights = isTemplate ? this.fromTemplate(graph, id) : await this.fromPrecreated(graph, id);
|
||||
|
||||
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 {
|
||||
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}`);
|
||||
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]);
|
||||
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;
|
||||
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;
|
||||
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<Uint8Array> {
|
||||
return new Promise(resolve => {
|
||||
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;
|
||||
const { cellsX, cellsY } = graph;
|
||||
canvas.width = cellsX;
|
||||
canvas.height = cellsY;
|
||||
|
||||
|
|
@ -537,7 +652,7 @@ class HeightmapGenerator {
|
|||
const img = new Image();
|
||||
img.src = `./heightmaps/${id}.png`;
|
||||
img.onload = () => {
|
||||
if(!ctx) {
|
||||
if (!ctx) {
|
||||
throw new Error("Could not get canvas context");
|
||||
}
|
||||
this.heights = this.heights || new Uint8Array(cellsX * cellsY);
|
||||
|
|
@ -550,11 +665,11 @@ class HeightmapGenerator {
|
|||
resolve(this.heights);
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
getHeights() {
|
||||
return this.heights;
|
||||
}
|
||||
}
|
||||
|
||||
window.HeightmapGenerator = new HeightmapGenerator();
|
||||
window.HeightmapGenerator = new HeightmapModule();
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ import "./features";
|
|||
import "./lakes";
|
||||
import "./ocean-layers";
|
||||
import "./river-generator";
|
||||
import "./biomes"
|
||||
import "./biomes";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { PackedGraphFeature } from "./features";
|
||||
import { min, mean } from "d3";
|
||||
import { byId,
|
||||
rn } from "../utils";
|
||||
import { mean, min } from "d3";
|
||||
import { byId, rn } from "../utils";
|
||||
import type { PackedGraphFeature } from "./features";
|
||||
|
||||
declare global {
|
||||
var Lakes: LakesModule;
|
||||
|
|
@ -12,24 +11,25 @@ export class LakesModule {
|
|||
|
||||
getHeight(feature: PackedGraphFeature) {
|
||||
const heights = pack.cells.h;
|
||||
const minShoreHeight = min(feature.shoreline.map(cellId => heights[cellId])) || 20;
|
||||
const minShoreHeight =
|
||||
min(feature.shoreline.map((cellId) => heights[cellId])) || 20;
|
||||
return rn(minShoreHeight - this.LAKE_ELEVATION_DELTA, 2);
|
||||
};
|
||||
}
|
||||
|
||||
defineNames() {
|
||||
pack.features.forEach((feature: PackedGraphFeature) => {
|
||||
if (feature.type !== "lake") return;
|
||||
feature.name = this.getName(feature);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
getName(feature: PackedGraphFeature): string {
|
||||
const landCell = feature.shoreline[0];
|
||||
const culture = pack.cells.culture[landCell];
|
||||
return Names.getCulture(culture);
|
||||
};
|
||||
}
|
||||
|
||||
cleanupLakeData = function () {
|
||||
cleanupLakeData = () => {
|
||||
for (const feature of pack.features) {
|
||||
if (feature.type !== "lake") continue;
|
||||
delete feature.river;
|
||||
|
|
@ -38,39 +38,50 @@ export class LakesModule {
|
|||
delete feature.closed;
|
||||
feature.height = rn(feature.height, 3);
|
||||
|
||||
const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r));
|
||||
const inlets = feature.inlets?.filter((r) =>
|
||||
pack.rivers.find((river) => river.i === r),
|
||||
);
|
||||
if (!inlets || !inlets.length) delete feature.inlets;
|
||||
else feature.inlets = inlets;
|
||||
|
||||
const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet);
|
||||
const outlet =
|
||||
feature.outlet &&
|
||||
pack.rivers.find((river) => river.i === feature.outlet);
|
||||
if (!outlet) delete feature.outlet;
|
||||
}
|
||||
};
|
||||
|
||||
defineClimateData(heights: number[] | Uint8Array) {
|
||||
const {cells, features} = pack;
|
||||
const { cells, features } = pack;
|
||||
const lakeOutCells = new Uint16Array(cells.i.length);
|
||||
|
||||
|
||||
const getFlux = (lake: PackedGraphFeature) => {
|
||||
return lake.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
|
||||
}
|
||||
return lake.shoreline.reduce(
|
||||
(acc, c) => acc + grid.cells.prec[cells.g[c]],
|
||||
0,
|
||||
);
|
||||
};
|
||||
|
||||
const getLakeTemp = (lake: PackedGraphFeature) => {
|
||||
if (lake.cells < 6) return grid.cells.temp[cells.g[lake.firstCell]];
|
||||
return rn(mean(lake.shoreline.map(c => grid.cells.temp[cells.g[c]])) as number, 1);
|
||||
}
|
||||
return rn(
|
||||
mean(lake.shoreline.map((c) => grid.cells.temp[cells.g[c]])) as number,
|
||||
1,
|
||||
);
|
||||
};
|
||||
|
||||
const getLakeEvaporation = (lake: PackedGraphFeature) => {
|
||||
const height = (lake.height - 18) ** Number(heightExponentInput.value); // height in meters
|
||||
const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11]
|
||||
const evaporation =
|
||||
((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11]
|
||||
return rn(evaporation * lake.cells);
|
||||
}
|
||||
};
|
||||
|
||||
const getLowestShoreCell = (lake: PackedGraphFeature) => {
|
||||
return lake.shoreline.sort((a, b) => heights[a] - heights[b])[0];
|
||||
}
|
||||
};
|
||||
|
||||
features.forEach(feature => {
|
||||
features.forEach((feature) => {
|
||||
if (feature.type !== "lake") return;
|
||||
feature.flux = getFlux(feature);
|
||||
feature.temp = getLakeTemp(feature);
|
||||
|
|
@ -82,14 +93,16 @@ export class LakesModule {
|
|||
});
|
||||
|
||||
return lakeOutCells;
|
||||
};
|
||||
}
|
||||
|
||||
// check if lake can be potentially open (not in deep depression)
|
||||
detectCloseLakes(h: number[] | Uint8Array) {
|
||||
const {cells} = pack;
|
||||
const ELEVATION_LIMIT = +(byId("lakeElevationLimitOutput") as HTMLInputElement)?.value;
|
||||
const { cells } = pack;
|
||||
const ELEVATION_LIMIT = +(
|
||||
byId("lakeElevationLimitOutput") as HTMLInputElement
|
||||
)?.value;
|
||||
|
||||
pack.features.forEach(feature => {
|
||||
pack.features.forEach((feature) => {
|
||||
if (feature.type !== "lake") return;
|
||||
delete feature.closed;
|
||||
|
||||
|
|
@ -100,7 +113,9 @@ export class LakesModule {
|
|||
}
|
||||
|
||||
let isDeep = true;
|
||||
const lowestShorelineCell = feature.shoreline.sort((a, b) => h[a] - h[b])[0];
|
||||
const lowestShorelineCell = feature.shoreline.sort(
|
||||
(a, b) => h[a] - h[b],
|
||||
)[0];
|
||||
const queue = [lowestShorelineCell];
|
||||
const checked = [];
|
||||
checked[lowestShorelineCell] = true;
|
||||
|
|
@ -114,7 +129,8 @@ export class LakesModule {
|
|||
|
||||
if (h[neibCellId] < 20) {
|
||||
const nFeature = pack.features[cells.f[neibCellId]];
|
||||
if (nFeature.type === "ocean" || feature.height > nFeature.height) isDeep = false;
|
||||
if (nFeature.type === "ocean" || feature.height > nFeature.height)
|
||||
isDeep = false;
|
||||
}
|
||||
|
||||
checked[neibCellId] = true;
|
||||
|
|
@ -124,7 +140,7 @@ export class LakesModule {
|
|||
|
||||
feature.closed = isDeep;
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
window.Lakes = new LakesModule();
|
||||
window.Lakes = new LakesModule();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { line, curveBasisClosed } from 'd3';
|
||||
import type { Selection } from 'd3';
|
||||
import { clipPoly,P,rn,round } from '../utils';
|
||||
import type { Selection } from "d3";
|
||||
import { curveBasisClosed, line } from "d3";
|
||||
import { clipPoly, P, rn, round } from "../utils";
|
||||
|
||||
declare global {
|
||||
var OceanLayers: typeof OceanModule.prototype.draw;
|
||||
|
|
@ -13,7 +13,6 @@ class OceanModule {
|
|||
private lineGen = line().curve(curveBasisClosed);
|
||||
private oceanLayers: Selection<SVGGElement, unknown, null, undefined>;
|
||||
|
||||
|
||||
constructor(oceanLayers: Selection<SVGGElement, unknown, null, undefined>) {
|
||||
this.oceanLayers = oceanLayers;
|
||||
}
|
||||
|
|
@ -35,11 +34,17 @@ class OceanModule {
|
|||
// connect vertices to chain
|
||||
connectVertices(start: number, t: number) {
|
||||
const chain = []; // vertices chain to form a path
|
||||
for (let i = 0, current = start; i === 0 || (current !== start && i < 10000); i++) {
|
||||
for (
|
||||
let i = 0, current = start;
|
||||
i === 0 || (current !== start && i < 10000);
|
||||
i++
|
||||
) {
|
||||
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||
chain.push(current); // add current vertex to sequence
|
||||
const c = this.vertices.c[current]; // cells adjacent to vertex
|
||||
c.filter((c: number) => this.cells.t[c] === t).forEach((c: number) => (this.used[c] = 1));
|
||||
c.filter((c: number) => this.cells.t[c] === t).forEach((c: number) => {
|
||||
this.used[c] = 1;
|
||||
});
|
||||
const v = this.vertices.v[current]; // neighboring vertices
|
||||
const c0 = !this.cells.t[c[0]] || this.cells.t[c[0]] === t - 1;
|
||||
const c1 = !this.cells.t[c[1]] || this.cells.t[c[1]] === t - 1;
|
||||
|
|
@ -58,9 +63,16 @@ class OceanModule {
|
|||
|
||||
// find eligible cell vertex to start path detection
|
||||
findStart(i: number, t: number) {
|
||||
if (this.cells.b[i]) return this.cells.v[i].find((v: number) => this.vertices.c[v].some((c: number) => c >= this.pointsN)); // map border cell
|
||||
return this.cells.v[i][this.cells.c[i].findIndex((c: number)=> this.cells.t[c] < t || !this.cells.t[c])];
|
||||
}
|
||||
if (this.cells.b[i])
|
||||
return this.cells.v[i].find((v: number) =>
|
||||
this.vertices.c[v].some((c: number) => c >= this.pointsN),
|
||||
); // map border cell
|
||||
return this.cells.v[i][
|
||||
this.cells.c[i].findIndex(
|
||||
(c: number) => this.cells.t[c] < t || !this.cells.t[c],
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
draw() {
|
||||
const outline = this.oceanLayers.attr("layers");
|
||||
|
|
@ -69,8 +81,11 @@ class OceanModule {
|
|||
this.cells = grid.cells;
|
||||
this.pointsN = grid.cells.i.length;
|
||||
this.vertices = grid.vertices;
|
||||
const limits = outline === "random" ? this.randomizeOutline() : outline.split(",").map((s: string) => +s);
|
||||
|
||||
const limits =
|
||||
outline === "random"
|
||||
? this.randomizeOutline()
|
||||
: outline.split(",").map((s: string) => +s);
|
||||
|
||||
const chains: [number, any[]][] = [];
|
||||
const opacity = rn(0.4 / limits.length, 2);
|
||||
this.used = new Uint8Array(this.pointsN); // to detect already passed cells
|
||||
|
|
@ -85,22 +100,33 @@ class OceanModule {
|
|||
const chain = this.connectVertices(start, t); // vertices chain to form a path
|
||||
if (chain.length < 4) continue;
|
||||
const relax = 1 + t * -2; // select only n-th point
|
||||
const relaxed = chain.filter((v, i) => !(i % relax) || this.vertices.c[v].some((c: number) => c >= this.pointsN));
|
||||
const relaxed = chain.filter(
|
||||
(v, i) =>
|
||||
!(i % relax) ||
|
||||
this.vertices.c[v].some((c: number) => c >= this.pointsN),
|
||||
);
|
||||
if (relaxed.length < 4) continue;
|
||||
|
||||
|
||||
const points = clipPoly(
|
||||
relaxed.map(v => this.vertices.p[v]),
|
||||
relaxed.map((v) => this.vertices.p[v]),
|
||||
graphWidth,
|
||||
graphHeight,
|
||||
1
|
||||
1,
|
||||
);
|
||||
chains.push([t, points]);
|
||||
}
|
||||
|
||||
for (const t of limits) {
|
||||
const layer = chains.filter((c: [number, any[]]) => c[0] === t);
|
||||
let path = layer.map((c: [number, any[]]) => round(this.lineGen(c[1]) || "")).join("");
|
||||
if (path) this.oceanLayers.append("path").attr("d", path).attr("fill", "#ecf2f9").attr("fill-opacity", opacity);
|
||||
const path = layer
|
||||
.map((c: [number, any[]]) => round(this.lineGen(c[1]) || ""))
|
||||
.join("");
|
||||
if (path)
|
||||
this.oceanLayers
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", "#ecf2f9")
|
||||
.attr("fill-opacity", opacity);
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("drawOceanLayers");
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import Alea from "alea";
|
||||
import { each, rn, round, rw} from "../utils";
|
||||
import { curveBasis, line, mean, min, sum, curveCatmullRom } from "d3";
|
||||
|
||||
|
||||
import { curveBasis, curveCatmullRom, line, mean, min, sum } from "d3";
|
||||
import { each, rn, round, rw } from "../utils";
|
||||
|
||||
declare global {
|
||||
var Rivers: RiverModule;
|
||||
|
|
@ -29,18 +27,20 @@ class RiverModule {
|
|||
private MAX_FLUX_WIDTH = 1;
|
||||
private LENGTH_FACTOR = 200;
|
||||
private LENGTH_STEP_WIDTH = 1 / this.LENGTH_FACTOR;
|
||||
private LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / this.LENGTH_FACTOR);
|
||||
private lineGen = line().curve(curveBasis)
|
||||
private LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(
|
||||
(n) => n / this.LENGTH_FACTOR,
|
||||
);
|
||||
private lineGen = line().curve(curveBasis);
|
||||
|
||||
riverTypes = {
|
||||
main: {
|
||||
big: {River: 1},
|
||||
small: {Creek: 9, River: 3, Brook: 3, Stream: 1}
|
||||
big: { River: 1 },
|
||||
small: { Creek: 9, River: 3, Brook: 3, Stream: 1 },
|
||||
},
|
||||
fork: {
|
||||
big: {Fork: 1},
|
||||
small: {Branch: 1}
|
||||
}
|
||||
big: { Fork: 1 },
|
||||
small: { Branch: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
smallLength: number | null = null;
|
||||
|
|
@ -48,10 +48,10 @@ class RiverModule {
|
|||
generate(allowErosion = true) {
|
||||
TIME && console.time("generateRivers");
|
||||
Math.random = Alea(seed);
|
||||
const {cells, features} = pack;
|
||||
const { cells, features } = pack;
|
||||
|
||||
const riversData: {[riverId: number]: number[]} = {};
|
||||
const riverParents: {[key: number]: number} = {};
|
||||
const riversData: { [riverId: number]: number[] } = {};
|
||||
const riverParents: { [key: number]: number } = {};
|
||||
|
||||
const addCellToRiver = (cellId: number, riverId: number) => {
|
||||
if (!riversData[riverId]) riversData[riverId] = [cellId];
|
||||
|
|
@ -60,26 +60,36 @@ class RiverModule {
|
|||
|
||||
const drainWater = () => {
|
||||
const MIN_FLUX_TO_FORM_RIVER = 30;
|
||||
const cellsNumberModifier = ((pointsInput.dataset.cells as any) / 10000) ** 0.25;
|
||||
const cellsNumberModifier =
|
||||
((pointsInput.dataset.cells as any) / 10000) ** 0.25;
|
||||
|
||||
const prec = grid.cells.prec;
|
||||
const land = cells.i.filter((i: number) => h[i] >= 20).sort((a: number, b: number) => h[b] - h[a]);
|
||||
const land = cells.i
|
||||
.filter((i: number) => h[i] >= 20)
|
||||
.sort((a: number, b: number) => h[b] - h[a]);
|
||||
const lakeOutCells = Lakes.defineClimateData(h);
|
||||
|
||||
land.forEach(function (i: number) {
|
||||
for (const i of land) {
|
||||
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
|
||||
|
||||
// create lake outlet if lake is not in deep depression and flux > evaporation
|
||||
const lakes = lakeOutCells[i]
|
||||
? features.filter((feature: any) => i === feature.outCell && feature.flux > feature.evaporation)
|
||||
? features.filter(
|
||||
(feature: any) =>
|
||||
i === feature.outCell && feature.flux > feature.evaporation,
|
||||
)
|
||||
: [];
|
||||
for (const lake of lakes) {
|
||||
const lakeCell = cells.c[i].find((c: number) => h[c] < 20 && cells.f[c] === lake.i)!;
|
||||
const lakeCell = cells.c[i].find(
|
||||
(c: number) => h[c] < 20 && cells.f[c] === lake.i,
|
||||
)!;
|
||||
cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
|
||||
|
||||
// allow chain lakes to retain identity
|
||||
if (cells.r[lakeCell] !== lake.river) {
|
||||
const sameRiver = cells.c[lakeCell].some((c: number) => cells.r[c] === lake.river);
|
||||
const sameRiver = cells.c[lakeCell].some(
|
||||
(c: number) => cells.r[c] === lake.river,
|
||||
);
|
||||
|
||||
if (sameRiver) {
|
||||
cells.r[lakeCell] = lake.river as number;
|
||||
|
|
@ -105,12 +115,18 @@ class RiverModule {
|
|||
}
|
||||
|
||||
// near-border cell: pour water out of the screen
|
||||
if (cells.b[i] && cells.r[i]) return addCellToRiver(-1, cells.r[i]);
|
||||
if (cells.b[i] && cells.r[i]) {
|
||||
addCellToRiver(-1, cells.r[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// downhill cell (make sure it's not in the source lake)
|
||||
let min = null;
|
||||
if (lakeOutCells[i]) {
|
||||
const filtered = cells.c[i].filter((c: number) => !lakes.map((lake: any) => lake.i).includes(cells.f[c]));
|
||||
const filtered = cells.c[i].filter(
|
||||
(c: number) =>
|
||||
!lakes.map((lake: any) => lake.i).includes(cells.f[c]),
|
||||
);
|
||||
min = filtered.sort((a: number, b: number) => h[a] - h[b])[0];
|
||||
} else if (cells.haven[i]) {
|
||||
min = cells.haven[i];
|
||||
|
|
@ -119,7 +135,7 @@ class RiverModule {
|
|||
}
|
||||
|
||||
// cells is depressed
|
||||
if (h[i] <= h[min]) return;
|
||||
if (h[i] <= h[min]) continue;
|
||||
|
||||
// debug
|
||||
// .append("line")
|
||||
|
|
@ -133,7 +149,7 @@ class RiverModule {
|
|||
if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
|
||||
// flux is too small to operate as a river
|
||||
if (h[min] >= 20) cells.fl[min] += cells.fl[i];
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
// proclaim a new river
|
||||
|
|
@ -144,8 +160,8 @@ class RiverModule {
|
|||
}
|
||||
|
||||
flowDown(min, cells.fl[i], cells.r[i]);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const flowDown = (toCell: number, fromFlux: number, river: number) => {
|
||||
const toFlux = cells.fl[toCell] - cells.conf[toCell];
|
||||
|
|
@ -167,7 +183,10 @@ class RiverModule {
|
|||
// pour water to the water body
|
||||
const waterBody = features[cells.f[toCell]];
|
||||
if (waterBody.type === "lake") {
|
||||
if (!waterBody.river || fromFlux > (waterBody.enteringFlux as number)) {
|
||||
if (
|
||||
!waterBody.river ||
|
||||
fromFlux > (waterBody.enteringFlux as number)
|
||||
) {
|
||||
waterBody.river = river;
|
||||
waterBody.enteringFlux = fromFlux;
|
||||
}
|
||||
|
|
@ -181,7 +200,7 @@ class RiverModule {
|
|||
}
|
||||
|
||||
addCellToRiver(toCell, river);
|
||||
}
|
||||
};
|
||||
|
||||
const defineRivers = () => {
|
||||
// re-initialize rivers and confluence arrays
|
||||
|
|
@ -189,7 +208,10 @@ class RiverModule {
|
|||
cells.conf = new Uint16Array(cells.i.length);
|
||||
pack.rivers = [];
|
||||
|
||||
const defaultWidthFactor = rn(1 / ((pointsInput.dataset.cells as any) / 10000) ** 0.25, 2);
|
||||
const defaultWidthFactor = rn(
|
||||
1 / ((pointsInput.dataset.cells as any) / 10000) ** 0.25,
|
||||
2,
|
||||
);
|
||||
const mainStemWidthFactor = defaultWidthFactor * 1.2;
|
||||
|
||||
for (const key in riversData) {
|
||||
|
|
@ -209,7 +231,10 @@ class RiverModule {
|
|||
const mouth = riverCells[riverCells.length - 2];
|
||||
const parent = riverParents[key] || 0;
|
||||
|
||||
const widthFactor = !parent || parent === riverId ? mainStemWidthFactor : defaultWidthFactor;
|
||||
const widthFactor =
|
||||
!parent || parent === riverId
|
||||
? mainStemWidthFactor
|
||||
: defaultWidthFactor;
|
||||
const meanderedPoints = this.addMeandering(riverCells);
|
||||
const discharge = cells.fl[mouth]; // m3 in second
|
||||
const length = this.getApproximateLength(meanderedPoints);
|
||||
|
|
@ -219,8 +244,8 @@ class RiverModule {
|
|||
flux: discharge,
|
||||
pointIndex: meanderedPoints.length,
|
||||
widthFactor,
|
||||
startingWidth: sourceWidth
|
||||
})
|
||||
startingWidth: sourceWidth,
|
||||
}),
|
||||
);
|
||||
|
||||
pack.rivers.push({
|
||||
|
|
@ -233,10 +258,10 @@ class RiverModule {
|
|||
widthFactor,
|
||||
sourceWidth,
|
||||
parent,
|
||||
cells: riverCells
|
||||
cells: riverCells,
|
||||
} as River);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const downcutRivers = () => {
|
||||
const MAX_DOWNCUT = 5;
|
||||
|
|
@ -245,14 +270,18 @@ class RiverModule {
|
|||
if (cells.h[i] < 35) continue; // don't donwcut lowlands
|
||||
if (!cells.fl[i]) continue;
|
||||
|
||||
const higherCells = cells.c[i].filter((c: number) => cells.h[c] > cells.h[i]);
|
||||
const higherFlux = higherCells.reduce((acc: number, c: number) => acc + cells.fl[c], 0) / higherCells.length;
|
||||
const higherCells = cells.c[i].filter(
|
||||
(c: number) => cells.h[c] > cells.h[i],
|
||||
);
|
||||
const higherFlux =
|
||||
higherCells.reduce((acc: number, c: number) => acc + cells.fl[c], 0) /
|
||||
higherCells.length;
|
||||
if (!higherFlux) continue;
|
||||
|
||||
const downcut = Math.floor(cells.fl[i] / higherFlux);
|
||||
if (downcut) cells.h[i] -= Math.min(downcut, MAX_DOWNCUT);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const calculateConfluenceFlux = () => {
|
||||
for (const i of cells.i) {
|
||||
|
|
@ -262,9 +291,13 @@ class RiverModule {
|
|||
.filter((c: number) => cells.r[c] && h[c] > h[i])
|
||||
.map((c: number) => cells.fl[c])
|
||||
.sort((a: number, b: number) => b - a);
|
||||
cells.conf[i] = sortedInflux.reduce((acc: number, flux: number, index: number) => (index ? acc + flux : acc), 0);
|
||||
cells.conf[i] = sortedInflux.reduce(
|
||||
(acc: number, flux: number, index: number) =>
|
||||
index ? acc + flux : acc,
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cells.fl = new Uint16Array(cells.i.length); // water flux array
|
||||
cells.r = new Uint16Array(cells.i.length); // rivers array
|
||||
|
|
@ -286,20 +319,28 @@ class RiverModule {
|
|||
}
|
||||
|
||||
TIME && console.timeEnd("generateRivers");
|
||||
};
|
||||
}
|
||||
|
||||
alterHeights(): number[] {
|
||||
const {h, c, t} = pack.cells as {h: Uint8Array, c: number[][], t: Uint8Array};
|
||||
const { h, c, t } = pack.cells as {
|
||||
h: Uint8Array;
|
||||
c: number[][];
|
||||
t: Uint8Array;
|
||||
};
|
||||
return Array.from(h).map((h, i) => {
|
||||
if (h < 20 || t[i] < 1) return h;
|
||||
return h + t[i] / 100 + (mean(c[i].map(c => t[c])) as number) / 10000;
|
||||
return h + t[i] / 100 + (mean(c[i].map((c) => t[c])) as number) / 10000;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// depression filling algorithm (for a correct water flux modeling)
|
||||
resolveDepressions(h: number[]) {
|
||||
const {cells, features} = pack;
|
||||
const maxIterations = +(document.getElementById("resolveDepressionsStepsOutput") as HTMLInputElement)?.value;
|
||||
const { cells, features } = pack;
|
||||
const maxIterations = +(
|
||||
document.getElementById(
|
||||
"resolveDepressionsStepsOutput",
|
||||
) as HTMLInputElement
|
||||
)?.value;
|
||||
const checkLakeMaxIteration = maxIterations * 0.85;
|
||||
const elevateLakeMaxIteration = maxIterations * 0.75;
|
||||
|
||||
|
|
@ -312,7 +353,11 @@ class RiverModule {
|
|||
const progress = [];
|
||||
let depressions = Infinity;
|
||||
let prevDepressions = null;
|
||||
for (let iteration = 0; depressions && iteration < maxIterations; iteration++) {
|
||||
for (
|
||||
let iteration = 0;
|
||||
depressions && iteration < maxIterations;
|
||||
iteration++
|
||||
) {
|
||||
if (progress.length > 5 && sum(progress) > 0) {
|
||||
// bad progress, abort and set heights back
|
||||
h = this.alterHeights();
|
||||
|
|
@ -329,8 +374,11 @@ class RiverModule {
|
|||
if (minHeight >= 100 || l.height > minHeight) continue;
|
||||
|
||||
if (iteration > elevateLakeMaxIteration) {
|
||||
l.shoreline.forEach((i: number) => (h[i] = cells.h[i]));
|
||||
l.height = (min(l.shoreline.map((s: number) => h[s])) as number) - 1;
|
||||
l.shoreline.forEach((i: number) => {
|
||||
h[i] = cells.h[i];
|
||||
});
|
||||
l.height =
|
||||
(min(l.shoreline.map((s: number) => h[s])) as number) - 1;
|
||||
l.closed = true;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -341,7 +389,9 @@ class RiverModule {
|
|||
}
|
||||
|
||||
for (const i of land) {
|
||||
const minHeight = min(cells.c[i].map((c: number) => height(c))) as number;
|
||||
const minHeight = min(
|
||||
cells.c[i].map((c: number) => height(c)),
|
||||
) as number;
|
||||
if (minHeight >= 100 || h[i] > minHeight) continue;
|
||||
|
||||
depressions++;
|
||||
|
|
@ -352,11 +402,19 @@ class RiverModule {
|
|||
prevDepressions = depressions;
|
||||
}
|
||||
|
||||
depressions && WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`);
|
||||
};
|
||||
depressions &&
|
||||
WARN &&
|
||||
console.warn(
|
||||
`Unresolved depressions: ${depressions}. Edit heightmap to fix`,
|
||||
);
|
||||
}
|
||||
|
||||
addMeandering(riverCells: number[], riverPoints = null, meandering = 0.5): [number, number, number][] {
|
||||
const {fl, h} = pack.cells;
|
||||
addMeandering(
|
||||
riverCells: number[],
|
||||
riverPoints = null,
|
||||
meandering = 0.5,
|
||||
): [number, number, number][] {
|
||||
const { fl, h } = pack.cells;
|
||||
const meandered = [];
|
||||
const lastStep = riverCells.length - 1;
|
||||
const points = this.getRiverPoints(riverCells, riverPoints);
|
||||
|
|
@ -382,7 +440,8 @@ class RiverModule {
|
|||
const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
|
||||
if (dist2 <= 25 && riverCells.length >= 6) continue;
|
||||
|
||||
const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
|
||||
const meander =
|
||||
meandering + 1 / step + Math.max(meandering - step / 100, 0);
|
||||
const angle = Math.atan2(y2 - y1, x2 - x1);
|
||||
const sinMeander = Math.sin(angle) * meander;
|
||||
const cosMeander = Math.cos(angle) * meander;
|
||||
|
|
@ -403,17 +462,17 @@ class RiverModule {
|
|||
}
|
||||
|
||||
return meandered as [number, number, number][];
|
||||
};
|
||||
}
|
||||
|
||||
getRiverPoints(riverCells: number[], riverPoints: [number, number][] | null) {
|
||||
if (riverPoints) return riverPoints;
|
||||
|
||||
const {p} = pack.cells;
|
||||
const { p } = pack.cells;
|
||||
return riverCells.map((cell, i) => {
|
||||
if (cell === -1) return this.getBorderPoint(riverCells[i - 1]);
|
||||
return p[cell];
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
getBorderPoint(i: number) {
|
||||
const [x, y] = pack.cells.p[i];
|
||||
|
|
@ -422,22 +481,42 @@ class RiverModule {
|
|||
else if (min === graphHeight - y) return [x, graphHeight];
|
||||
else if (min === x) return [0, y];
|
||||
return [graphWidth, y];
|
||||
};
|
||||
}
|
||||
|
||||
getOffset({flux, pointIndex, widthFactor, startingWidth}: {flux: number, pointIndex: number, widthFactor: number, startingWidth: number}) {
|
||||
getOffset({
|
||||
flux,
|
||||
pointIndex,
|
||||
widthFactor,
|
||||
startingWidth,
|
||||
}: {
|
||||
flux: number;
|
||||
pointIndex: number;
|
||||
widthFactor: number;
|
||||
startingWidth: number;
|
||||
}) {
|
||||
if (pointIndex === 0) return startingWidth;
|
||||
|
||||
const fluxWidth = Math.min(flux ** 0.7 / this.FLUX_FACTOR, this.MAX_FLUX_WIDTH);
|
||||
const lengthWidth = pointIndex * this.LENGTH_STEP_WIDTH + (this.LENGTH_PROGRESSION[pointIndex] || this.LENGTH_PROGRESSION.at(-1) as number);
|
||||
const fluxWidth = Math.min(
|
||||
flux ** 0.7 / this.FLUX_FACTOR,
|
||||
this.MAX_FLUX_WIDTH,
|
||||
);
|
||||
const lengthWidth =
|
||||
pointIndex * this.LENGTH_STEP_WIDTH +
|
||||
(this.LENGTH_PROGRESSION[pointIndex] ||
|
||||
(this.LENGTH_PROGRESSION.at(-1) as number));
|
||||
return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
|
||||
};
|
||||
}
|
||||
|
||||
getSourceWidth(flux: number) {
|
||||
return rn(Math.min(flux ** 0.9 / this.FLUX_FACTOR, this.MAX_FLUX_WIDTH), 2);
|
||||
}
|
||||
|
||||
// build polygon from a list of points and calculated offset (width)
|
||||
getRiverPath(points: [number, number, number][], widthFactor: number, startingWidth: number) {
|
||||
getRiverPath(
|
||||
points: [number, number, number][],
|
||||
widthFactor: number,
|
||||
startingWidth: number,
|
||||
) {
|
||||
this.lineGen.curve(curveCatmullRom.alpha(0.1));
|
||||
const riverPointsLeft: [number, number][] = [];
|
||||
const riverPointsRight: [number, number][] = [];
|
||||
|
|
@ -449,7 +528,12 @@ class RiverModule {
|
|||
const [x2, y2] = points[pointIndex + 1] || points[pointIndex];
|
||||
if (pointFlux > flux) flux = pointFlux;
|
||||
|
||||
const offset = this.getOffset({flux, pointIndex, widthFactor, startingWidth});
|
||||
const offset = this.getOffset({
|
||||
flux,
|
||||
pointIndex,
|
||||
widthFactor,
|
||||
startingWidth,
|
||||
});
|
||||
const angle = Math.atan2(y0 - y2, x0 - x2);
|
||||
const sinOffset = Math.sin(angle) * offset;
|
||||
const cosOffset = Math.cos(angle) * offset;
|
||||
|
|
@ -463,7 +547,7 @@ class RiverModule {
|
|||
left = left.substring(left.indexOf("C"));
|
||||
|
||||
return round(right + left, 1);
|
||||
};
|
||||
}
|
||||
|
||||
specify() {
|
||||
const rivers = pack.rivers;
|
||||
|
|
@ -474,57 +558,69 @@ class RiverModule {
|
|||
river.name = this.getName(river.mouth);
|
||||
river.type = this.getType(river);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getName(cell: number) {
|
||||
return Names.getCulture(pack.cells.culture[cell]);
|
||||
};
|
||||
}
|
||||
|
||||
getType({i, length, parent}: River) {
|
||||
getType({ i, length, parent }: River) {
|
||||
if (this.smallLength === null) {
|
||||
const threshold = Math.ceil(pack.rivers.length * 0.15);
|
||||
this.smallLength = pack.rivers.map(r => r.length || 0).sort((a: number, b: number) => a - b)[threshold];
|
||||
this.smallLength = pack.rivers
|
||||
.map((r) => r.length || 0)
|
||||
.sort((a: number, b: number) => a - b)[threshold];
|
||||
}
|
||||
|
||||
const isSmall: boolean = length < (this.smallLength as number);
|
||||
const isFork = each(3)(i) && parent && parent !== i;
|
||||
return rw(this.riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"]);
|
||||
};
|
||||
return rw(
|
||||
this.riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"],
|
||||
);
|
||||
}
|
||||
|
||||
getApproximateLength(points: [number, number, number][]) {
|
||||
const length = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
|
||||
const length = points.reduce(
|
||||
(s, v, i, p) =>
|
||||
s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0),
|
||||
0,
|
||||
);
|
||||
return rn(length, 2);
|
||||
};
|
||||
}
|
||||
|
||||
// Real mouth width examples: Amazon 6000m, Volga 6000m, Dniepr 3000m, Mississippi 1300m, Themes 900m,
|
||||
// Danube 800m, Daugava 600m, Neva 500m, Nile 450m, Don 400m, Wisla 300m, Pripyat 150m, Bug 140m, Muchavets 40m
|
||||
getWidth(offset: number) {
|
||||
return rn((offset / 1.5) ** 1.8, 2); // mouth width in km
|
||||
};
|
||||
return rn((offset / 1.5) ** 1.8, 2); // mouth width in km
|
||||
}
|
||||
|
||||
// remove river and all its tributaries
|
||||
remove(id: number) {
|
||||
const cells = pack.cells;
|
||||
const riversToRemove = pack.rivers.filter(r => r.i === id || r.parent === id || r.basin === id).map(r => r.i);
|
||||
riversToRemove.forEach(r => rivers.select("#river" + r).remove());
|
||||
const riversToRemove = pack.rivers
|
||||
.filter((r) => r.i === id || r.parent === id || r.basin === id)
|
||||
.map((r) => r.i);
|
||||
riversToRemove.forEach((r) => {
|
||||
rivers.select(`#river${r}`).remove();
|
||||
});
|
||||
cells.r.forEach((r, i) => {
|
||||
if (!r || !riversToRemove.includes(r)) return;
|
||||
cells.r[i] = 0;
|
||||
cells.fl[i] = grid.cells.prec[cells.g[i]];
|
||||
cells.conf[i] = 0;
|
||||
});
|
||||
pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
|
||||
};
|
||||
pack.rivers = pack.rivers.filter((r) => !riversToRemove.includes(r.i));
|
||||
}
|
||||
|
||||
getBasin(r: number): number {
|
||||
const parent = pack.rivers.find(river => river.i === r)?.parent;
|
||||
const parent = pack.rivers.find((river) => river.i === r)?.parent;
|
||||
if (!parent || r === parent) return r;
|
||||
return this.getBasin(parent);
|
||||
};
|
||||
}
|
||||
|
||||
getNextId(rivers: {i: number}[]) {
|
||||
return rivers.length ? Math.max(...rivers.map(r => r.i)) + 1 : 1;
|
||||
};
|
||||
getNextId(rivers: { i: number }[]) {
|
||||
return rivers.length ? Math.max(...rivers.map((r) => r.i)) + 1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
window.Rivers = new RiverModule()
|
||||
window.Rivers = new RiverModule();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import Delaunator from "delaunator";
|
||||
export type Vertices = { p: Point[], v: number[][], c: number[][] };
|
||||
export type Cells = { v: number[][], c: number[][], b: number[], i: Uint32Array<ArrayBufferLike> } ;
|
||||
import type Delaunator from "delaunator";
|
||||
export type Vertices = { p: Point[]; v: number[][]; c: number[][] };
|
||||
export type Cells = {
|
||||
v: number[][];
|
||||
c: number[][];
|
||||
b: number[];
|
||||
i: Uint32Array<ArrayBufferLike>;
|
||||
};
|
||||
export type Point = [number, number];
|
||||
|
||||
/**
|
||||
|
|
@ -11,36 +16,41 @@ export type Point = [number, number];
|
|||
* @param {number} pointsN The number of points.
|
||||
*/
|
||||
export class Voronoi {
|
||||
delaunay: Delaunator<Float64Array<ArrayBufferLike>>
|
||||
delaunay: Delaunator<Float64Array<ArrayBufferLike>>;
|
||||
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<Float64Array<ArrayBufferLike>>, points: Point[], pointsN: number) {
|
||||
|
||||
constructor(
|
||||
delaunay: Delaunator<Float64Array<ArrayBufferLike>>,
|
||||
points: Point[],
|
||||
pointsN: number,
|
||||
) {
|
||||
this.delaunay = delaunay;
|
||||
this.points = points;
|
||||
this.pointsN = pointsN;
|
||||
this.vertices
|
||||
this.vertices;
|
||||
|
||||
// Half-edges are the indices into the delaunator outputs:
|
||||
// delaunay.triangles[e] gives the point ID where the half-edge starts
|
||||
// delaunay.halfedges[e] returns either the opposite half-edge in the adjacent triangle, or -1 if there's not an adjacent triangle.
|
||||
for (let e = 0; e < this.delaunay.triangles.length; e++) {
|
||||
|
||||
const p = this.delaunay.triangles[this.nextHalfedge(e)];
|
||||
if (p < this.pointsN && !this.cells.c[p]) {
|
||||
const edges = this.edgesAroundPoint(e);
|
||||
this.cells.v[p] = edges.map(e => this.triangleOfEdge(e)); // cell: adjacent vertex
|
||||
this.cells.c[p] = edges.map(e => this.delaunay.triangles[e]).filter(c => c < this.pointsN); // cell: adjacent valid cells
|
||||
this.cells.b[p] = edges.length > this.cells.c[p].length ? 1 : 0; // cell: is border
|
||||
this.cells.v[p] = edges.map((e) => this.triangleOfEdge(e)); // cell: adjacent vertex
|
||||
this.cells.c[p] = edges
|
||||
.map((e) => this.delaunay.triangles[e])
|
||||
.filter((c) => c < this.pointsN); // cell: adjacent valid cells
|
||||
this.cells.b[p] = edges.length > this.cells.c[p].length ? 1 : 0; // cell: is border
|
||||
}
|
||||
|
||||
const t = this.triangleOfEdge(e);
|
||||
if (!this.vertices.p[t]) {
|
||||
this.vertices.p[t] = this.triangleCenter(t); // vertex: coordinates
|
||||
this.vertices.p[t] = this.triangleCenter(t); // vertex: coordinates
|
||||
this.vertices.v[t] = this.trianglesAdjacentToTriangle(t); // vertex: adjacent vertices
|
||||
this.vertices.c[t] = this.pointsOfTriangle(t); // vertex: adjacent cells
|
||||
this.vertices.c[t] = this.pointsOfTriangle(t); // vertex: adjacent cells
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -51,7 +61,9 @@ export class Voronoi {
|
|||
* @returns {[number, number, number]} The IDs of the points comprising the given triangle.
|
||||
*/
|
||||
private pointsOfTriangle(triangleIndex: number): [number, number, number] {
|
||||
return this.edgesOfTriangle(triangleIndex).map(edge => this.delaunay.triangles[edge]) as [number, number, number];
|
||||
return this.edgesOfTriangle(triangleIndex).map(
|
||||
(edge) => this.delaunay.triangles[edge],
|
||||
) as [number, number, number];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -60,9 +72,9 @@ export class Voronoi {
|
|||
* @returns {number[]} The indices of the triangles that share half-edges with this triangle.
|
||||
*/
|
||||
private trianglesAdjacentToTriangle(triangleIndex: number): number[] {
|
||||
let triangles = [];
|
||||
for (let edge of this.edgesOfTriangle(triangleIndex)) {
|
||||
let opposite = this.delaunay.halfedges[edge];
|
||||
const triangles = [];
|
||||
for (const edge of this.edgesOfTriangle(triangleIndex)) {
|
||||
const opposite = this.delaunay.halfedges[edge];
|
||||
triangles.push(this.triangleOfEdge(opposite));
|
||||
}
|
||||
return triangles;
|
||||
|
|
@ -90,7 +102,9 @@ export class Voronoi {
|
|||
* @returns {[number, number]} The coordinates of the triangle's circumcenter.
|
||||
*/
|
||||
private triangleCenter(triangleIndex: number): Point {
|
||||
let vertices = this.pointsOfTriangle(triangleIndex).map(p => this.points[p]);
|
||||
const vertices = this.pointsOfTriangle(triangleIndex).map(
|
||||
(p) => this.points[p],
|
||||
);
|
||||
return this.circumcenter(vertices[0], vertices[1], vertices[2]);
|
||||
}
|
||||
|
||||
|
|
@ -99,21 +113,27 @@ export class Voronoi {
|
|||
* @param {number} triangleIndex The index of the triangle
|
||||
* @returns {[number, number, number]} The edges of the triangle.
|
||||
*/
|
||||
private edgesOfTriangle(triangleIndex: number): [number, number, number] { return [3 * triangleIndex, 3 * triangleIndex + 1, 3 * triangleIndex + 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
|
||||
*/
|
||||
private triangleOfEdge(e: number): number { 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
|
||||
*/
|
||||
private nextHalfedge(e: number): number { 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.}
|
||||
|
|
@ -138,8 +158,8 @@ export class Voronoi {
|
|||
const cd = cx * cx + cy * cy;
|
||||
const D = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
|
||||
return [
|
||||
Math.floor(1 / D * (ad * (by - cy) + bd * (cy - ay) + cd * (ay - by))),
|
||||
Math.floor(1 / D * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax)))
|
||||
Math.floor((1 / D) * (ad * (by - cy) + bd * (cy - ay) + cd * (ay - by))),
|
||||
Math.floor((1 / D) * (ad * (cx - bx) + bd * (ax - cx) + cd * (bx - ax))),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue